Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
175 kB · 4569 lines
Zig
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162416341644165416641674168416941704171417241734174417541764177417841794180418141824183418441854186418741884189419041914192419341944195419641974198419942004201420242034204420542064207420842094210421142124213421442154216421742184219422042214222422342244225422642274228422942304231423242334234423542364237423842394240424142424243424442454246424742484249425042514252425342544255425642574258425942604261426242634264426542664267426842694270427142724273427442754276427742784279428042814282428342844285428642874288428942904291429242934294429542964297429842994300430143024303430443054306430743084309431043114312431343144315431643174318431943204321432243234324432543264327432843294330433143324333433443354336433743384339434043414342434343444345434643474348434943504351435243534354435543564357435843594360436143624363436443654366436743684369437043714372437343744375437643774378437943804381438243834384438543864387438843894390439143924393439443954396439743984399440044014402440344044405440644074408440944104411441244134414441544164417441844194420442144224423442444254426442744284429443044314432443344344435443644374438443944404441444244434444444544464447444844494450445144524453445444554456445744584459446044614462446344644465446644674468446944704471447244734474447544764477447844794480448144824483448444854486448744884489449044914492449344944495449644974498449945004501450245034504450545064507450845094510451145124513451445154516451745184519452045214522452345244525452645274528452945304531453245334534453545364537453845394540454145424543454445454546454745484549455045514552455345544555455645574558455945604561456245634564456545664567456845694570const std = @import("std");const build_checkpoint = @import("build_checkpoint.zig");const debug_trace = @import("../../core/shared/debug_trace.zig");const display_width = @import("../../core/shared/display_width.zig");const types = @import("../../core/shared/types.zig");const command_output_content = @import("../../core/tooling/command_output_content.zig");const assistant_wrap = @import("assistant_wrap.zig");const transcript_measure = @import("transcript_measure.zig");const user_message_card = @import("../assistant/user_message_card.zig");const input_visual_layout = @import("../input/visual_layout.zig");const vt_emulator = @import("../../core/terminal/engine.zig");const assistant_presentation = @import("../../core/agent/assistant_presentation.zig");const code_highlight = @import("code_highlight.zig");const code_highlight_languages = @import("code_highlight_languages.zig");
const Allocator = std.mem.Allocator;
pub const WalkResult = transcript_measure.WalkResult;pub const walkText = transcript_measure.walkText;pub const nextCursorColForLine = transcript_measure.nextCursorColForLine;pub const skipVisualRowsInLine = transcript_measure.skipVisualRowsInLine;pub const visualRowsForLine = transcript_measure.visualRowsForLine;
pub const CommandOutputDisplayState = struct { open_block: ?usize = null, open_command_block: ?usize = null,};
pub const CodeHighlightTheme = code_highlight.Theme;
pub const Styles = struct { system_notice_label_style: []const u8 = "", system_notice_text_style: []const u8 = "", reset_style: []const u8 = "", dim_style: []const u8 = "", red_style: []const u8 = "", cancelled_text_style: []const u8 = "", notice_information_style: []const u8 = "", notice_success_style: []const u8 = "", notice_warning_style: []const u8 = "", notice_error_style: []const u8 = "", notice_cancelled_style: []const u8 = "", code_highlight_theme: CodeHighlightTheme = .dark,};
pub const ToolFallbackDisposition = enum { completion_unreported, not_executed,};
pub const ToolDetailRecord = struct { entry_id: u32, tool_name: []u8, captured_command: bool = false, activity_kind: ?types.ToolActivityKind = null, arguments_json: ?[]u8 = null, result: ?[]u8 = null, result_handle: ?[]u8 = null, command_artifact_handle: ?[]u8 = null, command_output_replay: ?types.CommandOutputReplay = null, command_process_presentation: ?types.CommandProcessPresentation = null, outcome: ?types.ToolOutcomeKind = null, fallback_disposition: ?ToolFallbackDisposition = null, lifecycle_id: ?types.ToolLifecycleId = null, presentation_group_id: ?types.ToolPresentationGroupId = null, command_output_entry_id: ?u32 = null,
pub fn isCapturedCommand(self: ToolDetailRecord) bool { // Hand-built details and restored records may predate the explicit // classifier. Preserve their historical presentation contract. return self.captured_command or std.mem.eql(u8, self.tool_name, "run_command"); }
pub fn deinit(self: *ToolDetailRecord, alloc: std.mem.Allocator) void { alloc.free(self.tool_name); if (self.arguments_json) |value| alloc.free(value); if (self.result) |value| alloc.free(value); if (self.result_handle) |value| alloc.free(value); if (self.command_artifact_handle) |value| alloc.free(value); if (self.command_output_replay) |replay| { types.freeCommandOutputReplay(alloc, replay); } if (self.lifecycle_id) |id| alloc.free(@constCast(id.call_id)); self.* = undefined; }};
pub const FullDetailAppend = struct { attached: bool = false, ends_with_newline: bool = false,};
pub const FullPresentationSink = struct { context: *anyopaque, skip_entry: *const fn (context: *anyopaque, entry: TranscriptEntry, index: usize) bool, override_kind: *const fn (context: *anyopaque, entry: TranscriptEntry) ?TranscriptBlockKind, append_override: *const fn (context: *anyopaque, entry: TranscriptEntry, out: *std.Io.Writer.Allocating) anyerror!bool, before_entry: *const fn (context: *anyopaque, entry_id: u32, out: *std.Io.Writer.Allocating) anyerror!void, record_code_action: ?*const fn (context: *anyopaque, entry_id: u32, span: CodeBlockActionSpan) anyerror!void = null, append_detail: *const fn (context: *anyopaque, entry_id: u32, out: *std.Io.Writer.Allocating) anyerror!FullDetailAppend,};
pub const AssistantTurnSegments = struct { /// Raw streamed text from the pacer, with inline SGR runs. No /// injected wraps. Paint-time reflow decides wrap boundaries /// for the current cols and reasserts SGR at each boundary so /// bold and colour never leak into continuation rows. text: std.ArrayList(u8) = .empty,
pub fn deinit(self: *AssistantTurnSegments, alloc: Allocator) void { self.text.deinit(alloc); }};
pub const RawEntryClass = enum { welcome, turn_summary, tool_status, command_output, diff_block, question_resolution, subagent_status, unknown_raw,};
pub const TranscriptBlockKind = enum { user_turn, assistant_turn, assistant_table, assistant_code_block, assistant_thematic_rule, turn_summary, welcome, tool_status, command_output, diff_block, system_notice, error_notice, cancel_notice, subagent_status, unknown_raw,};
pub const TranscriptEntryClass = enum { user_turn, assistant_turn, assistant_table, assistant_code_block, assistant_thematic_rule, welcome, turn_summary, tool_status, command_output, diff_block, system_notice, context_notice, error_notice, cancel_notice, subagent_status, unknown_raw,};
pub const LineProvenance = union(enum) { entry: struct { entry_id: u32, entry_class: TranscriptEntryClass, }, block_separator, boundary_blank, capped_continuation, folded_command_output: struct { entry_id: ?u32, stream: command_output_content.Stream, }, unattributed, empty_transcript,};
pub const RowProvenance = struct { row: u16, source: LineProvenance,};
pub fn blockKindForRawClass(class: RawEntryClass) TranscriptBlockKind { return switch (class) { .welcome => .welcome, .turn_summary => .turn_summary, .tool_status => .tool_status, .command_output => .command_output, .diff_block => .diff_block, .question_resolution => .cancel_notice, .subagent_status => .subagent_status, .unknown_raw => .unknown_raw, };}
pub fn blockKindForNoticeTone(tone: types.NoticeTone) TranscriptBlockKind { return switch (tone) { .information, .success, .warning, .neutral => .system_notice, .@"error" => .error_notice, .cancelled => .cancel_notice, };}
pub fn blockGapRowsBetween(prev: TranscriptBlockKind, next: TranscriptBlockKind) u16 { return default_block_gap_policy.gapBetween(prev, next);}
fn blockKindForEntry(entry: TranscriptEntry) TranscriptBlockKind { return switch (entry) { .raw_bytes => |e| blockKindForRawClass(e.class), .semantic_notice => |e| blockKindForNoticeTone(e.tone), .user_turn => .user_turn, .assistant_turn => .assistant_turn, .assistant_table => .assistant_table, .assistant_code_block => .assistant_code_block, .assistant_thematic_rule => .assistant_thematic_rule, };}
pub fn isEntryVisibleInCompactPresentation(entry: TranscriptEntry) bool { return switch (entry) { .raw_bytes => true, .semantic_notice => |notice| notice.visibility == .compact_and_full, else => true, };}
pub fn entryClassForEntry(entry: TranscriptEntry) TranscriptEntryClass { return switch (entry) { .user_turn => .user_turn, .assistant_turn => .assistant_turn, .assistant_table => .assistant_table, .assistant_code_block => .assistant_code_block, .assistant_thematic_rule => .assistant_thematic_rule, .semantic_notice => |notice| switch (notice.tone) { .information, .success, .warning, .neutral => if (std.ascii.eqlIgnoreCase(notice.topic, "context")) .context_notice else .system_notice, .@"error" => .error_notice, .cancelled => .cancel_notice, }, .raw_bytes => |raw| switch (raw.class) { .welcome => .welcome, .turn_summary => .turn_summary, .tool_status => .tool_status, .command_output => .command_output, .diff_block => .diff_block, .question_resolution => .cancel_notice, .subagent_status => .subagent_status, .unknown_raw => .unknown_raw, }, };}
pub const BlockGapPolicy = struct { default_gap_rows: u16 = 1,
pub fn gapBetween(self: BlockGapPolicy, prev: TranscriptBlockKind, next: TranscriptBlockKind) u16 { if (prev == .tool_status and next == .command_output) return 0; if (prev == .command_output and next == .command_output) return 0; if (prev == .subagent_status and next == .subagent_status) return 0; return self.default_gap_rows; }};
pub const EntryRenderOverride = struct { entry_id: u32, kind: TranscriptBlockKind, bytes: []const u8,};
pub const EntryRenderAction = union(enum) { keep, hide, override: struct { kind: TranscriptBlockKind, bytes: []const u8, },};
pub const default_block_gap_policy: BlockGapPolicy = .{};
pub const TranscriptEntry = union(enum) { /// Banners, command-output summaries, system notices, spinners: /// content with a fixed byte shape that does not reshape across /// widths. Entry owns the byte slice; freed with the entry. raw_bytes: RawBytesEntry, /// One retained semantic notice. Topic and body are owned by the entry; /// paint-time rendering supplies theme, width, marker, and spacing. semantic_notice: SemanticNoticeEntry, /// One user prompt. Stored as its logical text plus images and optional /// display-only skill spans; the bordered card is rebuilt at paint time /// at the current cols so reshape is correct at any width. user_turn: UserTurnEntry, /// One assistant response, populated incrementally as the pacer /// emits tokens. Reflowed at paint time for the current cols. assistant_turn: AssistantTurnEntry, /// One completed Markdown table, retained as cells so it can be /// re-laid out at the active terminal width. assistant_table: AssistantTableEntry, /// One completed Markdown code block, retained as literal source so it /// can be re-laid out at the active terminal width. assistant_code_block: AssistantCodeBlockEntry, /// One completed Markdown thematic rule, retained semantically so it /// can fill the active terminal width. assistant_thematic_rule: AssistantThematicRuleEntry,
pub const RawBytesEntry = struct { id: u32, created_at_ms: i64 = 0, bytes: []const u8, class: RawEntryClass = .unknown_raw, lifecycle_pinned: bool = false, };
pub const SemanticNoticeEntry = struct { id: u32, created_at_ms: i64 = 0, topic: []const u8, tone: types.NoticeTone, body: []const u8, visibility: types.NoticeVisibility, // The producer will replace this entry in place; its bytes are not // final and must not be released into terminal history. pending_replacement: bool = false, };
pub const UserTurnEntry = struct { id: u32, created_at_ms: i64 = 0, turn: types.UserTurn, skill_tokens: []input_visual_layout.SkillTokenSpan = &.{}, };
pub const AssistantTurnEntry = struct { id: u32, created_at_ms: i64 = 0, segments: AssistantTurnSegments, };
pub const AssistantTableEntry = struct { id: u32, created_at_ms: i64 = 0, table: assistant_presentation.TablePayload, };
pub const AssistantCodeBlockEntry = struct { id: u32, created_at_ms: i64 = 0, block: assistant_presentation.CodeBlockPayload, };
pub const AssistantThematicRuleEntry = struct { id: u32, created_at_ms: i64 = 0, };
pub fn id(self: TranscriptEntry) u32 { return switch (self) { .raw_bytes => |e| e.id, .semantic_notice => |e| e.id, .user_turn => |e| e.id, .assistant_turn => |e| e.id, .assistant_table => |e| e.id, .assistant_code_block => |e| e.id, .assistant_thematic_rule => |e| e.id, }; }
pub fn createdAtMs(self: TranscriptEntry) i64 { return switch (self) { .raw_bytes => |e| e.created_at_ms, .semantic_notice => |e| e.created_at_ms, .user_turn => |e| e.created_at_ms, .assistant_turn => |e| e.created_at_ms, .assistant_table => |e| e.created_at_ms, .assistant_code_block => |e| e.created_at_ms, .assistant_thematic_rule => |e| e.created_at_ms, }; }
pub fn deinit(self: *TranscriptEntry, alloc: Allocator) void { switch (self.*) { .raw_bytes => |e| { alloc.free(e.bytes); }, .semantic_notice => |e| { alloc.free(e.topic); alloc.free(e.body); }, .user_turn => |*e| { types.freeUserTurn(alloc, e.turn); for (e.skill_tokens) |token| { alloc.free(@constCast(token.name)); alloc.free(@constCast(token.path)); } if (e.skill_tokens.len > 0) alloc.free(e.skill_tokens); }, .assistant_turn => |*e| e.segments.deinit(alloc), .assistant_table => |*e| e.table.deinit(alloc), .assistant_code_block => |*e| e.block.deinit(alloc), .assistant_thematic_rule => {}, } }};
/// Reflow assistant text to fit within `cols` while preserving inline/// SGR attributes and OSC 8 links across wrap boundaries. Paragraphs wrap at whitespace/// and move a preceding word down when that avoids a single-word tail./// Pre-existing hard newlines in `text` are honoured. The returned slice is/// owned by the caller and freed with `alloc`.pub fn wrapAssistantText(alloc: Allocator, text: []const u8, cols: u16) ![]u8 { return assistant_wrap.wrapAssistantText(alloc, text, cols);}
fn renderThematicRuleForTranscript(alloc: Allocator, cols: u16) ![]u8 { if (cols == 0) return alloc.dupe(u8, "");
const gutter = assistant_wrap.gutterWidth(cols); const rule_width = cols -| gutter; var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); try out.appendNTimes(alloc, ' ', gutter); try out.appendSlice(alloc, "\x1b[2m"); var i: u16 = 0; while (i < rule_width) : (i += 1) try out.appendSlice(alloc, "\xe2\x94\x80"); try out.appendSlice(alloc, "\x1b[22m"); return out.toOwnedSlice(alloc);}
fn toolStatusOmissionMarker(cols: u16) []const u8 { return switch (cols) { 0 => "", 1 => ".", 2 => "..", else => "...", };}
const ToolStatusLine = struct { end: usize, next: usize, hard_break: bool = false, omitted_rune: bool = false,};
fn skipToolStatusWrapWhitespace(text: []const u8, start: usize) usize { var index = start; while (index < text.len and (text[index] == ' ' or text[index] == '\t')) : (index += 1) {} return index;}
fn toolStatusTokenExceedsWidth(text: []const u8, start: usize, max_width: usize) bool { if (max_width == 0) return false;
var index = start; var width: usize = 0; while (index < text.len) { const byte = text[index]; if (byte == 0x1b) { const end = display_width.ansiSequenceEnd(text, index); if (end <= index) return false; index = end; continue; } if (byte == ' ' or byte == '\t' or byte == '\n' or byte == '\r') return false; if (byte < 32) { index += 1; continue; }
const unit = display_width.displayUnitAt(text, index); const cell_width = unit.cell_width; if (width + cell_width > max_width) return true; width += cell_width; index += unit.byte_len; } return false;}
fn scanToolStatusLine(text: []const u8, start: usize, max_width: usize) ToolStatusLine { if (start >= text.len or max_width == 0) return .{ .end = start, .next = start };
var index = start; var width: usize = 0; var last_wrap_end: ?usize = null; var last_wrap_next: usize = start; while (index < text.len) { const byte = text[index]; if (byte == 0x1b) { const end = display_width.ansiSequenceEnd(text, index); if (end <= index) return .{ .end = index, .next = text.len, .omitted_rune = true }; index = end; continue; } if (byte == '\n' or byte == '\r') { const next = if (byte == '\r' and index + 1 < text.len and text[index + 1] == '\n') index + 2 else index + 1; return .{ .end = index, .next = next, .hard_break = true }; } if (byte == ' ' or byte == '\t') { const next = skipToolStatusWrapWhitespace(text, index); const cell_width: usize = if (byte == ' ') 1 else 0; if (width + cell_width > max_width) { return .{ .end = index, .next = next }; } last_wrap_end = index; last_wrap_next = next; width += cell_width; index += 1; continue; } if (byte < 32) { index += 1; continue; }
const unit = display_width.displayUnitAt(text, index); const cell_width = unit.cell_width; if (width + cell_width > max_width) { if (last_wrap_end) |end| { if (toolStatusTokenExceedsWidth(text, last_wrap_next, max_width)) { return .{ .end = index, .next = index }; } return .{ .end = end, .next = last_wrap_next }; } if (width == 0) { return .{ .end = index, .next = index + unit.byte_len, .omitted_rune = true, }; } return .{ .end = index, .next = index }; } width += cell_width; index += unit.byte_len; }
return .{ .end = index, .next = index };}
fn lineLeavesToolStatusContent(text: []const u8, line: ToolStatusLine) bool { return line.next < text.len or line.omitted_rune;}
fn plainToolStatusFitsPreview(text: []const u8, cols: u16) bool { if (text.len > cols) return false;
var hard_breaks: u8 = 0; var index: usize = 0; while (index < text.len) : (index += 1) { switch (text[index]) { 0x1b => return false, '\n' => { hard_breaks += 1; if (hard_breaks > 1) return false; }, '\r' => { hard_breaks += 1; if (hard_breaks > 1) return false; if (index + 1 < text.len and text[index + 1] == '\n') index += 1; }, else => {}, } } return true;}
fn toolStatusNeedsPreview(text: []const u8, cols: u16) bool { if (cols == 0 or text.len == 0) return false; if (plainToolStatusFitsPreview(text, cols)) return false;
const first = scanToolStatusLine(text, 0, cols); if (first.omitted_rune) return true; if (first.hard_break) return first.next < text.len; return lineLeavesToolStatusContent(text, first);}
fn appendToolStatusSgrReplay(alloc: Allocator, out: *std.ArrayList(u8), text: []const u8) !void { var index: usize = 0; var replayed_sgr = false; while (index < text.len) { if (text[index] != 0x1b) { index += 1; continue; }
const end = display_width.ansiSequenceEnd(text, index); if (end <= index) { index += 1; continue; } const sequence = text[index..end]; if (sequence.len >= 3 and sequence[1] == '[' and sequence[sequence.len - 1] == 'm') { if (!replayed_sgr) { try out.appendSlice(alloc, "\x1b[0m"); replayed_sgr = true; } try out.appendSlice(alloc, sequence); } index = end; }}
const DiffPrefix = struct { text_start: usize, width: u16,};
fn skipDiffPrefixAnsi(line: []const u8, index: *usize) bool { while (index.* < line.len and line[index.*] == 0x1b) { const end = display_width.ansiSequenceEnd(line, index.*); if (end <= index.*) return false; index.* = end; } return true;}
fn consumeDiffPrefixByte(line: []const u8, index: *usize, expected: u8) bool { if (!skipDiffPrefixAnsi(line, index)) return false; if (index.* >= line.len or line[index.*] != expected) return false; index.* += 1; return true;}
fn diffPrefix(line: []const u8) ?DiffPrefix { var index: usize = 0; if (!consumeDiffPrefixByte(line, &index, ' ')) return null; if (!consumeDiffPrefixByte(line, &index, ' ')) return null; if (!skipDiffPrefixAnsi(line, &index)) return null; if (!std.mem.startsWith(u8, line[index..], "│")) return null; index += "│".len; if (!consumeDiffPrefixByte(line, &index, ' ')) return null;
while (true) { if (!skipDiffPrefixAnsi(line, &index)) return null; if (index >= line.len or line[index] != ' ') break; index += 1; } var digit_count: usize = 0; while (true) { if (!skipDiffPrefixAnsi(line, &index)) return null; if (index >= line.len or !std.ascii.isDigit(line[index])) break; digit_count += 1; index += 1; } if (digit_count == 0) return null; if (!consumeDiffPrefixByte(line, &index, ' ')) return null; if (!skipDiffPrefixAnsi(line, &index) or index >= line.len) return null; switch (line[index]) { '+', '-', ' ' => {}, else => return null, } index += 1; if (!consumeDiffPrefixByte(line, &index, ' ')) return null; if (!skipDiffPrefixAnsi(line, &index)) return null;
const text_start = index; const width = std.math.cast(u16, display_width.visibleWidthIgnoringAnsi(line[0..text_start])) orelse return null; if (width < 4) return null; return .{ .text_start = text_start, .width = width };}
fn isCompleteSgr(sequence: []const u8) bool { return sequence.len >= 3 and sequence[0] == 0x1b and sequence[1] == '[' and sequence[sequence.len - 1] == 'm';}
fn appendSgrTransitions( alloc: Allocator, transitions: *std.ArrayList(u8), bytes: []const u8,) !void { var index: usize = 0; while (index < bytes.len) { if (bytes[index] != 0x1b) { index += display_width.decodeNextRune(bytes, index).len; continue; } const end = display_width.ansiSequenceEnd(bytes, index); if (end <= index) return; const sequence = bytes[index..end]; if (isCompleteSgr(sequence)) try transitions.appendSlice(alloc, sequence); index = end; }}
fn reflowDiffRow( alloc: Allocator, out: *std.ArrayList(u8), line: []const u8, prefix: DiffPrefix, cols: u16,) !void { try out.appendSlice(alloc, line[0..prefix.text_start]);
var sgr_transitions: std.ArrayList(u8) = .empty; defer sgr_transitions.deinit(alloc); try appendSgrTransitions(alloc, &sgr_transitions, line[0..prefix.text_start]);
var col: u16 = prefix.width + 1; var index = prefix.text_start; while (index < line.len) { if (line[index] == 0x1b) { const end = display_width.ansiSequenceEnd(line, index); if (end <= index) { try out.appendSlice(alloc, line[index..]); return; } const sequence = line[index..end]; try out.appendSlice(alloc, sequence); if (isCompleteSgr(sequence)) try sgr_transitions.appendSlice(alloc, sequence); index = end; continue; }
const unit = display_width.displayUnitAt(line, index); const width = unit.cell_width; if (width == 0) { try out.appendSlice(alloc, line[index .. index + unit.byte_len]); index += unit.byte_len; continue; } const cell_width: u16 = @intCast(width); if (display_width.shouldWrapAt(col, cell_width, cols)) { try out.appendSlice(alloc, "\x1b[0m\n"); try out.appendSlice(alloc, sgr_transitions.items); try out.appendSlice(alloc, " │ "); try out.appendNTimes(alloc, ' ', prefix.width - 4); col = prefix.width + 1; } try out.appendSlice(alloc, line[index .. index + unit.byte_len]); col += cell_width; index += unit.byte_len; }}
/// Reflows logical diff rows for the current terminal width. The caller owns/// the returned bytes.pub fn reflowDiffBlock(alloc: Allocator, text: []const u8, cols: u16) ![]u8 { if (cols == 0 or text.len == 0) return alloc.dupe(u8, text);
var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); var start: usize = 0; while (start < text.len) { const newline = std.mem.indexOfScalarPos(u8, text, start, '\n'); const end = newline orelse text.len; const line = text[start..end]; if (diffPrefix(line)) |prefix| { if (prefix.width < cols) { try reflowDiffRow(alloc, &out, line, prefix, cols); } else { try out.appendSlice(alloc, line); } } else { try out.appendSlice(alloc, line); } if (newline) |_| { try out.append(alloc, '\n'); start = end + 1; } else break; } return out.toOwnedSlice(alloc);}
fn formatToolStatusPreview(alloc: Allocator, text: []const u8, cols: u16) ![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); if (cols == 0 or text.len == 0) return out.toOwnedSlice(alloc);
const first = scanToolStatusLine(text, 0, cols); try out.appendSlice(alloc, text[0..first.end]); if (!lineLeavesToolStatusContent(text, first)) { if (first.hard_break) try out.append(alloc, '\n'); return out.toOwnedSlice(alloc); }
try out.append(alloc, '\n'); try appendToolStatusSgrReplay(alloc, &out, text[0..first.end]); const continuation_indent: usize = if (cols >= 6) 2 else 0; try out.appendNTimes(alloc, ' ', continuation_indent); const continuation_width = @as(usize, cols) - continuation_indent; const full_second = scanToolStatusLine(text, first.next, continuation_width); if (!first.omitted_rune and !lineLeavesToolStatusContent(text, full_second)) { try out.appendSlice(alloc, text[first.next..full_second.end]); if (full_second.hard_break) try out.append(alloc, '\n'); return out.toOwnedSlice(alloc); }
const marker = toolStatusOmissionMarker(cols); const clipped_second = scanToolStatusLine(text, first.next, continuation_width - marker.len); try out.appendSlice(alloc, text[first.next..clipped_second.end]); try out.appendSlice(alloc, marker); if (std.mem.findScalar(u8, text, 0x1b) != null) try out.appendSlice(alloc, "\x1b[0m"); return out.toOwnedSlice(alloc);}
/// Renders a typed command status as one clipped physical row. The caller owns/// the returned bytes.pub fn formatCompactCommandStatus( alloc: Allocator, text: []const u8, cols: u16,) ![]u8 { if (cols == 0 or text.len == 0) return alloc.alloc(u8, 0);
const line_end = std.mem.indexOfAny(u8, text, "\r\n") orelse text.len; var next = line_end; if (next < text.len) { if (text[next] == '\r' and next + 1 < text.len and text[next + 1] == '\n') { next += 2; } else { next += 1; } } const line = text[0..line_end]; const preserve_trailing_newline = text.len > 0 and (text[text.len - 1] == '\n' or text[text.len - 1] == '\r'); if (next == text.len and display_width.visibleWidthIgnoringAnsi(line) <= cols) { return if (preserve_trailing_newline) std.fmt.allocPrint(alloc, "{s}\n", .{line}) else alloc.dupe(u8, line); }
const prefix = display_width.prefixByWidthIgnoringAnsi(line, cols - 1); var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); try out.appendSlice(alloc, prefix); try out.appendSlice(alloc, "…"); if (std.mem.findScalar(u8, prefix, 0x1b) != null) { try out.appendSlice(alloc, "\x1b[0m"); } if (preserve_trailing_newline) try out.append(alloc, '\n'); return out.toOwnedSlice(alloc);}
fn formatFullToolStatus(alloc: Allocator, text: []const u8, cols: u16) ![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); if (cols == 0 or text.len == 0) return out.toOwnedSlice(alloc);
var start: usize = 0; var continuation = false; while (start < text.len) { const indent: usize = if (continuation and cols >= 6) 2 else 0; const line = scanToolStatusLine(text, start, @as(usize, cols) - indent); if (continuation) { try out.append(alloc, '\n'); try appendToolStatusSgrReplay(alloc, &out, text[0..start]); try out.appendNTimes(alloc, ' ', indent); } try out.appendSlice(alloc, text[start..line.end]);
if (line.next > start) { start = line.next; } else if (line.end > start) { start = line.end; } else { const unit = display_width.displayUnitAt(text, start); try out.append(alloc, '?'); start += unit.byte_len; } continuation = true;
if (line.hard_break and start == text.len) try out.append(alloc, '\n'); } return out.toOwnedSlice(alloc);}
pub fn renderTableForTranscript( alloc: Allocator, table: assistant_presentation.TablePayload, cols: u16,) ![]u8 { var rendered: std.ArrayList(u8) = .empty; errdefer rendered.deinit(alloc); if (cols <= 2) { try renderUnboxedTable(alloc, table, cols, &rendered); } else { const widths = try alloc.alloc(usize, table.column_count); defer alloc.free(widths); @memset(widths, 0); for (table.rows) |row| for (row.cells, 0..) |cell, col| { widths[col] = @max(widths[col], display_width.visibleWidthIgnoringAnsi(cell)); }; const grid_width = table.column_count * 3 + 1 + sumWidths(widths); if (table.column_count > 0 and grid_width <= cols) { try renderBoxedGrid(alloc, table, widths, &rendered); } else { try renderBoxedVertical(alloc, table, cols, &rendered); } } return rendered.toOwnedSlice(alloc);}
pub fn renderCodeBlockForTranscript( alloc: Allocator, block: assistant_presentation.CodeBlockPayload, cols: u16,) ![]u8 { return (try renderCodeBlockForTranscriptWithTheme(alloc, block, cols, .dark, null)).bytes;}
pub const CodeBlockActionSpan = struct { first_col: u16, last_col: u16,};
const CodeBlockRender = struct { bytes: []u8, action_span: ?CodeBlockActionSpan = null,};
fn renderCodeBlockForTranscriptWithTheme( alloc: Allocator, block: assistant_presentation.CodeBlockPayload, cols: u16, theme: CodeHighlightTheme, header_action: ?[]const u8,) !CodeBlockRender { var rendered: std.ArrayList(u8) = .empty; errdefer rendered.deinit(alloc); if (cols == 0) return .{ .bytes = try rendered.toOwnedSlice(alloc) };
const profile = if (block.language.len > 0) code_highlight_languages.resolve(block.language) else code_highlight_languages.infer(alloc, block.code); const language = if (block.language.len > 0) block.language else if (profile) |inferred| inferred.label else ""; const styled_code = if (profile) |resolved| try code_highlight.highlight(alloc, block.code, resolved, theme) else null; defer if (styled_code) |code| alloc.free(code); const code = styled_code orelse block.code;
const max_code_width = maxCodeLineWidth(block.code); const available_width: usize = cols; const frame_would_wrap = max_code_width > available_width -| 4 and max_code_width <= available_width; const action_width = if (header_action) |action| display_width.visibleWidth(action) else 0; if (cols <= 5 or frame_would_wrap or (header_action != null and cols < action_width + 4)) { try renderUnboxedCode(alloc, code, cols, &rendered); return .{ .bytes = try rendered.toOwnedSlice(alloc) }; }
const panel_width = codePanelWidth(max_code_width, language, header_action, cols); const inner_width = panel_width - 4; var action_span: ?CodeBlockActionSpan = null; if (header_action) |action| { action_span = try appendCodePanelActionHeader( alloc, &rendered, panel_width, language, action, ); } else if (language.len > 0) { try appendCodePanelHeader(alloc, &rendered, panel_width, language); } else { try appendCodePanelBorder(alloc, &rendered, panel_width, "┌", "┐"); }
var start: usize = 0; var emitted_line = false; while (start < code.len) { const end = std.mem.indexOfScalarPos(u8, code, start, '\n') orelse code.len; try appendCodePanelLine(alloc, &rendered, code[start..end], inner_width); emitted_line = true; if (end == code.len) break; start = end + 1; } if (!emitted_line) try appendCodePanelLine(alloc, &rendered, "", inner_width); try appendCodePanelBorder(alloc, &rendered, panel_width, "└", "┘"); return .{ .bytes = try rendered.toOwnedSlice(alloc), .action_span = action_span, };}
const CodeStyle = struct { foreground: ?[]const u8 = null,
fn apply(self: *CodeStyle, text: []const u8) void { var index: usize = 0; while (index < text.len) { if (text[index] == 0x1b) { const end = display_width.ansiSequenceEnd(text, index); if (end > index) { self.applySequence(text[index..end]); index = end; continue; } } const rune = display_width.decodeNextRune(text, index); index += rune.len; } }
fn applySequence(self: *CodeStyle, sequence: []const u8) void { if (sequence.len < 4 or sequence[0] != 0x1b or sequence[1] != '[' or sequence[sequence.len - 1] != 'm') return; if (std.mem.startsWith(u8, sequence, "\x1b[38;5;")) { self.foreground = sequence; } else if (std.mem.eql(u8, sequence, "\x1b[39m") or std.mem.eql(u8, sequence, "\x1b[0m")) { self.foreground = null; } }};
const VisibleUnit = struct { start: usize, len: usize, width: usize,};
fn firstCodeGlyph(text: []const u8) ?VisibleUnit { var index: usize = 0; while (index < text.len) { if (text[index] == 0x1b) { const end = display_width.ansiSequenceEnd(text, index); if (end > index) { index = end; continue; } } const unit = display_width.displayUnitAt(text, index); if (unit.cell_width > 0) return .{ .start = index, .len = unit.byte_len, .width = unit.cell_width }; index += unit.byte_len; } return null;}
fn maxCodeLineWidth(code: []const u8) usize { var max_width: usize = 0; var start: usize = 0; while (start < code.len) { const end = std.mem.indexOfScalarPos(u8, code, start, '\n') orelse code.len; max_width = @max(max_width, display_width.visibleWidth(code[start..end])); if (end == code.len) break; start = end + 1; } return max_width;}
fn codePanelWidth( max_code_width: usize, language: []const u8, header_action: ?[]const u8, cols: u16,) usize { const label_width = if (language.len == 0) 0 else @min( display_width.visibleWidth(language), @as(usize, cols) - 5, ); const action_width = if (header_action) |action| if (label_width == 0) display_width.visibleWidth(action) + 4 else label_width + display_width.visibleWidth(action) + 7 else 0; return @min( @as(usize, cols), @max(@as(usize, 6), @max(max_code_width + 4, @max(label_width + 5, action_width))), );}
fn appendCodePanelActionHeader( alloc: Allocator, out: *std.ArrayList(u8), panel_width: usize, language: []const u8, action: []const u8,) !CodeBlockActionSpan { const action_width = display_width.visibleWidth(action); std.debug.assert(action_width > 0); std.debug.assert(panel_width >= action_width + 4); const action_first_col = panel_width - action_width - 1; const header_width = action_first_col - 3;
try out.appendSlice(alloc, "┌"); const label_limit = header_width -| 2; const label = display_width.prefixByWidth(language, label_limit); const label_width = display_width.visibleWidth(label); if (label_width > 0) { try out.appendSlice(alloc, " "); try out.appendSlice(alloc, "\x1b[2m"); try out.appendSlice(alloc, label); try out.appendSlice(alloc, "\x1b[22m "); } var edge: usize = label_width + @as(usize, @intFromBool(label_width > 0)) * 2; while (edge < header_width) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, " "); try out.appendSlice(alloc, action); try out.appendSlice(alloc, " ┐\n"); std.debug.assert(panel_width <= std.math.maxInt(u16)); return .{ .first_col = @intCast(action_first_col), .last_col = @intCast(action_first_col + action_width - 1), };}
fn appendCodePanelHeader( alloc: Allocator, out: *std.ArrayList(u8), panel_width: usize, language: []const u8,) !void { const label_limit = panel_width - 5; const label_prefix = display_width.prefixByWidth(language, label_limit); const label = if (label_prefix.len > 0) label_prefix else "?"; const label_width = display_width.visibleWidth(label);
try out.appendSlice(alloc, "┌ "); try out.appendSlice(alloc, "\x1b[2m"); try out.appendSlice(alloc, label); try out.appendSlice(alloc, "\x1b[22m "); var edge: usize = 0; while (edge < panel_width - 4 - label_width) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, "┐\n");}
fn appendCodePanelBorder( alloc: Allocator, out: *std.ArrayList(u8), panel_width: usize, left: []const u8, right: []const u8,) !void { try out.appendSlice(alloc, left); var edge: usize = 0; while (edge < panel_width - 2) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, right); try out.append(alloc, '\n');}
fn appendCodePanelLine( alloc: Allocator, out: *std.ArrayList(u8), line: []const u8, inner_width: usize,) !void { const indent = leadingCodeIndent(line); const indent_width = display_width.visibleWidth(indent); var remaining = line; var continuation = false; var style: CodeStyle = .{}; if (remaining.len == 0) { try appendPaddedCodeRow(alloc, out, "", .{}, "", .{}, inner_width); return; } while (remaining.len > 0) { const continuation_indent = if (continuation and indent_width < inner_width) indent else ""; const available_width = inner_width - display_width.visibleWidth(continuation_indent); var prefix = display_width.prefixByWidthIgnoringAnsi(remaining, available_width); if (firstCodeGlyph(prefix) == null) { const rune = firstCodeGlyph(remaining) orelse { style.apply(remaining); break; }; if (rune.width > available_width) { var fallback_style = style; fallback_style.apply(remaining[0..rune.start]); try appendPaddedCodeRow(alloc, out, continuation_indent, fallback_style, "?", fallback_style, inner_width); style = fallback_style; remaining = remaining[rune.start + rune.len ..]; continuation = true; continue; } prefix = remaining[0 .. rune.start + rune.len]; } const row_style = style; style.apply(prefix); try appendPaddedCodeRow(alloc, out, continuation_indent, row_style, prefix, style, inner_width); remaining = remaining[prefix.len..]; continuation = true; }}
fn leadingCodeIndent(line: []const u8) []const u8 { var index: usize = 0; while (index < line.len and (line[index] == ' ' or line[index] == '\t')) : (index += 1) {} return line[0..index];}
fn appendPaddedCodeRow( alloc: Allocator, out: *std.ArrayList(u8), leading: []const u8, before: CodeStyle, content: []const u8, after: CodeStyle, inner_width: usize,) !void { const visible = display_width.visibleWidthIgnoringAnsi(leading) + display_width.visibleWidthIgnoringAnsi(content); try out.appendSlice(alloc, "│ "); try out.appendSlice(alloc, leading); if (before.foreground) |foreground| try out.appendSlice(alloc, foreground); try out.appendSlice(alloc, content); if (after.foreground != null) try out.appendSlice(alloc, "\x1b[0m"); try out.appendNTimes(alloc, ' ', inner_width -| visible); try out.appendSlice(alloc, " │\n");}
fn renderUnboxedCode( alloc: Allocator, code: []const u8, cols: u16, out: *std.ArrayList(u8),) !void { var start: usize = 0; while (start < code.len) { const end = std.mem.indexOfScalarPos(u8, code, start, '\n') orelse code.len; const line = code[start..end]; const indent = leadingCodeIndent(line); const indent_width = display_width.visibleWidth(indent); var remaining = line; var continuation = false; var style: CodeStyle = .{}; if (remaining.len == 0) { try out.append(alloc, '\n'); } else while (remaining.len > 0) { const continuation_indent = if (continuation and indent_width < cols) indent else ""; const available_width = @as(usize, cols) - display_width.visibleWidth(continuation_indent); var prefix = display_width.prefixByWidthIgnoringAnsi(remaining, available_width); if (firstCodeGlyph(prefix) == null) { const rune = firstCodeGlyph(remaining) orelse { style.apply(remaining); break; }; if (rune.width > available_width) { var fallback_style = style; fallback_style.apply(remaining[0..rune.start]); try out.appendSlice(alloc, continuation_indent); if (fallback_style.foreground) |foreground| try out.appendSlice(alloc, foreground); try out.append(alloc, '?'); if (fallback_style.foreground != null) try out.appendSlice(alloc, "\x1b[0m"); style = fallback_style; remaining = remaining[rune.start + rune.len ..]; try out.append(alloc, '\n'); continuation = true; continue; } prefix = remaining[0 .. rune.start + rune.len]; } const row_style = style; style.apply(prefix); try out.appendSlice(alloc, continuation_indent); if (row_style.foreground) |foreground| try out.appendSlice(alloc, foreground); try out.appendSlice(alloc, prefix); if (style.foreground != null) try out.appendSlice(alloc, "\x1b[0m"); try out.append(alloc, '\n'); remaining = remaining[prefix.len..]; continuation = true; } if (end == code.len) break; start = end + 1; }}
fn sumWidths(widths: []const usize) usize { var total: usize = 0; for (widths) |width| total += width; return total;}
fn appendTableBorder(alloc: Allocator, out: *std.ArrayList(u8), left: []const u8, middle: []const u8, right: []const u8, widths: []const usize) !void { try out.appendSlice(alloc, left); for (widths, 0..) |width, index| { if (index > 0) try out.appendSlice(alloc, middle); var count: usize = 0; while (count < width + 2) : (count += 1) try out.appendSlice(alloc, "─"); } try out.appendSlice(alloc, right); try out.append(alloc, '\n');}
fn renderBoxedGrid(alloc: Allocator, table: assistant_presentation.TablePayload, widths: []const usize, out: *std.ArrayList(u8)) !void { try appendTableBorder(alloc, out, "┌", "┬", "┐", widths); for (table.rows, 0..) |row, row_index| { try out.appendSlice(alloc, "│"); for (widths, 0..) |width, col| { const cell: []const u8 = if (col < row.cells.len) row.cells[col] else ""; const visible = display_width.visibleWidthIgnoringAnsi(cell); const pad = width -| visible; const alignment: assistant_presentation.TableColumnAlign = if (row_index == 0) .left else table.alignments[col]; const left_pad = switch (alignment) { .left => 0, .right => pad, .center => pad / 2, }; try out.append(alloc, ' '); try out.appendNTimes(alloc, ' ', left_pad); if (row_index == 0) { try assistant_presentation.writeTableHeaderCell(alloc, out, cell); } else { try out.appendSlice(alloc, cell); } try out.appendNTimes(alloc, ' ', pad - left_pad); try out.append(alloc, ' '); try out.appendSlice(alloc, "│"); } try out.append(alloc, '\n'); if (row_index == 0 and table.rows.len > 1) try appendTableBorder(alloc, out, "├", "┼", "┤", widths); if (row_index > 0 and row_index + 1 < table.rows.len) try appendTableBorder(alloc, out, "├", "┼", "┤", widths); } try appendTableBorder(alloc, out, "└", "┴", "┘", widths);}
fn appendVerticalLine(alloc: Allocator, out: *std.ArrayList(u8), content: []const u8, inner_width: usize) !void { var remaining = content; if (remaining.len == 0) { try out.appendSlice(alloc, "│"); try out.appendNTimes(alloc, ' ', inner_width); try out.appendSlice(alloc, "│\n"); return; } while (remaining.len > 0) { var prefix = display_width.prefixByWidthIgnoringAnsi(remaining, inner_width); var consumed = prefix.len; if (prefix.len == 0) { const unit = display_width.displayUnitAt(remaining, 0); prefix = "?"; consumed = unit.byte_len; } const visible = display_width.visibleWidthIgnoringAnsi(prefix); try out.appendSlice(alloc, "│"); try out.appendSlice(alloc, prefix); try out.appendNTimes(alloc, ' ', inner_width -| visible); try out.appendSlice(alloc, "│\n"); remaining = remaining[consumed..]; }}
fn renderBoxedVertical(alloc: Allocator, table: assistant_presentation.TablePayload, cols: u16, out: *std.ArrayList(u8)) !void { const inner_width: usize = cols - 2; try out.appendSlice(alloc, "┌"); var edge: usize = 0; while (edge < inner_width) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, "┐\n"); for (table.rows[1..], 0..) |row, row_index| { for (table.alignments, 0..) |_, col| { var field: std.ArrayList(u8) = .empty; defer field.deinit(alloc); const header: []const u8 = if (col < table.rows[0].cells.len) table.rows[0].cells[col] else ""; const value: []const u8 = if (col < row.cells.len) row.cells[col] else ""; try field.appendSlice(alloc, header); try field.appendSlice(alloc, ": "); try field.appendSlice(alloc, value); try appendVerticalLine(alloc, out, field.items, inner_width); } if (row_index + 1 < table.rows.len - 1) { try out.appendSlice(alloc, "├"); edge = 0; while (edge < inner_width) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, "┤\n"); } } try out.appendSlice(alloc, "└"); edge = 0; while (edge < inner_width) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, "┘\n");}
fn renderUnboxedTable(alloc: Allocator, table: assistant_presentation.TablePayload, cols: u16, out: *std.ArrayList(u8)) !void { const width: usize = cols; for (table.rows[1..]) |row| for (table.alignments, 0..) |_, col| { const header: []const u8 = if (col < table.rows[0].cells.len) table.rows[0].cells[col] else ""; const value: []const u8 = if (col < row.cells.len) row.cells[col] else ""; var field: std.ArrayList(u8) = .empty; defer field.deinit(alloc); try field.appendSlice(alloc, header); try field.appendSlice(alloc, ": "); try field.appendSlice(alloc, value); const prefix = display_width.prefixByWidthIgnoringAnsi(field.items, width); try out.appendSlice(alloc, prefix); try out.append(alloc, '\n'); };}
const NoticeLine = struct { end: usize, next: usize,};
const notice_osc8_prefix = "\x1b]8;";const notice_osc8_close = "\x1b]8;;\x1b\\";
fn updateNoticeHyperlinkState(fragment: []const u8, active: *?[]const u8) void { var offset: usize = 0; while (std.mem.findPos(u8, fragment, offset, notice_osc8_prefix)) |start| { const sequence_end = display_width.ansiSequenceEnd(fragment, start); if (sequence_end <= start or sequence_end > fragment.len) return; const sequence = fragment[start..sequence_end]; const terminator_len: usize = if (sequence[sequence.len - 1] == 0x07) 1 else if (sequence.len >= 2 and sequence[sequence.len - 2] == 0x1b and sequence[sequence.len - 1] == '\\') 2 else return; const payload = sequence[notice_osc8_prefix.len .. sequence.len - terminator_len]; const separator = std.mem.findScalar(u8, payload, ';') orelse return; active.* = if (separator + 1 == payload.len) null else sequence; offset = sequence_end; }}
fn skipNoticeWhitespace(text: []const u8, start: usize) usize { var index = start; while (index < text.len and (text[index] == ' ' or text[index] == '\t')) : (index += 1) {} return index;}
fn scanNoticeLine(text: []const u8, start: usize, max_width: usize) NoticeLine { std.debug.assert(max_width > 0); if (start >= text.len) return .{ .end = start, .next = start };
var index = start; var width: usize = 0; var last_wrap_end: ?usize = null; var last_wrap_next: usize = start; while (index < text.len) { const byte = text[index]; if (byte == 0x1b) { const sequence_end = display_width.ansiSequenceEnd(text, index); if (sequence_end <= index) return .{ .end = index, .next = index + 1 }; index = sequence_end; continue; } if (byte == '\n' or byte == '\r') { const next = if (byte == '\r' and index + 1 < text.len and text[index + 1] == '\n') index + 2 else index + 1; return .{ .end = index, .next = next }; } if (byte == ' ' or byte == '\t') { const next = skipNoticeWhitespace(text, index); if (width + 1 > max_width) { return .{ .end = index, .next = next }; } last_wrap_end = index; last_wrap_next = next; width += 1; index += 1; continue; }
const unit = display_width.displayUnitAt(text, index); const cell_width = unit.cell_width; if (width + cell_width > max_width) { if (last_wrap_end) |end| { if (end > start) return .{ .end = end, .next = last_wrap_next }; } if (index == start) { return .{ .end = index + unit.byte_len, .next = index + unit.byte_len }; } return .{ .end = index, .next = index }; } width += cell_width; index += unit.byte_len; if (byte == '/' or byte == '\\') { last_wrap_end = index; last_wrap_next = index; } } return .{ .end = index, .next = index };}
fn noticeLabelStyle(styles: Styles, tone: types.NoticeTone) []const u8 { return switch (tone) { .information => styles.notice_information_style, .success => styles.notice_success_style, .warning => styles.notice_warning_style, .@"error" => styles.notice_error_style, .cancelled => styles.notice_cancelled_style, .neutral => styles.system_notice_text_style, };}
fn noticeContinuationIndent(text: []const u8, cursor: usize, cols: u16) usize { if (cols <= 2 or cursor >= text.len) return 0; if (text[cursor] == '\n' or text[cursor] == '\r') return 2; const unit = display_width.displayUnitAt(text, cursor); return if (unit.cell_width <= cols - 2) 2 else 0;}
fn appendNoticeRow( out: *std.ArrayList(u8), alloc: Allocator, logical: []const u8, label_end: usize, start: usize, end: usize, indent: usize, styles: Styles, tone: types.NoticeTone, active_hyperlink: *?[]const u8,) !void { try out.appendNTimes(alloc, ' ', indent); if (active_hyperlink.*) |opener| try out.appendSlice(alloc, opener); if (start < @min(end, label_end)) { try out.appendSlice(alloc, noticeLabelStyle(styles, tone)); try out.appendSlice(alloc, logical[start..@min(end, label_end)]); try out.appendSlice(alloc, styles.reset_style); } if (end > label_end) { try out.appendSlice(alloc, styles.system_notice_text_style); try out.appendSlice(alloc, logical[@max(start, label_end)..end]); try out.appendSlice(alloc, styles.reset_style); } if (start == end) try out.appendSlice(alloc, styles.reset_style); updateNoticeHyperlinkState(logical[start..end], active_hyperlink); if (active_hyperlink.* != null) try out.appendSlice(alloc, notice_osc8_close);}
/// Returns an owned semantic notice block without surrounding blank rows.pub fn renderSemanticNotice( alloc: Allocator, notice: types.SemanticNotice, styles: Styles, cols: u16,) ![]u8 { var logical: std.ArrayList(u8) = .empty; defer logical.deinit(alloc); try logical.appendSlice(alloc, "● "); // An empty topic drops the "Topic:" label and renders the body alone. if (notice.topic.len > 0) { try logical.append(alloc, std.ascii.toUpper(notice.topic[0])); try logical.appendSlice(alloc, notice.topic[1..]); try logical.append(alloc, ':'); } const label_end = logical.items.len; const body = std.mem.trimEnd(u8, notice.body, "\r\n"); if (body.len > 0) { if (notice.topic.len > 0) try logical.append(alloc, ' '); try logical.appendSlice(alloc, body); }
var out: std.ArrayList(u8) = .empty; errdefer out.deinit(alloc); var cursor: usize = 0; var row: usize = 0; var active_hyperlink: ?[]const u8 = null; while (cursor < logical.items.len) : (row += 1) { if (row > 0) try out.append(alloc, '\n'); const indent: usize = if (row > 0) noticeContinuationIndent(logical.items, cursor, cols) else 0; const available: usize = @max(@as(usize, cols) -| indent, 1); const line = scanNoticeLine(logical.items, cursor, available); try appendNoticeRow( &out, alloc, logical.items, label_end, cursor, line.end, indent, styles, notice.tone, &active_hyperlink, ); std.debug.assert(line.next > cursor); cursor = line.next; } return out.toOwnedSlice(alloc);}
const TranscriptPresentation = enum { compact, full,};
pub fn renderEntryToBlock( alloc: Allocator, entry: TranscriptEntry, cols: u16, styles: Styles,) !RenderedBlock { return renderEntryToBlockForPresentation(alloc, entry, cols, styles, .compact);}
fn renderEntryToBlockForPresentation( alloc: Allocator, entry: TranscriptEntry, cols: u16, styles: Styles, presentation: TranscriptPresentation,) !RenderedBlock { return renderEntryToBlockForPresentationInterruptible( alloc, entry, cols, styles, presentation, null, ) catch |err| switch (err) { error.InputPending => unreachable, else => |other| return other, };}
fn renderEntryToBlockForPresentationInterruptible( alloc: Allocator, entry: TranscriptEntry, cols: u16, styles: Styles, presentation: TranscriptPresentation, checkpoint: ?*build_checkpoint.BuildCheckpoint,) !RenderedBlock { const kind = blockKindForEntry(entry); if (presentation == .compact and !isEntryVisibleInCompactPresentation(entry)) { return .{ .kind = kind, .bytes = "", .stored_tail_newlines = 0, }; } return switch (entry) { .raw_bytes => |e| if (e.class == .tool_status and presentation == .full) blk: { const full = try formatFullToolStatus(alloc, e.bytes, cols); break :blk try normalizeOwnedRenderedBlock(alloc, kind, full); } else if (e.class == .tool_status and toolStatusNeedsPreview(e.bytes, cols)) blk: { const preview = try formatToolStatusPreview(alloc, e.bytes, cols); break :blk try normalizeOwnedRenderedBlock(alloc, kind, preview); } else if (e.class == .diff_block and presentation == .compact) blk: { const compact = try reflowDiffBlock(alloc, e.bytes, cols); break :blk try normalizeOwnedRenderedBlock(alloc, kind, compact); } else try normalizeRenderedBlockTail(alloc, kind, e.bytes), .semantic_notice => |e| blk: { const rendered = try renderSemanticNotice( alloc, .{ .topic = e.topic, .tone = e.tone, .body = e.body, .visibility = e.visibility, }, styles, cols, ); break :blk try normalizeOwnedRenderedBlock(alloc, kind, rendered); }, .user_turn => |e| blk: { const card = try user_message_card.buildUserPromptCardWithSkillTokensForTerminalPresentationInterruptible( alloc, e.turn.text, e.turn.images, cols, e.skill_tokens, checkpoint, ); break :blk try normalizeOwnedRenderedBlock(alloc, kind, card); }, .assistant_turn => |e| blk: { const wrapped = try assistant_wrap.wrapTranscriptAssistantTextInterruptible( alloc, e.segments.text.items, cols, checkpoint, ); break :blk try normalizeOwnedRenderedBlockWithAllocation( alloc, kind, trimAssistantBlockHead(wrapped), wrapped, ); }, .assistant_table => |e| blk: { const gutter = assistant_wrap.gutterWidth(cols); const rendered = try renderTableForTranscript(alloc, e.table, cols -| gutter); defer alloc.free(rendered); const prefixed = try assistant_wrap.prefixStructuralRows(alloc, rendered, gutter); break :blk try normalizeOwnedRenderedBlock(alloc, kind, prefixed); }, .assistant_code_block => |e| blk: { const gutter = assistant_wrap.gutterWidth(cols); const rendered = try renderCodeBlockForTranscriptWithTheme( alloc, e.block, cols -| gutter, styles.code_highlight_theme, if (presentation == .full) "Copy" else null, ); defer alloc.free(rendered.bytes); const prefixed = try assistant_wrap.prefixStructuralRows(alloc, rendered.bytes, gutter); var normalized = try normalizeOwnedRenderedBlock(alloc, kind, prefixed); if (rendered.action_span) |span| normalized.code_action_span = .{ .first_col = span.first_col + gutter, .last_col = span.last_col + gutter, }; break :blk normalized; }, .assistant_thematic_rule => blk: { const rendered = try renderThematicRuleForTranscript(alloc, cols); break :blk .{ .kind = kind, .bytes = rendered, .stored_tail_newlines = 0, .allocation = rendered, .owned = true, }; }, };}
fn normalizeOwnedRenderedBlock( alloc: Allocator, kind: TranscriptBlockKind, bytes: []const u8,) !RenderedBlock { return normalizeOwnedRenderedBlockWithAllocation(alloc, kind, bytes, bytes);}
fn normalizeOwnedRenderedBlockWithAllocation( alloc: Allocator, kind: TranscriptBlockKind, bytes: []const u8, allocation: []const u8,) !RenderedBlock { errdefer alloc.free(allocation); var block = try normalizeRenderedBlockTail(alloc, kind, bytes); if (block.owned) { alloc.free(allocation); } else { block.allocation = allocation; block.owned = true; } return block;}
pub fn renderEntriesForFullPresentationInterruptible( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, sink: *const FullPresentationSink, out: *std.Io.Writer.Allocating, checkpoint: ?*build_checkpoint.BuildCheckpoint,) !void { var sequence: BlockSequenceState = .{};
for (entries, 0..) |entry, entry_index| { try build_checkpoint.tick(checkpoint); const entry_id = entry.id(); if (sink.skip_entry(sink.context, entry, entry_index)) { try sink.before_entry(sink.context, entry_id, out); continue; }
if (sink.override_kind(sink.context, entry)) |kind| { try out.writer.splatByteAll('\n', sequence.separatorNewlineCountBefore(kind)); try sink.before_entry(sink.context, entry_id, out); const override_ends_with_newline = try sink.append_override(sink.context, entry, out); const detail = try sink.append_detail(sink.context, entry_id, out); sequence.observe( kind, if (detail.attached) detail.ends_with_newline else override_ends_with_newline, 0, ); continue; }
const block = try renderEntryToBlockForPresentationInterruptible( alloc, entry, cols, styles, .full, checkpoint, ); defer block.deinit(alloc); if (!renderedBlockHasContent(block)) continue;
try out.writer.splatByteAll('\n', sequence.separatorNewlineCountBefore(block.kind)); try sink.before_entry(sink.context, entry_id, out); if (block.code_action_span) |span| { if (sink.record_code_action) |record| try record(sink.context, entry_id, span); } try out.writer.writeAll(block.bytes); const detail = try sink.append_detail(sink.context, entry_id, out); sequence.observe(block.kind, detail.ends_with_newline, if (detail.attached) 0 else block.stored_tail_newlines); }
try out.writer.splatByteAll('\n', sequence.finalNewlineCount());}
test "auto permission notice contributes content only to full presentation" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendSemanticNoticeTestEntry(&entries, alloc, 1, .{ .topic = "system", .tone = .information, .body = "Auto agent approved this request: Running command.", .visibility = .full_only, }); const entry = entries.items[0];
const compact = try renderEntryToBlockForPresentation( alloc, entry, 80, .{}, .compact, ); defer compact.deinit(alloc); try std.testing.expectEqual(TranscriptBlockKind.system_notice, compact.kind); try std.testing.expectEqual(@as(usize, 0), compact.bytes.len);
const full = try renderEntryToBlockForPresentation( alloc, entry, 80, .{}, .full, ); defer full.deinit(alloc); try std.testing.expectEqual(TranscriptBlockKind.system_notice, full.kind); try std.testing.expectEqualStrings( "● System: Auto agent approved this request: Running command.", full.bytes, );}
test "full presentation composition keeps normal block gaps around detail" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try entries.append(alloc, .{ .assistant_turn = .{ .id = 1, .segments = .{} } }); try entries.append(alloc, .{ .raw_bytes = .{ .id = 2, .bytes = try alloc.dupe(u8, "● Ran a deliberately long command that reaches FULL_STATUS_TAIL\n"), .class = .tool_status, } }); try entries.append(alloc, .{ .assistant_turn = .{ .id = 3, .segments = .{} } }); try entries.items[0].assistant_turn.segments.text.appendSlice(alloc, "Before"); try entries.items[2].assistant_turn.segments.text.appendSlice(alloc, "After");
const Context = struct { fn skip(_: *anyopaque, _: TranscriptEntry, _: usize) bool { return false; }
fn before(_: *anyopaque, _: u32, _: *std.Io.Writer.Allocating) !void {}
fn override_kind(_: *anyopaque, _: TranscriptEntry) ?TranscriptBlockKind { return null; }
fn append_override(_: *anyopaque, _: TranscriptEntry, _: *std.Io.Writer.Allocating) !bool { unreachable; }
fn append_detail(_: *anyopaque, entry_id: u32, out: *std.Io.Writer.Allocating) !FullDetailAppend { if (entry_id != 2) return .{}; try out.writer.writeAll("\n input\n FULL_DETAIL\n"); return .{ .attached = true, .ends_with_newline = true }; } };
var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); var context: u8 = 0; const sink = FullPresentationSink{ .context = &context, .skip_entry = Context.skip, .override_kind = Context.override_kind, .append_override = Context.append_override, .before_entry = Context.before, .append_detail = Context.append_detail, }; try renderEntriesForFullPresentationInterruptible( alloc, entries.items, 24, .{}, &sink, &out, null, );
try std.testing.expect(std.mem.indexOf(u8, out.written(), " Before\n\n● Ran") != null); try std.testing.expect(std.mem.indexOf(u8, out.written(), "FULL_STATUS_TAIL") != null); try std.testing.expect(std.mem.indexOf(u8, out.written(), " FULL_DETAIL\n\n After") != null);}
fn renderedBlockHasContent(block: RenderedBlock) bool { return block.bytes.len > 0;}
pub fn leadingWelcomeCutLine(alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles) !?usize { if (entries.len == 0) return null; for (entries[1..]) |entry| { switch (entry) { .user_turn, .assistant_turn, .assistant_table, .assistant_code_block, .assistant_thematic_rule => return null, .raw_bytes, .semantic_notice => {}, } } const boundary = try leadingWelcomeBoundary(alloc, entries, cols, styles) orelse return null; return boundary.full_cut_line;}
pub const LeadingWelcomeBoundary = struct { full_cut_line: usize, tail_replay_line: usize,};
pub fn leadingWelcomeBoundary(alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles) !?LeadingWelcomeBoundary { if (entries.len == 0) return null; if (entries[0] != .raw_bytes or entries[0].raw_bytes.class != .welcome) return null;
const welcome_block = try renderEntryToBlock(alloc, entries[0], cols, styles); defer welcome_block.deinit(alloc); if (!renderedBlockHasContent(welcome_block)) return null; const welcome_line_count = renderedHardLineCount(welcome_block.bytes); if (welcome_line_count == 0) return null;
var next_kind: ?TranscriptBlockKind = null; for (entries[1..]) |entry| { const next_block = try renderEntryToBlock(alloc, entry, cols, styles); defer next_block.deinit(alloc); if (!renderedBlockHasContent(next_block)) continue; next_kind = next_block.kind; break; } const following_kind = next_kind orelse return null;
var prefix: std.ArrayList(u8) = .empty; defer prefix.deinit(alloc); try prefix.appendSlice(alloc, welcome_block.bytes); try appendBlockSeparator(&prefix, alloc, default_block_gap_policy.gapBetween(welcome_block.kind, following_kind));
return .{ .full_cut_line = renderedHardLineCount(prefix.items), .tail_replay_line = welcome_line_count - 1, };}
fn renderedHardLineCount(bytes: []const u8) usize { if (bytes.len == 0) return 0; var total: usize = 1; for (bytes[0 .. bytes.len - 1]) |byte| { if (byte == '\n') total += 1; } return total;}
const RenderedEntries = struct { bytes: []u8, target_entry_start_line: ?usize, target_entry_start_byte: ?usize,};
const BlockSequenceState = struct { previous_kind: ?TranscriptBlockKind = null, previous_ends_with_newline: bool = false, emitted_any: bool = false, final_tail_newlines: usize = 0,
fn gapRowsBefore(self: BlockSequenceState, next_kind: TranscriptBlockKind) u16 { if (!self.emitted_any) return 0; return default_block_gap_policy.gapBetween(self.previous_kind.?, next_kind); }
fn separatorNewlineCountBefore(self: BlockSequenceState, next_kind: TranscriptBlockKind) u16 { if (!self.emitted_any) return 0; return blockSeparatorNewlineCount(self.previous_kind.?, next_kind) -| @intFromBool(self.previous_ends_with_newline); }
fn observe( self: *BlockSequenceState, kind: TranscriptBlockKind, ends_with_newline: bool, tail_newlines: usize, ) void { self.previous_kind = kind; self.previous_ends_with_newline = ends_with_newline; self.final_tail_newlines = tail_newlines; self.emitted_any = true; }
fn finalNewlineCount(self: BlockSequenceState) u16 { if (!self.emitted_any) return 0; return finalBlockTailNewlineCount(self.final_tail_newlines); }};
const RenderEntriesOptions = struct { target_entry_id: ?u32 = null, target_byte_entry_id: ?u32 = null, finality_entry_ids: []const u32 = &.{}, finality_entry_start_bytes: []?usize = &.{}, omitted_entry_id: ?u32 = null, entry_actions: []const EntryRenderAction = &.{}, summary_entry_ids: []const ?u32 = &.{}, summary_transcript_indices: []usize = &.{}, line_provenance: ?*std.ArrayList(LineProvenance) = null, entry_overrides: []const EntryRenderOverride = &.{},
fn resetSummaryIndices(self: RenderEntriesOptions) void { std.debug.assert(self.summary_entry_ids.len == self.summary_transcript_indices.len); @memset(self.summary_transcript_indices, 0); }
fn shouldOmit( self: RenderEntriesOptions, entry_index: usize, entry: TranscriptEntry, ) bool { if (self.omitted_entry_id == entry.id()) return true; if (self.entry_actions.len == 0) return false; std.debug.assert(self.entry_actions.len > entry_index); return self.entry_actions[entry_index] == .hide; }
fn overrideForEntry( self: RenderEntriesOptions, entry_index: usize, entry: TranscriptEntry, ) ?EntryRenderOverride { if (self.entry_actions.len > 0) { std.debug.assert(self.entry_actions.len > entry_index); return switch (self.entry_actions[entry_index]) { .override => |override| .{ .entry_id = entry.id(), .kind = override.kind, .bytes = override.bytes, }, .keep, .hide => null, }; } for (self.entry_overrides) |override| { if (override.entry_id == entry.id()) return override; } return null; }};
const RenderEntriesBuilder = struct { out: std.ArrayList(u8) = .empty, sequence: BlockSequenceState = .{}, line_index: usize = 0, target_entry_start_line: ?usize = null, target_entry_start_byte: ?usize = null,
fn deinit(self: *RenderEntriesBuilder, alloc: Allocator) void { self.out.deinit(alloc); }
fn appendSeparatorBefore( self: *RenderEntriesBuilder, alloc: Allocator, next_kind: TranscriptBlockKind, provenance: ?*std.ArrayList(LineProvenance), ) !void { if (!self.sequence.emitted_any) return;
const gap_rows = self.sequence.gapRowsBefore(next_kind); const separator_newlines = self.sequence.separatorNewlineCountBefore(next_kind); try self.out.appendNTimes(alloc, '\n', separator_newlines); self.line_index += separator_newlines; if (provenance) |lines| { var gap_index: u16 = 0; while (gap_index < gap_rows) : (gap_index += 1) { try lines.append(alloc, .block_separator); } } debug_trace.logf( "transcript.block_gap", "prev={s} next={s} rows={d}", .{ @tagName(self.sequence.previous_kind.?), @tagName(next_kind), gap_rows }, ); }
fn appendBlockProvenance( alloc: Allocator, provenance: ?*std.ArrayList(LineProvenance), entry: TranscriptEntry, block: RenderedBlock, ) !void { const lines = provenance orelse return; const source = LineProvenance{ .entry = .{ .entry_id = entry.id(), .entry_class = entryClassForEntry(entry), } }; var block_line: usize = 0; const block_lines = renderedHardLineCount(block.bytes); while (block_line < block_lines) : (block_line += 1) { try lines.append(alloc, source); } }
fn appendBlock( self: *RenderEntriesBuilder, alloc: Allocator, entry: TranscriptEntry, block: RenderedBlock, options: RenderEntriesOptions, checkpoint: ?*build_checkpoint.BuildCheckpoint, ) !void { try self.appendSeparatorBefore(alloc, block.kind, options.line_provenance);
const entry_id = entry.id(); if (options.target_entry_id == entry_id) self.target_entry_start_line = self.line_index; if (options.target_byte_entry_id == entry_id) self.target_entry_start_byte = self.out.items.len; for (options.finality_entry_ids, 0..) |finality_entry_id, index| { if (finality_entry_id == entry_id) { options.finality_entry_start_bytes[index] = self.out.items.len; } } for (options.summary_entry_ids, 0..) |summary_entry_id, index| { if (summary_entry_id != null and summary_entry_id.? == entry_id) { options.summary_transcript_indices[index] = self.line_index + 1; } }
try self.out.appendSlice(alloc, block.bytes); try appendBlockProvenance(alloc, options.line_provenance, entry, block); for (block.bytes) |byte| { try build_checkpoint.tick(checkpoint); if (byte == '\n') self.line_index += 1; } self.sequence.observe(block.kind, false, block.stored_tail_newlines); }
fn finish(self: *RenderEntriesBuilder, alloc: Allocator) !RenderedEntries { try self.out.appendNTimes(alloc, '\n', self.sequence.finalNewlineCount()); return .{ .bytes = try self.out.toOwnedSlice(alloc), .target_entry_start_line = self.target_entry_start_line, .target_entry_start_byte = self.target_entry_start_byte, }; }};
/// Reflows structured entries at paint-time width and centralizes block spacing.fn renderEntries( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, options: RenderEntriesOptions,) !RenderedEntries { return renderEntriesInterruptible(alloc, entries, cols, styles, options, null) catch |err| switch (err) { error.InputPending => unreachable, else => |other| return other, };}
fn renderEntriesInterruptible( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, options: RenderEntriesOptions, checkpoint: ?*build_checkpoint.BuildCheckpoint,) !RenderedEntries { options.resetSummaryIndices(); if (options.entry_actions.len > 0) { std.debug.assert(options.entry_actions.len == entries.len); }
var builder: RenderEntriesBuilder = .{}; errdefer builder.deinit(alloc);
for (entries, 0..) |entry, entry_index| { try build_checkpoint.tick(checkpoint); if (options.shouldOmit(entry_index, entry)) continue; if (options.overrideForEntry(entry_index, entry)) |override| { const block = try normalizeRenderedBlockTail( alloc, override.kind, override.bytes, ); defer block.deinit(alloc); if (!renderedBlockHasContent(block)) continue; try builder.appendBlock(alloc, entry, block, options, checkpoint); continue; } const block = try renderEntryToBlockForPresentationInterruptible( alloc, entry, cols, styles, .compact, checkpoint, ); defer block.deinit(alloc); if (!renderedBlockHasContent(block)) continue;
try builder.appendBlock(alloc, entry, block, options, checkpoint); }
return builder.finish(alloc);}
pub fn renderEntriesToBytes( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles,) ![]u8 { return (try renderEntries(alloc, entries, cols, styles, .{})).bytes;}
pub fn renderEntriesWithOverridesToBytes( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, entry_overrides: []const EntryRenderOverride,) ![]u8 { return (try renderEntries(alloc, entries, cols, styles, .{ .entry_overrides = entry_overrides, })).bytes;}
pub fn renderEntriesWithProjectionToBytes( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, entry_actions: []const EntryRenderAction,) ![]u8 { return (try renderEntries(alloc, entries, cols, styles, .{ .entry_actions = entry_actions, })).bytes;}
test "compact projection overrides its first entry and hides later entries" { const alloc = std.testing.allocator; const entries = [_]TranscriptEntry{ .{ .raw_bytes = .{ .id = 1, .bytes = "FIRST_TOOL", .class = .tool_status } }, .{ .raw_bytes = .{ .id = 2, .bytes = "SECOND_TOOL", .class = .tool_status } }, }; const actions = [_]EntryRenderAction{ .{ .override = .{ .kind = .tool_status, .bytes = "● 2 tool calls · 1 read · 1 edit", } }, .hide, };
const rendered = try renderEntriesWithProjectionToBytes( alloc, &entries, 80, .{}, &actions, ); defer alloc.free(rendered);
try std.testing.expectEqualStrings("● 2 tool calls · 1 read · 1 edit", rendered); try std.testing.expectEqualStrings("FIRST_TOOL", entries[0].raw_bytes.bytes); try std.testing.expectEqualStrings("SECOND_TOOL", entries[1].raw_bytes.bytes);}
pub fn renderedEntryStartLine( alloc: Allocator, entries: []const TranscriptEntry, entry_id: u32, cols: u16, styles: Styles,) !?usize { const rendered = try renderEntries(alloc, entries, cols, styles, .{ .target_entry_id = entry_id }); defer alloc.free(rendered.bytes); return rendered.target_entry_start_line;}
pub const TranscriptFlowBytes = struct { bytes: []u8, trailing_boundary_blank_rows: u16, target_entry_start_line: ?usize = null,};
pub fn renderEntriesToFlowBytes( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles,) !TranscriptFlowBytes { return renderEntriesToFlowBytesForEntry(alloc, entries, cols, styles, null);}
pub fn renderEntriesToFlowBytesForEntry( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, target_entry_id: ?u32,) !TranscriptFlowBytes { const rendered = try renderEntries(alloc, entries, cols, styles, .{ .target_entry_id = target_entry_id }); errdefer alloc.free(rendered.bytes);
const boundary = splitTrailingBoundaryRows(rendered.bytes); if (boundary.content_len == rendered.bytes.len) { return .{ .bytes = rendered.bytes, .trailing_boundary_blank_rows = boundary.blank_rows, .target_entry_start_line = rendered.target_entry_start_line, }; }
const trimmed = try alloc.dupe(u8, rendered.bytes[0..boundary.content_len]); alloc.free(rendered.bytes); return .{ .bytes = trimmed, .trailing_boundary_blank_rows = boundary.blank_rows, .target_entry_start_line = rendered.target_entry_start_line, };}
pub const TranscriptPreparationBytes = struct { bytes: []u8, trailing_boundary_blank_rows: u16, target_entry_start_line: ?usize = null, target_entry_start_byte: ?usize = null, folded_summary_indices: []usize, line_provenance: []const LineProvenance = &.{},
pub fn deinit(self: *TranscriptPreparationBytes, alloc: Allocator) void { alloc.free(self.bytes); alloc.free(self.folded_summary_indices); if (self.line_provenance.len > 0) alloc.free(self.line_provenance); self.* = undefined; }};
pub const TranscriptPreparationOptions = struct { target_entry_id: ?u32 = null, target_byte_entry_id: ?u32 = null, finality_entry_ids: []const u32 = &.{}, finality_entry_start_bytes: []?usize = &.{}, omitted_entry_id: ?u32 = null, entry_actions: []const EntryRenderAction = &.{}, folded_summary_entry_ids: []const ?u32 = &.{}, capture_provenance: bool = false, entry_overrides: []const EntryRenderOverride = &.{},};
pub fn renderEntriesForPreparation( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, options: TranscriptPreparationOptions,) !TranscriptPreparationBytes { return renderEntriesForPreparationInterruptible( alloc, entries, cols, styles, options, null, ) catch |err| switch (err) { error.InputPending => unreachable, else => |other| return other, };}
pub fn renderEntriesForPreparationInterruptible( alloc: Allocator, entries: []const TranscriptEntry, cols: u16, styles: Styles, options: TranscriptPreparationOptions, checkpoint: ?*build_checkpoint.BuildCheckpoint,) !TranscriptPreparationBytes { const summary_indices = try alloc.alloc(usize, options.folded_summary_entry_ids.len); errdefer alloc.free(summary_indices); var line_provenance: std.ArrayList(LineProvenance) = .empty; errdefer line_provenance.deinit(alloc); const rendered = try renderEntriesInterruptible( alloc, entries, cols, styles, .{ .target_entry_id = options.target_entry_id, .target_byte_entry_id = options.target_byte_entry_id, .finality_entry_ids = options.finality_entry_ids, .finality_entry_start_bytes = options.finality_entry_start_bytes, .omitted_entry_id = options.omitted_entry_id, .entry_actions = options.entry_actions, .summary_entry_ids = options.folded_summary_entry_ids, .summary_transcript_indices = summary_indices, .line_provenance = if (options.capture_provenance) &line_provenance else null, .entry_overrides = options.entry_overrides, }, checkpoint, ); errdefer alloc.free(rendered.bytes);
const boundary = splitTrailingBoundaryRows(rendered.bytes); const kept_line_count = renderedHardLineCount(rendered.bytes[0..boundary.content_len]); if (options.capture_provenance) { std.debug.assert(kept_line_count <= line_provenance.items.len); line_provenance.items.len = kept_line_count; } const owned_provenance = if (options.capture_provenance) try line_provenance.toOwnedSlice(alloc) else &.{}; errdefer if (owned_provenance.len > 0) alloc.free(owned_provenance); if (boundary.content_len == rendered.bytes.len) { return .{ .bytes = rendered.bytes, .trailing_boundary_blank_rows = boundary.blank_rows, .target_entry_start_line = rendered.target_entry_start_line, .target_entry_start_byte = rendered.target_entry_start_byte, .folded_summary_indices = summary_indices, .line_provenance = owned_provenance, }; }
const trimmed = try alloc.dupe(u8, rendered.bytes[0..boundary.content_len]); alloc.free(rendered.bytes); return .{ .bytes = trimmed, .trailing_boundary_blank_rows = boundary.blank_rows, .target_entry_start_line = rendered.target_entry_start_line, .target_entry_start_byte = rendered.target_entry_start_byte, .folded_summary_indices = summary_indices, .line_provenance = owned_provenance, };}
test "interruptible compact preparation checks inside one oversized assistant entry" { const alloc = std.testing.allocator; var entries = [_]TranscriptEntry{.{ .assistant_turn = .{ .id = 1, .segments = .{} } }}; defer entries[0].assistant_turn.segments.deinit(alloc); try entries[0].assistant_turn.segments.text.appendNTimes(alloc, 'x', 16 * 1024);
const Probe = struct { fn pending(_: *anyopaque) bool { return true; } }; var context: u8 = 0; var checkpoint = build_checkpoint.BuildCheckpoint.init(&context, Probe.pending); try std.testing.expectError( error.InputPending, renderEntriesForPreparationInterruptible(alloc, &entries, 80, .{}, .{}, &checkpoint), );}
const BoundarySplit = struct { content_len: usize, blank_rows: u16,};
fn splitTrailingBoundaryRows(bytes: []const u8) BoundarySplit { var blank_rows: usize = 0; var content_len = if (bytes.len > 0 and bytes[bytes.len - 1] == '\n') bytes.len - 1 else bytes.len; while (content_len > 0) { const line_start = if (std.mem.findScalarLast(u8, bytes[0..content_len], '\n')) |newline| newline + 1 else 0; if (!isBoundaryBlankLine(bytes[line_start..content_len])) break; blank_rows += 1; content_len = if (line_start > 0) line_start - 1 else 0; }
return .{ .content_len = content_len, .blank_rows = @intCast(@min(blank_rows, @as(usize, std.math.maxInt(u16)))), };}
fn isBoundaryBlankLine(line: []const u8) bool { var i: usize = 0; while (i < line.len) { const byte = line[i]; if (byte == 0x1b) { const end = display_width.ansiSequenceEnd(line, i); if (end <= i) return false; i = end; continue; } switch (byte) { ' ', '\t', '\r' => i += 1, else => return false, } } return true;}
pub fn stripTrailingNewline(text: []const u8) []const u8 { if (text.len > 0 and text[text.len - 1] == '\n') return text[0 .. text.len - 1]; return text;}
fn trimAssistantBlockHead(text: []const u8) []const u8 { var start: usize = 0; while (start < text.len and text[start] == '\n') : (start += 1) {} return text[start..];}
pub fn isVisuallyBlankLine(text: []const u8) bool { return display_width.visibleWidthIgnoringAnsi(stripTrailingNewline(text)) == 0;}
pub fn trimBlockTail(bytes: []const u8) []const u8 { var end = if (trailingAnsiOnlySuffixAfterFinalNewline(bytes)) |suffix| bytes.len - suffix.len else bytes.len; while (end > 0 and bytes[end - 1] == '\n') : (end -= 1) {} return bytes[0..end];}
fn isAnsiOnlyTail(bytes: []const u8) bool { if (bytes.len == 0) return false;
var i: usize = 0; while (i < bytes.len) { if (bytes[i] != 0x1b) return false; const next = display_width.ansiSequenceEnd(bytes, i); if (next <= i) return false; i = next; } return true;}
fn trailingAnsiOnlySuffixAfterFinalNewline(bytes: []const u8) ?[]const u8 { if (bytes.len == 0 or bytes[bytes.len - 1] == '\n') return null; const newline = std.mem.findScalarLast(u8, bytes, '\n') orelse return null; const suffix = bytes[newline + 1 ..]; return if (isAnsiOnlyTail(suffix)) suffix else null;}
fn normalizeRenderedBlockTail(alloc: Allocator, kind: TranscriptBlockKind, bytes: []const u8) !RenderedBlock { const ansi_suffix = trailingAnsiOnlySuffixAfterFinalNewline(bytes); const logical_end = if (ansi_suffix) |suffix| bytes.len - suffix.len else bytes.len; var tail_newlines: usize = 0; var content_end = logical_end; while (content_end > 0 and bytes[content_end - 1] == '\n') : (content_end -= 1) { tail_newlines += 1; }
if (ansi_suffix) |suffix| { if (tail_newlines > 0) { const normalized = try alloc.alloc(u8, content_end + suffix.len); @memcpy(normalized[0..content_end], bytes[0..content_end]); @memcpy(normalized[content_end..], suffix); return .{ .kind = kind, .bytes = normalized, .stored_tail_newlines = tail_newlines, .allocation = normalized, .owned = true, }; } }
return .{ .kind = kind, .bytes = bytes[0..content_end], .stored_tail_newlines = tail_newlines, .owned = false, };}
pub fn tailVisibleBlockKind(entries: []const TranscriptEntry) ?TranscriptBlockKind { return tailVisibleBlockKindOmittingEntries(entries, null, &.{}, .full);}
pub fn compactTailVisibleBlockKind(entries: []const TranscriptEntry) ?TranscriptBlockKind { return tailVisibleBlockKindOmittingEntries(entries, null, &.{}, .compact);}
pub fn compactTailVisibleBlockKindWithoutEntry( entries: []const TranscriptEntry, omitted_entry_id: u32,) ?TranscriptBlockKind { return tailVisibleBlockKindOmittingEntries(entries, omitted_entry_id, &.{}, .compact);}
pub fn compactTailVisibleBlockKindForProjection( entries: []const TranscriptEntry, omitted_entry_id: ?u32, entry_actions: []const EntryRenderAction,) ?TranscriptBlockKind { return tailVisibleBlockKindOmittingEntries( entries, omitted_entry_id, entry_actions, .compact, );}
fn tailVisibleBlockKindOmittingEntries( entries: []const TranscriptEntry, omitted_entry_id: ?u32, entry_actions: []const EntryRenderAction, presentation: TranscriptPresentation,) ?TranscriptBlockKind { var index = entries.len; while (index > 0) { index -= 1; const entry = entries[index]; if (omitted_entry_id == entry.id()) continue; if (entry_actions.len > 0) { std.debug.assert(entry_actions.len == entries.len); if (entry_actions[index] == .hide) continue; } if (presentation == .compact and !isEntryVisibleInCompactPresentation(entry)) continue; switch (entry) { .raw_bytes => |e| { if (trimBlockTail(e.bytes).len == 0) continue; return blockKindForRawClass(e.class); }, .semantic_notice => |e| { if (e.topic.len == 0 and e.body.len == 0) continue; return blockKindForNoticeTone(e.tone); }, .user_turn => return .user_turn, .assistant_turn => |e| { if (e.segments.text.items.len == 0) continue; return .assistant_turn; }, .assistant_table => return .assistant_table, .assistant_code_block => return .assistant_code_block, .assistant_thematic_rule => return .assistant_thematic_rule, } } return null;}
pub fn footerBoundaryGapRowsForTail(kind: ?TranscriptBlockKind) u16 { return switch (kind orelse return 0) { .assistant_turn, .turn_summary, .tool_status, .system_notice, .error_notice, .cancel_notice, .subagent_status, => 1, else => 0, };}
test "footer boundary gap applies to response-like and notice tail blocks" { try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.assistant_turn)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.turn_summary)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.tool_status)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.subagent_status)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.system_notice)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.error_notice)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.cancel_notice)); try std.testing.expectEqual(@as(u16, 0), footerBoundaryGapRowsForTail(.user_turn)); try std.testing.expectEqual(@as(u16, 0), footerBoundaryGapRowsForTail(.welcome)); try std.testing.expectEqual(@as(u16, 0), footerBoundaryGapRowsForTail(null));}
test "compact tail policy ignores hidden context notices" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "assistant response"); try appendSemanticNoticeTestEntry(&entries, alloc, 2, .{ .topic = "context", .tone = .warning, .body = "trailing warning", .visibility = .full_only, });
try std.testing.expectEqual(@as(?TranscriptBlockKind, .system_notice), tailVisibleBlockKind(entries.items)); try std.testing.expectEqual(@as(?TranscriptBlockKind, .assistant_turn), compactTailVisibleBlockKind(entries.items)); try std.testing.expectEqual(@as(?TranscriptBlockKind, .assistant_turn), compactTailVisibleBlockKindWithoutEntry(entries.items, 2));
var only_context: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&only_context, alloc); try appendSemanticNoticeTestEntry(&only_context, alloc, 3, .{ .topic = "context", .tone = .warning, .body = "only warning", .visibility = .full_only, }); try std.testing.expectEqual(@as(?TranscriptBlockKind, null), compactTailVisibleBlockKind(only_context.items));
var grouped_command: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&grouped_command, alloc); try appendRawTestEntry(&grouped_command, alloc, 4, "● Ran command\n", .tool_status); try appendRawTestEntry(&grouped_command, alloc, 5, "command output\n", .command_output); try std.testing.expectEqual( @as(?TranscriptBlockKind, .tool_status), compactTailVisibleBlockKindForProjection( grouped_command.items, null, &.{ .keep, .hide }, ), );}
fn appendBlockSeparator(out: *std.ArrayList(u8), alloc: Allocator, gap_rows: u16) !void { try out.appendNTimes(alloc, '\n', 1 + gap_rows);}
pub fn blockSeparatorNewlineCount(prev: TranscriptBlockKind, next: TranscriptBlockKind) u16 { return 1 + default_block_gap_policy.gapBetween(prev, next);}
pub fn finalBlockTailNewlineCount(stored_tail_newlines: usize) u16 { return if (stored_tail_newlines == 0) 0 else if (stored_tail_newlines > 1) 2 else 1;}
pub const RenderedBlock = struct { kind: TranscriptBlockKind, bytes: []const u8, stored_tail_newlines: usize, allocation: []const u8 = &.{}, owned: bool = false, code_action_span: ?CodeBlockActionSpan = null,
pub fn deinit(self: RenderedBlock, alloc: Allocator) void { if (self.owned) alloc.free(self.allocation); }};
pub fn transcriptLineCount(text: []const u8) usize { var total: usize = 1; for (text) |byte| { if (byte == '\n') total += 1; } return total;}
fn deinitTestEntries(entries: *std.ArrayList(TranscriptEntry), alloc: Allocator) void { for (entries.items) |*entry| entry.deinit(alloc); entries.deinit(alloc);}
fn appendRawTestEntry(entries: *std.ArrayList(TranscriptEntry), alloc: Allocator, id: u32, text: []const u8, class: RawEntryClass) !void { const bytes = try alloc.dupe(u8, text); errdefer alloc.free(bytes); try entries.append(alloc, .{ .raw_bytes = .{ .id = id, .bytes = bytes, .class = class } });}
fn appendAssistantTestEntry(entries: *std.ArrayList(TranscriptEntry), alloc: Allocator, id: u32, text: []const u8) !void { var segments: AssistantTurnSegments = .{}; errdefer segments.deinit(alloc); try segments.text.appendSlice(alloc, text); try entries.append(alloc, .{ .assistant_turn = .{ .id = id, .segments = segments } });}
fn appendSemanticNoticeTestEntry( entries: *std.ArrayList(TranscriptEntry), alloc: Allocator, id: u32, notice: types.SemanticNotice,) !void { const owned = try types.dupeSemanticNotice(alloc, notice); errdefer types.freeSemanticNotice(alloc, owned); try entries.append(alloc, .{ .semantic_notice = .{ .id = id, .topic = owned.topic, .tone = owned.tone, .body = owned.body, .visibility = owned.visibility, } });}
fn appendWithoutAsciiWhitespace(out: *std.ArrayList(u8), alloc: Allocator, text: []const u8) !void { for (text) |byte| switch (byte) { ' ', '\t', '\r', '\n' => {}, else => try out.append(alloc, byte), };}
test "semantic notice renders every tone and resets before following content" { const alloc = std.testing.allocator; const styles: Styles = .{ .system_notice_text_style = "\x1b[37m", .reset_style = "\x1b[0m", .notice_information_style = "\x1b[36m", .notice_success_style = "\x1b[32m", .notice_warning_style = "\x1b[33m", .notice_error_style = "\x1b[31m", .notice_cancelled_style = "\x1b[90m", }; const tones = [_]types.NoticeTone{ .information, .success, .warning, .@"error", .cancelled }; const label_styles = [_][]const u8{ "\x1b[36m", "\x1b[32m", "\x1b[33m", "\x1b[31m", "\x1b[90m" };
for (tones, label_styles) |tone, label_style| { const rendered = try renderSemanticNotice(alloc, .{ .topic = "topic", .tone = tone, .body = "body", }, styles, 80); defer alloc.free(rendered);
const expected = try std.fmt.allocPrint( alloc, "{s}● Topic:\x1b[0m\x1b[37m body\x1b[0m", .{label_style}, ); defer alloc.free(expected); try std.testing.expectEqualStrings(expected, rendered); try std.testing.expect(std.mem.find(u8, rendered, "[Topic]") == null);
var feed: std.ArrayList(u8) = .empty; defer feed.deinit(alloc); try feed.appendSlice(alloc, rendered); try feed.append(alloc, 'Z'); var grid = try vt_emulator.Grid.init(alloc, 40, 2); defer grid.deinit(); try grid.feed(feed.items); const following = grid.cellAt(1, 14).?; try std.testing.expectEqual(@as(u21, 'Z'), following.codepoint); try std.testing.expect(following.style.fg.eql(.default)); try std.testing.expect(following.style.bg.eql(.default)); }}
test "semantic notice keeps an OSC 8 target hidden and clickable" { const alloc = std.testing.allocator; const url = "https://fx.sh/feedback"; const body = try std.fmt.allocPrint( alloc, "\x1b]8;;{s}\x1b\\Open feedback form\x1b]8;;\x1b\\.", .{url}, ); defer alloc.free(body); const rendered = try renderSemanticNotice(alloc, .{ .topic = "feedback", .tone = .neutral, .body = body, }, .{}, 120); defer alloc.free(rendered);
var grid = try vt_emulator.Grid.init(alloc, 120, 2); defer grid.deinit(); try grid.feed(rendered); var row: std.ArrayList(u8) = .empty; defer row.deinit(alloc); try grid.rowTextTrimmed(1, &row); try std.testing.expectEqualStrings("● Feedback: Open feedback form.", row.items);
const link_cell = grid.cellAt(1, 13).?; try std.testing.expectEqual(@as(u21, 'O'), link_cell.codepoint); try std.testing.expect(link_cell.style.hyperlink_id != 0); try std.testing.expectEqualStrings(url, grid.hyperlinkUrl(link_cell.style.hyperlink_id).?); try std.testing.expectEqual(@as(u32, 0), grid.cellAt(1, 31).?.style.hyperlink_id);
var narrow_grid = try vt_emulator.Grid.init(alloc, 9, 16); defer narrow_grid.deinit(); const narrow = try renderSemanticNotice(alloc, .{ .topic = "feedback", .tone = .neutral, .body = body, }, .{}, 9); defer alloc.free(narrow); try narrow_grid.feed(narrow);
var linked_text: std.ArrayList(u8) = .empty; defer linked_text.deinit(alloc); for (1..narrow_grid.rows + 1) |row_index| { for (1..narrow_grid.cols + 1) |col_index| { const cell = narrow_grid.cellAt(@intCast(row_index), @intCast(col_index)).?; if (cell.style.hyperlink_id == 0) continue; try std.testing.expect(col_index > 2); try std.testing.expectEqualStrings(url, narrow_grid.hyperlinkUrl(cell.style.hyperlink_id).?); try linked_text.append(alloc, @intCast(cell.codepoint)); } } try std.testing.expectEqualStrings("Openfeedback form", linked_text.items); try std.testing.expectEqual(@as(u32, 0), narrow_grid.current_style.hyperlink_id);}
test "background semantic notices render one topic for launch and failure" { const alloc = std.testing.allocator; const launch = try renderSemanticNotice(alloc, .{ .topic = "background", .tone = .information, .body = "Command #1 started. Log: /tmp/run.log", }, .{}, 80); defer alloc.free(launch); try std.testing.expectEqualStrings( "● Background: Command #1 started. Log: /tmp/run.log", launch, );
const failure = try renderSemanticNotice(alloc, .{ .topic = "background", .tone = .@"error", .body = "Command #1 failed (exit 1).", }, .{}, 80); defer alloc.free(failure); try std.testing.expectEqualStrings( "● Background: Command #1 failed (exit 1).", failure, );
for ([_][]const u8{ launch, failure }) |rendered| { try std.testing.expect(std.mem.find(u8, rendered, "System: Background") == null); try std.testing.expect(std.mem.find(u8, rendered, "Background: Background") == null); }}
test "semantic notice wraps words paths UTF-8 and explicit newlines without truncation" { const alloc = std.testing.allocator; const body = "alpha beta/gamma/delta\n東京🙂 final-token"; var logical: std.ArrayList(u8) = .empty; defer logical.deinit(alloc); try appendWithoutAsciiWhitespace(&logical, alloc, "● System: "); try appendWithoutAsciiWhitespace(&logical, alloc, body);
for ([_]u16{ 0, 1, 2, 3, 6, 12, 18 }) |cols| { const rendered = try renderSemanticNotice(alloc, .{ .topic = "system", .tone = .information, .body = body, }, .{}, cols); defer alloc.free(rendered); try std.testing.expect(std.unicode.utf8ValidateSlice(rendered));
var normalized: std.ArrayList(u8) = .empty; defer normalized.deinit(alloc); try appendWithoutAsciiWhitespace(&normalized, alloc, rendered); try std.testing.expectEqualStrings(logical.items, normalized.items);
var lines = std.mem.splitScalar(u8, rendered, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= @max(cols, 2)); } }
const path = try renderSemanticNotice(alloc, .{ .topic = "x", .tone = .information, .body = "alpha/beta/gamma", }, .{}, 11); defer alloc.free(path); try std.testing.expectEqualStrings("● X: alpha/\n beta/\n gamma", path);
const words = try renderSemanticNotice(alloc, .{ .topic = "x", .tone = .information, .body = "one two\nthree", }, .{}, 9); defer alloc.free(words); try std.testing.expectEqualStrings("● X: one\n two\n three", words);}
test "semantic notices are independent blocks with exact neighboring and footer gaps" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendRawTestEntry(&entries, alloc, 1, "before", .unknown_raw); try appendSemanticNoticeTestEntry(&entries, alloc, 2, .{ .topic = "one", .tone = .information, .body = "first", }); try appendSemanticNoticeTestEntry(&entries, alloc, 3, .{ .topic = "two", .tone = .success, .body = "second", }); try appendRawTestEntry(&entries, alloc, 4, "after", .unknown_raw);
const rendered = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(rendered); try std.testing.expectEqualStrings( "before\n\n● One: first\n\n● Two: second\n\nafter", rendered, ); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.system_notice)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.error_notice)); try std.testing.expectEqual(@as(u16, 1), footerBoundaryGapRowsForTail(.cancel_notice));}
test "semantic notice visibility tail classification and provenance remain semantic" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendSemanticNoticeTestEntry(&entries, alloc, 41, .{ .topic = "hidden", .tone = .@"error", .body = "full only", .visibility = .full_only, });
try std.testing.expectEqual(@as(?TranscriptBlockKind, .error_notice), tailVisibleBlockKind(entries.items)); try std.testing.expectEqual(@as(?TranscriptBlockKind, null), compactTailVisibleBlockKind(entries.items)); const compact = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(compact); try std.testing.expectEqualStrings("", compact);
entries.items[0].semantic_notice.visibility = .compact_and_full; var prepared = try renderEntriesForPreparation( alloc, entries.items, 80, .{}, .{ .capture_provenance = true }, ); defer prepared.deinit(alloc); try std.testing.expectEqualStrings("● Hidden: full only", prepared.bytes); try std.testing.expectEqual(@as(usize, 1), prepared.line_provenance.len); try std.testing.expectEqualDeep( LineProvenance{ .entry = .{ .entry_id = 41, .entry_class = .error_notice } }, prepared.line_provenance[0], );}
test "notice palette changes leave non-system rendering unchanged" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendAssistantTestEntry(&entries, alloc, 1, "assistant response wraps normally"); try appendRawTestEntry(&entries, alloc, 2, "raw tool output", .tool_status);
const first = try renderEntriesToBytes(alloc, entries.items, 18, .{ .notice_information_style = "\x1b[36m", .notice_success_style = "\x1b[32m", .notice_warning_style = "\x1b[33m", .notice_error_style = "\x1b[31m", .notice_cancelled_style = "\x1b[90m", }); defer alloc.free(first); const second = try renderEntriesToBytes(alloc, entries.items, 18, .{ .notice_information_style = "\x1b[34m", .notice_success_style = "\x1b[92m", .notice_warning_style = "\x1b[93m", .notice_error_style = "\x1b[91m", .notice_cancelled_style = "\x1b[2m", }); defer alloc.free(second); try std.testing.expectEqualStrings(first, second);}
test "renderCodeBlockForTranscript frames language labels in the top border" { const alloc = std.testing.allocator;
const labeled_language = try alloc.dupe(u8, "zig"); defer alloc.free(labeled_language); const labeled_code = try alloc.dupe(u8, "x"); defer alloc.free(labeled_code); const labeled = try renderCodeBlockForTranscript(alloc, .{ .language = labeled_language, .code = labeled_code, }, 80); defer alloc.free(labeled); try std.testing.expectEqualStrings( "┌ \x1b[2mzig\x1b[22m ─┐\n" ++ "│ x │\n" ++ "└──────┘\n", labeled, );
const unlabeled_language = try alloc.dupe(u8, ""); defer alloc.free(unlabeled_language); const unlabeled_code = try alloc.dupe(u8, "x"); defer alloc.free(unlabeled_code); const unlabeled = try renderCodeBlockForTranscript(alloc, .{ .language = unlabeled_language, .code = unlabeled_code, }, 80); defer alloc.free(unlabeled); try std.testing.expectEqualStrings( "┌────┐\n" ++ "│ x │\n" ++ "└────┘\n", unlabeled, );
const truncated_language = try alloc.dupe(u8, "typescript"); defer alloc.free(truncated_language); const truncated_code = try alloc.dupe(u8, "x"); defer alloc.free(truncated_code); const truncated = try renderCodeBlockForTranscript(alloc, .{ .language = truncated_language, .code = truncated_code, }, 8); defer alloc.free(truncated); try std.testing.expectEqualStrings( "┌ \x1b[2mtyp\x1b[22m ─┐\n" ++ "│ x │\n" ++ "└──────┘\n", truncated, );
const wide_rune_language = try alloc.dupe(u8, "\xe6\xbc\xa2"); defer alloc.free(wide_rune_language); const wide_rune_code = try alloc.dupe(u8, "x"); defer alloc.free(wide_rune_code); const wide_rune = try renderCodeBlockForTranscript(alloc, .{ .language = wide_rune_language, .code = wide_rune_code, }, 6); defer alloc.free(wide_rune); try std.testing.expectEqualStrings( "┌ \x1b[2m?\x1b[22m ─┐\n" ++ "│ x │\n" ++ "└────┘\n", wide_rune, );}
test "full presentation shows Copy only for boxed code blocks" { const alloc = std.testing.allocator; const language = try alloc.dupe(u8, "zig"); defer alloc.free(language); const code = try alloc.dupe(u8, "const value = 1;"); defer alloc.free(code); const entry = TranscriptEntry{ .assistant_code_block = .{ .id = 1, .block = .{ .language = language, .code = code }, } };
const inline_block = try renderEntryToBlockForPresentation(alloc, entry, 80, .{}, .compact); defer inline_block.deinit(alloc); try std.testing.expect(std.mem.find(u8, inline_block.bytes, "Copy") == null);
const boxed = try renderEntryToBlockForPresentation(alloc, entry, 80, .{}, .full); defer boxed.deinit(alloc); try std.testing.expect(std.mem.find(u8, boxed.bytes, "Copy") != null);
const unboxed = try renderEntryToBlockForPresentation(alloc, entry, 5, .{}, .full); defer unboxed.deinit(alloc); try std.testing.expect(std.mem.find(u8, unboxed.bytes, "Copy") == null);}
test "boxed code action span is one-based inclusive and panel-local" { const alloc = std.testing.allocator; const language = try alloc.dupe(u8, "zig"); defer alloc.free(language); const code = try alloc.dupe(u8, "const value = 1;"); defer alloc.free(code); const block = assistant_presentation.CodeBlockPayload{ .language = language, .code = code, };
const boxed = try renderCodeBlockForTranscriptWithTheme(alloc, block, 80, .dark, "Copy"); defer alloc.free(boxed.bytes); const copy_byte = std.mem.find(u8, boxed.bytes, "Copy") orelse return error.TestUnexpectedResult; const expected_first: u16 = @intCast( display_width.visibleWidthIgnoringAnsi(boxed.bytes[0..copy_byte]) + 1, ); const span = boxed.action_span orelse return error.TestUnexpectedResult; try std.testing.expectEqual(expected_first, span.first_col); try std.testing.expectEqual(expected_first + 3, span.last_col);
const unboxed = try renderCodeBlockForTranscriptWithTheme(alloc, block, 5, .dark, "Copy"); defer alloc.free(unboxed.bytes); try std.testing.expect(unboxed.action_span == null); try std.testing.expect(std.mem.find(u8, unboxed.bytes, "Copy") == null);}
test "renderCodeBlockForTranscript bounds highlighted rows across every supported width" { const alloc = std.testing.allocator; const language = try alloc.dupe(u8, "Zig"); defer alloc.free(language); const code = try alloc.dupe(u8, " const value = \"ready\"; // comment \xe6\xbc\xa2"); defer alloc.free(code); const block = assistant_presentation.CodeBlockPayload{ .language = language, .code = code, };
for (1..81) |cols| { const rendered = try renderCodeBlockForTranscript(alloc, block, @intCast(cols)); defer alloc.free(rendered); var lines = std.mem.splitScalar(u8, rendered, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= cols); } }}
test "renderCodeBlockForTranscript highlights registered profiles without styling unknown labels" { const alloc = std.testing.allocator;
const zig_language = try alloc.dupe(u8, "Zig"); defer alloc.free(zig_language); const zig_code = try alloc.dupe(u8, "const value = 1;"); defer alloc.free(zig_code); const highlighted = try renderCodeBlockForTranscript(alloc, .{ .language = zig_language, .code = zig_code, }, 80); defer alloc.free(highlighted); try std.testing.expect(std.mem.indexOf(u8, highlighted, "\x1b[38;5;252mconst\x1b[39m") != null);
const python_language = try alloc.dupe(u8, "python"); defer alloc.free(python_language); const python_code = try alloc.dupe(u8, "def ready():\n return True"); defer alloc.free(python_code); const python = try renderCodeBlockForTranscript(alloc, .{ .language = python_language, .code = python_code, }, 80); defer alloc.free(python); try std.testing.expect(std.mem.indexOf(u8, python, "┌ \x1b[2mpython\x1b[22m ─") != null); try std.testing.expect(std.mem.indexOf(u8, python, "\x1b[38;5;252mdef\x1b[39m") != null);
const unknown_language = try alloc.dupe(u8, "brainfuck"); defer alloc.free(unknown_language); const unknown_code = try alloc.dupe(u8, "+++[>+++<-]"); defer alloc.free(unknown_code); const raw = try renderCodeBlockForTranscript(alloc, .{ .language = unknown_language, .code = unknown_code, }, 80); defer alloc.free(raw); try std.testing.expect(std.mem.indexOf(u8, raw, "\x1b[38;5;") == null); try std.testing.expect(std.mem.indexOf(u8, raw, "+++[>+++<-]") != null);}
test "semantic code blocks use the light syntax palette when requested" { const alloc = std.testing.allocator; const language = try alloc.dupe(u8, "zig"); defer alloc.free(language); const code = try alloc.dupe(u8, "const value = \"ready\"; // comment"); defer alloc.free(code); const entry = TranscriptEntry{ .assistant_code_block = .{ .id = 1, .block = .{ .language = language, .code = code, }, } };
const rendered = try renderEntryToBlock(alloc, entry, 80, .{ .code_highlight_theme = .light, }); defer rendered.deinit(alloc);
try std.testing.expect(std.mem.indexOf(u8, rendered.bytes, "\x1b[38;5;238mconst\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.bytes, "\x1b[38;5;241m\"ready\"\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, rendered.bytes, "\x1b[38;5;243m// comment\x1b[39m") != null);}
test "renderCodeBlockForTranscript infers registered high-confidence code blocks" { const alloc = std.testing.allocator; const unlabeled_language = try alloc.dupe(u8, ""); defer alloc.free(unlabeled_language); const code = try alloc.dupe(u8, "const hook = await resumeHook(token, { cleanup: true } as CleanupSignal);"); defer alloc.free(code);
const unlabeled = try renderCodeBlockForTranscript(alloc, .{ .language = unlabeled_language, .code = code, }, 100); defer alloc.free(unlabeled); try std.testing.expect(std.mem.indexOf(u8, unlabeled, "┌ \x1b[2mts\x1b[22m ─") != null); try std.testing.expect(std.mem.indexOf(u8, unlabeled, "\x1b[38;5;252mconst\x1b[39m") != null); try std.testing.expect(std.mem.indexOf(u8, unlabeled, "\x1b[38;5;252mawait\x1b[39m") != null);
const json_language = try alloc.dupe(u8, ""); defer alloc.free(json_language); const json_code = try alloc.dupe(u8, "{\"ready\": true}"); defer alloc.free(json_code); const json = try renderCodeBlockForTranscript(alloc, .{ .language = json_language, .code = json_code, }, 100); defer alloc.free(json); try std.testing.expect(std.mem.indexOf(u8, json, "┌ \x1b[2mjson\x1b[22m ─") != null); try std.testing.expect(std.mem.indexOf(u8, json, "\x1b[38;5;250m\"ready\"\x1b[39m") != null);
const ambiguous_language = try alloc.dupe(u8, ""); defer alloc.free(ambiguous_language); const ambiguous_code = try alloc.dupe(u8, "const value = 1;"); defer alloc.free(ambiguous_code); const ambiguous = try renderCodeBlockForTranscript(alloc, .{ .language = ambiguous_language, .code = ambiguous_code, }, 100); defer alloc.free(ambiguous); try std.testing.expect(std.mem.indexOf(u8, ambiguous, "\x1b[2mts\x1b[22m") == null); try std.testing.expect(std.mem.indexOf(u8, ambiguous, "\x1b[38;5;") == null);
const explicit_unknown_language = try alloc.dupe(u8, "brainfuck"); defer alloc.free(explicit_unknown_language); const explicit_unknown_code = try alloc.dupe(u8, "const hook = await resumeHook(token, { cleanup: true } as CleanupSignal);"); defer alloc.free(explicit_unknown_code); const explicit_unknown = try renderCodeBlockForTranscript(alloc, .{ .language = explicit_unknown_language, .code = explicit_unknown_code, }, 100); defer alloc.free(explicit_unknown); try std.testing.expect(std.mem.indexOf(u8, explicit_unknown, "┌ \x1b[2mbrainfuck\x1b[22m ─") != null); try std.testing.expect(std.mem.indexOf(u8, explicit_unknown, "\x1b[38;5;") == null);}
test "renderCodeBlockForTranscript contains CJK fallback color in boxed and unboxed rows" { const alloc = std.testing.allocator; const language = try alloc.dupe(u8, "zig"); defer alloc.free(language); const code = try alloc.dupe(u8, " //abcdefghijklmnop\xe6\xbc\xa2"); defer alloc.free(code); const block = assistant_presentation.CodeBlockPayload{ .language = language, .code = code, };
const boxed = try renderCodeBlockForTranscript(alloc, block, 6); defer alloc.free(boxed); var boxed_grid = try vt_emulator.Grid.init(alloc, 6, 24); defer boxed_grid.deinit(); try boxed_grid.feed(boxed); try boxed_grid.feed("z");
var boxed_fallback: ?vt_emulator.Cell = null; var row: u16 = 1; while (row <= 24) : (row += 1) { var col: u16 = 1; while (col <= 6) : (col += 1) { const cell = boxed_grid.cellAt(row, col).?; if (cell.codepoint == '?') boxed_fallback = cell; if (cell.codepoint == '\u{2502}' or cell.codepoint == '\u{2500}' or cell.codepoint == ' ') { try std.testing.expect(cell.style.fg.eql(.default)); } if (cell.codepoint == 'z') try std.testing.expect(cell.style.fg.eql(.default)); } } try std.testing.expect(boxed_fallback != null); try std.testing.expect(boxed_fallback.?.style.fg.eql(.{ .indexed = 245 }));
const unboxed = try renderCodeBlockForTranscript(alloc, block, 1); defer alloc.free(unboxed); var unboxed_grid = try vt_emulator.Grid.init(alloc, 1, 24); defer unboxed_grid.deinit(); try unboxed_grid.feed(unboxed); try unboxed_grid.feed("z");
var unboxed_fallback: ?vt_emulator.Cell = null; row = 1; while (row <= 24) : (row += 1) { const cell = unboxed_grid.cellAt(row, 1).?; if (cell.codepoint == '?') unboxed_fallback = cell; if (cell.codepoint == 'z') try std.testing.expect(cell.style.fg.eql(.default)); } try std.testing.expect(unboxed_fallback != null); try std.testing.expect(unboxed_fallback.?.style.fg.eql(.{ .indexed = 245 }));}
fn appendUserTestEntry(entries: *std.ArrayList(TranscriptEntry), alloc: Allocator, id: u32, text: []const u8) !void { const text_dup = try alloc.dupe(u8, text); errdefer alloc.free(text_dup); const images = try alloc.alloc(types.ImageAttachment, 0); errdefer alloc.free(images); try entries.append(alloc, .{ .user_turn = .{ .id = id, .turn = .{ .text = text_dup, .images = images } } });}
test "leading welcome boundary remains available after a user turn" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendRawTestEntry(&entries, alloc, 1, "welcome", .welcome); try appendUserTestEntry(&entries, alloc, 2, "hello");
try std.testing.expectEqual(@as(?usize, null), try leadingWelcomeCutLine(alloc, entries.items, 80, .{})); const boundary = (try leadingWelcomeBoundary(alloc, entries.items, 80, .{})).?; try std.testing.expectEqual(@as(usize, 2), boundary.full_cut_line); try std.testing.expectEqual(@as(usize, 0), boundary.tail_replay_line);}
test "renderEntriesToBytes keeps narrow definition bodies within terminal width" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendAssistantTestEntry(&entries, alloc, 1, "\x1b[2m \x1b[22malphabet");
var cols: u16 = 1; while (cols <= 5) : (cols += 1) { const out = try renderEntriesToBytes(alloc, entries.items, cols, .{}); defer alloc.free(out); var lines = std.mem.splitScalar(u8, out, '\n'); while (lines.next()) |line| { if (line.len == 0) continue; try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= cols); try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) > 0); } }
entries.items[0].deinit(alloc); entries.items.len = 0; try appendAssistantTestEntry(&entries, alloc, 1, "\x1b[2m \x1b[22m\xe6\xbc\xa2\xe5\xad\x97abcdef");
cols = 2; while (cols <= 5) : (cols += 1) { const out = try renderEntriesToBytes(alloc, entries.items, cols, .{}); defer alloc.free(out); var lines = std.mem.splitScalar(u8, out, '\n'); while (lines.next()) |line| { if (line.len == 0) continue; try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= cols); try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) > 0); } }}
test "tool status preview uses deterministic narrow markers" { const alloc = std.testing.allocator;
const one = try formatToolStatusPreview(alloc, "abcdefg", 1); defer alloc.free(one); try std.testing.expectEqualStrings("a\n.", one);
const two = try formatToolStatusPreview(alloc, "abcdefg", 2); defer alloc.free(two); try std.testing.expectEqualStrings("ab\n..", two);
const three = try formatToolStatusPreview(alloc, "abcdefg", 3); defer alloc.free(three); try std.testing.expectEqualStrings("abc\n...", three);}
test "tool status preview skips plain status that already fits in two rows" { try std.testing.expect(!toolStatusNeedsPreview("● Reading progress\n", 80)); try std.testing.expect(!toolStatusNeedsPreview("first line\nsecond line", 80));}
test "tool status preview wraps words and indents ordinary continuations" { const alloc = std.testing.allocator; const out = try formatToolStatusPreview( alloc, "● Ran command with readable words that extend past line two", 24, ); defer alloc.free(out);
try std.testing.expectEqualStrings( "● Ran command with\n readable words that...", out, );}
test "tool status preview keeps a long unbroken value beside its label" { const alloc = std.testing.allocator; const out = try formatToolStatusPreview( alloc, "● Searched INTENTIONALLY_LONG_SEARCH_QUERY_TO_VERIFY_SECOND_LINE_CONTINUATION_INDENTATION_AND_ELLIPSIS", 24, ); defer alloc.free(out);
try std.testing.expectEqualStrings( "● Searched INTENTIONALLY\n _LONG_SEARCH_QUERY_...", out, );}
test "tool status preview reasserts ANSI before standalone continuations" { const alloc = std.testing.allocator; const Fixture = struct { source: []const u8, marker_color: u8, }; const fixtures = [_]Fixture{ .{ .source = "\x1b[38;5;243m●\x1b[0m\x1b[1m Running\x1b[0m \x1b[38;5;245msleep waiting command words extend past line two\x1b[0m", .marker_color = 243 }, .{ .source = "\x1b[38;5;252m■\x1b[0m\x1b[1m\x1b[38;5;255m Cancelled\x1b[0m \x1b[38;5;245msleep waiting command words extend past line two\x1b[0m", .marker_color = 252 }, .{ .source = "\x1b[38;5;252m●\x1b[0m\x1b[1m Ran\x1b[0m \x1b[38;5;245msleep waiting command words extend past line two\x1b[0m", .marker_color = 252 }, };
for (fixtures) |fixture| { const out = try formatToolStatusPreview(alloc, fixture.source, 24); defer alloc.free(out);
const newline = std.mem.findScalar(u8, out, '\n') orelse return error.TestExpectedNewline; const continuation = out[newline + 1 ..]; try std.testing.expect(std.mem.startsWith(u8, continuation, "\x1b[0m"));
var first_row = try vt_emulator.Grid.init(alloc, 24, 1); defer first_row.deinit(); try first_row.feed(out[0..newline]); try std.testing.expect(first_row.cellAt(1, 1).?.style.fg.eql(.{ .indexed = fixture.marker_color }));
var continuation_row = try vt_emulator.Grid.init(alloc, 24, 1); defer continuation_row.deinit(); try continuation_row.feed(continuation); var command_col: u16 = 1; while (continuation_row.cellAt(1, command_col).?.codepoint == ' ') : (command_col += 1) {} try std.testing.expect(continuation_row.cellAt(1, command_col).?.style.fg.eql(.{ .indexed = 245 })); }}
test "full tool status reasserts ANSI before wrapped command continuations" { const alloc = std.testing.allocator; const source = "\x1b[38;5;252m●\x1b[0m\x1b[1m Ran\x1b[0m \x1b[38;5;245m" ++ "printf command words that extend across several physical rows\x1b[0m"; const out = try formatFullToolStatus(alloc, source, 24); defer alloc.free(out);
var lines = std.mem.splitScalar(u8, out, '\n'); _ = lines.next() orelse return error.TestExpectedFirstToolStatusRow; var continuation_rows: usize = 0; while (lines.next()) |line| { if (line.len == 0) continue; continuation_rows += 1;
var grid = try vt_emulator.Grid.init(alloc, 24, 1); defer grid.deinit(); try grid.feed(line);
var command_col: u16 = 1; while (grid.cellAt(1, command_col).?.codepoint == ' ') : (command_col += 1) {} try std.testing.expect(grid.cellAt(1, command_col).?.style.fg.eql(.{ .indexed = 245 })); } try std.testing.expect(continuation_rows >= 2);}
test "tool status preview honors explicit newlines and terminal width" { const alloc = std.testing.allocator; const out = try formatToolStatusPreview( alloc, "first command\nsecond command with a very long tail", 24, ); defer alloc.free(out);
try std.testing.expectEqualStrings("first command\n second command with...", out); var lines = std.mem.splitScalar(u8, out, '\n'); var line_count: usize = 0; while (lines.next()) |line| { line_count += 1; try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 24); } try std.testing.expectEqual(@as(usize, 2), line_count);}
test "tool status rendering retains full source and provenance" { const alloc = std.testing.allocator; const source = "● Failed\x1b[0m \x1b[38;5;245mcommand with readable words that extend past line two\x1b[0m\n"; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendRawTestEntry(&entries, alloc, 11, source, .tool_status);
var prepared = try renderEntriesForPreparation( alloc, entries.items, 24, .{}, .{ .capture_provenance = true }, ); defer prepared.deinit(alloc);
try std.testing.expect(std.mem.endsWith(u8, prepared.bytes, "...\x1b[0m")); try std.testing.expectEqual(@as(usize, 2), prepared.line_provenance.len); for (prepared.line_provenance) |provenance| { try std.testing.expectEqualDeep( LineProvenance{ .entry = .{ .entry_id = 11, .entry_class = .tool_status, } }, provenance, ); } try std.testing.expectEqualStrings(source, entries.items[0].raw_bytes.bytes);}
test "tool status preview reflows the same source after resize" { const alloc = std.testing.allocator; const source = "● Failed sh -c 'printf OBSERVER_TOOL_STATUS >&2; exit 17' # this command is intentionally verbose so the failed status must wrap and truncate at the terminal boundary\n"; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendRawTestEntry(&entries, alloc, 11, source, .tool_status);
const wide = try renderEntriesToBytes(alloc, entries.items, 200, .{}); defer alloc.free(wide); try std.testing.expectEqualStrings(source, wide);
const narrow = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(narrow); try std.testing.expect(std.mem.endsWith(u8, narrow, "...")); try std.testing.expectEqual(@as(usize, 2), renderedHardLineCount(narrow)); var lines = std.mem.splitScalar(u8, narrow, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 80); } try std.testing.expectEqualStrings(source, entries.items[0].raw_bytes.bytes);}
test "compact diff blocks reflow styled rows inside their gutters" { const alloc = std.testing.allocator; const source = "\x1b]9050;17\x07" ++ "\x1b[38;5;245m │ 2 CONTEXT_ROW_MARKER with a deliberately long value that must wrap inside the gutter\x1b[0m\n" ++ "\x1b[38;5;252m │ \x1b[38;5;203m3 -\x1b[38;5;252m REMOVED_ROW_MARKER with a deliberately long value that must wrap inside the gutter\x1b[0m\n" ++ "\x1b[38;5;252m │ \x1b[38;5;77m3 +\x1b[38;5;252m MUTATION_NEW_MARKER with a deliberately long replacement value that must wrap correctly in the diff preview\x1b[0m\n" ++ "\x1b]9051;17\x07"; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendRawTestEntry(&entries, alloc, 17, source, .diff_block);
const narrow = try renderEntriesToBytes(alloc, entries.items, 48, .{}); defer alloc.free(narrow); try std.testing.expect(std.mem.indexOf(u8, narrow, "\x1b[0m\n\x1b[38;5;245m │ ") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "\x1b[0m\n\x1b[38;5;252m\x1b[38;5;203m\x1b[38;5;252m │ ") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "\x1b[0m\n\x1b[38;5;252m\x1b[38;5;77m\x1b[38;5;252m │ ") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "CONTEXT_ROW_MARKER") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "REMOVED_ROW_MARKER") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "MUTATION_NEW_MARKER") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "\x1b]9050;17\x07") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "\x1b]9051;17\x07") != null); var lines = std.mem.splitScalar(u8, narrow, '\n'); while (lines.next()) |line| { if (display_width.visibleWidthIgnoringAnsi(line) == 0) continue; try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 48); } try std.testing.expectEqualStrings(source, entries.items[0].raw_bytes.bytes);}
test "diff block reflow preserves fitting malformed and unicode rows" { const alloc = std.testing.allocator; const fitting = "\x1b[38;5;252m │ \x1b[38;5;203m1 -\x1b[38;5;252m short λ value\x1b[0m\n"; const wide = try reflowDiffBlock(alloc, fitting, 80); defer alloc.free(wide); try std.testing.expectEqualStrings(fitting, wide);
const malformed = "\x1b[38;5;252m │ not-a-diff-row that remains byte-identical\x1b[0m\n"; const unchanged = try reflowDiffBlock(alloc, malformed, 16); defer alloc.free(unchanged); try std.testing.expectEqualStrings(malformed, unchanged);
const unicode = "\x1b[38;5;252m │ \x1b[38;5;77m2 +\x1b[38;5;252m 😀😀😀😀UNICODE_TAIL\x1b[0m\n"; const narrow = try reflowDiffBlock(alloc, unicode, 16); defer alloc.free(narrow); try std.testing.expect(std.mem.indexOf(u8, narrow, " │ UNICODE_") != null); try std.testing.expect(std.mem.indexOf(u8, narrow, "TAIL") != null); var lines = std.mem.splitScalar(u8, narrow, '\n'); while (lines.next()) |line| { if (display_width.visibleWidthIgnoringAnsi(line) == 0) continue; try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 16); }}
test "renderEntriesToBytes normalizes raw byte entry tails and inserts block gaps" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "first\n\n", .unknown_raw); try appendRawTestEntry(&entries, alloc, 2, "second\n", .unknown_raw);
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out); try std.testing.expectEqualStrings("first\n\nsecond\n", out);}
test "compact presentation hides context notices while full presentation retains them" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendSemanticNoticeTestEntry(&entries, alloc, 1, .{ .topic = "context", .tone = .warning, .body = "first warning", .visibility = .full_only, }); try appendSemanticNoticeTestEntry(&entries, alloc, 2, .{ .topic = "system", .tone = .information, .body = "ordinary system notice", }); try appendSemanticNoticeTestEntry(&entries, alloc, 3, .{ .topic = "context", .tone = .warning, .body = "second warning", .visibility = .full_only, }); try appendSemanticNoticeTestEntry(&entries, alloc, 4, .{ .topic = "system", .tone = .@"error", .body = "ordinary error notice", });
const compact = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(compact); try std.testing.expect(std.mem.indexOf(u8, compact, "Context:") == null); try std.testing.expect(std.mem.indexOf(u8, compact, "ordinary system notice") != null); try std.testing.expect(std.mem.indexOf(u8, compact, "ordinary error notice") != null);
const first_full = try renderEntryToBlockForPresentation(alloc, entries.items[0], 80, .{}, .full); defer first_full.deinit(alloc); const second_full = try renderEntryToBlockForPresentation(alloc, entries.items[2], 80, .{}, .full); defer second_full.deinit(alloc); try std.testing.expectEqualStrings("● Context: first warning", first_full.bytes); try std.testing.expectEqualStrings("● Context: second warning", second_full.bytes); try std.testing.expectEqual(TranscriptEntryClass.context_notice, entryClassForEntry(entries.items[0]));}
test "rendered entry start line follows identity after raw block normalization" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "first\n\n", .unknown_raw); try appendRawTestEntry( &entries, alloc, 2, "● Running command\x1b[0m\n", .tool_status, );
try std.testing.expectEqual( @as(?usize, 2), try renderedEntryStartLine(alloc, entries.items, 2, 80, .{}), );
const flow = try renderEntriesToFlowBytesForEntry(alloc, entries.items, 80, .{}, 2); defer alloc.free(flow.bytes); try std.testing.expectEqual(@as(?usize, 2), flow.target_entry_start_line); try std.testing.expectEqualStrings( "first\n\n● Running command\x1b[0m", flow.bytes, );}
test "turn summary renders as response-like transcript tail" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "assistant text"); try appendRawTestEntry(&entries, alloc, 2, "\x1b[38;5;245m 2m 10s (15k tokens)\x1b[0m\n", .turn_summary);
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out); try std.testing.expectEqualStrings(" assistant text\n\n\x1b[38;5;245m 2m 10s (15k tokens)\x1b[0m\n", out); try std.testing.expectEqual(@as(?TranscriptBlockKind, .turn_summary), tailVisibleBlockKind(entries.items));}
test "renderEntriesToBytes trims trailing raw gap tail for a single block" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "hello\n\n", .unknown_raw);
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out);
try std.testing.expectEqualStrings("hello\n\n", out);}
test "renderEntriesToBytes keeps final single raw newline cursor-only" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "hello\n", .unknown_raw);
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out);
try std.testing.expectEqualStrings("hello\n", out);}
test "renderEntriesToFlowBytes separates final boundary blank row from content" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "assistant text\n\n", .unknown_raw);
const flow = try renderEntriesToFlowBytes(alloc, entries.items, 80, .{}); defer alloc.free(flow.bytes);
try std.testing.expectEqualStrings("assistant text", flow.bytes); try std.testing.expectEqual(@as(u16, 1), flow.trailing_boundary_blank_rows);}
test "renderEntriesToFlowBytes separates assistant final boundary blank row from content" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "assistant text\n\n");
const flow = try renderEntriesToFlowBytes(alloc, entries.items, 80, .{}); defer alloc.free(flow.bytes);
try std.testing.expectEqualStrings(" assistant text", flow.bytes); try std.testing.expectEqual(@as(u16, 1), flow.trailing_boundary_blank_rows);}
test "renderEntriesToFlowBytes separates whitespace ansi tail as boundary row" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "assistant text\n \x1b[0m");
const flow = try renderEntriesToFlowBytes(alloc, entries.items, 80, .{}); defer alloc.free(flow.bytes);
try std.testing.expectEqualStrings(" assistant text", flow.bytes); try std.testing.expectEqual(@as(u16, 1), flow.trailing_boundary_blank_rows);}
test "renderEntriesToBytes separates tool status and assistant text" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "tests passed\n", .tool_status); try appendAssistantTestEntry(&entries, alloc, 2, "next answer");
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out);
try std.testing.expectEqualStrings("tests passed\n\n next answer", out);}
test "renderEntriesToBytes indents assistant prose without moving tool status" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "● tool status\n", .tool_status); try appendAssistantTestEntry(&entries, alloc, 2, "assistant response");
const normal = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(normal); try std.testing.expectEqualStrings("● tool status\n\n assistant response", normal);
var narrow_entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&narrow_entries, alloc); try appendAssistantTestEntry(&narrow_entries, alloc, 3, "ab"); const narrow = try renderEntriesToBytes(alloc, narrow_entries.items, 2, .{}); defer alloc.free(narrow); try std.testing.expectEqualStrings(" a\n b", narrow);}
test "renderEntriesToBytes keeps the assistant gutter outside bold prose" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "\x1b[1mabcdef\x1b[22m");
const out = try renderEntriesToBytes(alloc, entries.items, 5, .{}); defer alloc.free(out); try std.testing.expectEqualStrings( " \x1b[1mabc\x1b[0m\n \x1b[1mdef\x1b[22m", out, );}
test "renderEntriesToBytes keeps the assistant gutter outside light inline code" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "\x1b[38;5;247mabcdef\x1b[39m");
const out = try renderEntriesToBytes(alloc, entries.items, 5, .{}); defer alloc.free(out); try std.testing.expectEqualStrings( " \x1b[38;5;247mabc\x1b[0m\n \x1b[38;5;247mdef\x1b[39m", out, );}
test "renderEntriesToBytes preserves Setext heading styles through narrow wrapping" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendAssistantTestEntry( &entries, alloc, 1, "\x1b[1m\x1b[4mSetext H1 heading that wraps across narrow terminal rows\x1b[24m\x1b[22m\n" ++ "\x1b[1mSetext H2 heading that also wraps across narrow terminal rows\x1b[22m", );
const out = try renderEntriesToBytes(alloc, entries.items, 18, .{}); defer alloc.free(out);
try std.testing.expect(std.mem.startsWith(u8, out, " \x1b[1m\x1b[4m")); try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[24m\x1b[22m") != null); try std.testing.expect(std.mem.indexOf(u8, out, "\x1b[1mSetext H2") != null); var lines = std.mem.splitScalar(u8, out, '\n'); while (lines.next()) |line| { try std.testing.expect(std.mem.startsWith(u8, line, " ")); try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 18); }}
test "renderEntriesToBytes keeps the assistant gutter outside bold prose after a hard newline" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "\x1b[1mfirst\nsecond\x1b[22m");
const out = try renderEntriesToBytes(alloc, entries.items, 20, .{}); defer alloc.free(out); try std.testing.expectEqualStrings( " \x1b[1mfirst\x1b[0m\n \x1b[1msecond\x1b[22m", out, );}
test "renderEntriesToBytes keeps the assistant gutter outside an OSC 8 link" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry( &entries, alloc, 1, "\x1b]8;id=fx-1;https://example.com\x1b\\\x1b[4mabcdef\x1b[24m\x1b]8;;\x1b\\", );
const out = try renderEntriesToBytes(alloc, entries.items, 5, .{}); defer alloc.free(out); try std.testing.expect(std.mem.startsWith(u8, out, " \x1b[4m\x1b]8;id=fx-1;https://example.com\x1b\\abc")); try std.testing.expect(std.mem.find(u8, out, "\x1b[0m\x1b]8;;\x1b\\\n \x1b[4m\x1b]8;id=fx-1") != null);}
test "renderEntriesToBytes reflows an inline image label at narrow widths" { const alloc = std.testing.allocator; var processor = assistant_presentation.MarkdownProcessor{}; defer processor.deinit(alloc); var source: std.ArrayList(u8) = .empty; defer source.deinit(alloc); try processor.push( alloc, " trailing prose\n", &source, );
var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendAssistantTestEntry(&entries, alloc, 1, source.items);
for ([_]u16{ 1, 2, 3, 32, 120 }) |cols| { const out = try renderEntriesToBytes(alloc, entries.items, cols, .{}); defer alloc.free(out);
var lines = std.mem.splitScalar(u8, out, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= cols); } try std.testing.expect(std.mem.indexOf(u8, out, "▧") != null); if (cols >= 32) try std.testing.expect(std.mem.indexOf(u8, out, "trailing prose") != null); }}
test "renderEntriesToBytes keeps narrow CJK and emoji prose within the gutter" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "\xe6\xbc\xa2\xf0\x9f\x98\x80\n");
for (1..6) |width| { const cols: u16 = @intCast(width); const out = try renderEntriesToBytes(alloc, entries.items, cols, .{}); defer alloc.free(out);
var lines = std.mem.splitScalar(u8, out, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= cols); } const gutter_tail = if (cols >= 3) "\n " else "\n "; try std.testing.expect(!std.mem.endsWith(u8, out, gutter_tail)); if (cols <= 3) { try std.testing.expectEqual(@as(usize, 2), std.mem.count(u8, out, "?")); try std.testing.expect(std.mem.find(u8, out, "\xe6\xbc\xa2") == null); try std.testing.expect(std.mem.find(u8, out, "\xf0\x9f\x98\x80") == null); } }}
test "renderEntriesToBytes normalizes leading assistant newline after tool status" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "inspected\n", .tool_status); try appendAssistantTestEntry(&entries, alloc, 2, "\nassistant paragraph");
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out);
try std.testing.expectEqualStrings("inspected\n\n assistant paragraph", out);}
test "renderEntriesToBytes keeps adjacent command output chunks contiguous" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "one\n", .command_output); try appendRawTestEntry(&entries, alloc, 2, "two\n", .command_output); try appendAssistantTestEntry(&entries, alloc, 3, "assistant after command");
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out);
try std.testing.expectEqualStrings("one\ntwo\n\n assistant after command", out);}
test "renderEntriesToFlowBytes preserves internal command output and assistant gaps" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "one\n", .command_output); try appendRawTestEntry(&entries, alloc, 2, "two\n", .command_output); try appendAssistantTestEntry(&entries, alloc, 3, "assistant after command");
const flow = try renderEntriesToFlowBytes(alloc, entries.items, 80, .{}); defer alloc.free(flow.bytes);
try std.testing.expectEqualStrings("one\ntwo\n\n assistant after command", flow.bytes); try std.testing.expectEqual(@as(u16, 0), flow.trailing_boundary_blank_rows);}
test "preparation provenance preserves entry identity and block separators" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 11, "tool status\n", .tool_status); try appendAssistantTestEntry(&entries, alloc, 12, "assistant response");
var prepared = try renderEntriesForPreparation( alloc, entries.items, 80, .{}, .{ .capture_provenance = true }, ); defer prepared.deinit(alloc);
try std.testing.expectEqualStrings("tool status\n\n assistant response", prepared.bytes); try std.testing.expectEqual(@as(usize, 3), prepared.line_provenance.len); try std.testing.expectEqualDeep( LineProvenance{ .entry = .{ .entry_id = 11, .entry_class = .tool_status, } }, prepared.line_provenance[0], ); try std.testing.expectEqual(LineProvenance.block_separator, prepared.line_provenance[1]); try std.testing.expectEqualDeep( LineProvenance{ .entry = .{ .entry_id = 12, .entry_class = .assistant_turn, } }, prepared.line_provenance[2], );}
test "renderEntriesToBytes keeps adjacent subagent statuses contiguous" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendRawTestEntry(&entries, alloc, 1, "\x1b[2m │ subagent started\n\x1b[0m", .subagent_status); try appendRawTestEntry(&entries, alloc, 2, "\x1b[2m │ subagent | 1 tools | Reading file\n\x1b[0m", .subagent_status); try appendRawTestEntry(&entries, alloc, 3, "\x1b[2m │ subagent done | summary\n\x1b[0m", .subagent_status);
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out);
try std.testing.expectEqualStrings( "\x1b[2m │ subagent started\x1b[0m\n\x1b[2m │ subagent | 1 tools | Reading file\x1b[0m\n\x1b[2m │ subagent done | summary\x1b[0m\n", out, );}
test "renderEntriesToBytes reflows assistant_turn at paint-time cols" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "abcdefghij");
const wide = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(wide); try std.testing.expectEqualStrings(" abcdefghij", wide);
const narrow = try renderEntriesToBytes(alloc, entries.items, 5, .{}); defer alloc.free(narrow); try std.testing.expectEqualStrings(" abc\n def\n ghi\n j", narrow);}
test "renderEntriesToBytes reflows parser-rendered lists at paint-time cols" { const alloc = std.testing.allocator; var processor = assistant_presentation.MarkdownProcessor{}; defer processor.deinit(alloc); var source: std.ArrayList(u8) = .empty; defer source.deinit(alloc); try processor.push( alloc, "- first-abcdefghijklmnopqrstuvwxyzabcdefghijklmnop\n" ++ "1. second-abcdefghijklmnopqrstuvwxyzabcdefghijklmnop\n" ++ " - third-abcdefghijklmnopqrstuvwxyzabcdefghijklmnop\n", &source, ); try processor.flush(alloc, &source);
var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendAssistantTestEntry(&entries, alloc, 1, source.items);
const wide = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(wide); try std.testing.expect(std.mem.find(u8, wide, "first-abcdefghijklmnopqrstuvwxyz") != null); try std.testing.expect(std.mem.find(u8, wide, "second-abcdefghijklmnopqrstuvwxyz") != null); try std.testing.expect(std.mem.find(u8, wide, "third-abcdefghijklmnopqrstuvwxyz") != null);
const narrow = try renderEntriesToBytes(alloc, entries.items, 20, .{}); defer alloc.free(narrow); const first = std.mem.indexOf(u8, narrow, "first-").?; const second = std.mem.indexOf(u8, narrow, "second-").?; const third = std.mem.indexOf(u8, narrow, "third-").?; try std.testing.expect(first < second and second < third); try std.testing.expect(std.mem.find(u8, narrow, "\n ") != null); try std.testing.expect(std.mem.find(u8, narrow, "\n ") != null); try std.testing.expect(std.mem.find(u8, narrow, "\n ") != null);
var lines = std.mem.splitScalar(u8, narrow, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 20); }}
test "renderEntriesToBytes reflows parser-rendered task lists at paint-time cols" { const alloc = std.testing.allocator; var processor = assistant_presentation.MarkdownProcessor{}; defer processor.deinit(alloc); var source: std.ArrayList(u8) = .empty; defer source.deinit(alloc); try processor.push( alloc, "- [ ] first-abcdefghijklmnopqrstuvwxyzabcdefghijklmnop\n" ++ "1. [x] completed-abcdefghijklmnopqrstuvwxyzabcdefghijklmnop\n" ++ " - [X] nested-abcdefghijklmnopqrstuvwxyzabcdefghijklmnop\n", &source, ); try processor.flush(alloc, &source);
try std.testing.expect(std.mem.find(u8, source.items, "[ ]") == null); try std.testing.expect(std.mem.find(u8, source.items, "[x]") == null); try std.testing.expect(std.mem.find(u8, source.items, "[X]") == null); try std.testing.expect(std.mem.find(u8, source.items, "\x1b[38;5;252m\xe2\x9c\x93\x1b[39m completed-") != null);
var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendAssistantTestEntry(&entries, alloc, 1, source.items);
const wide = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(wide); try std.testing.expect(std.mem.find(u8, wide, "first-abcdefghijklmnopqrstuvwxyz") != null); try std.testing.expect(std.mem.find(u8, wide, "completed-abcdefghijklmnopqrstuvwxyz") != null); try std.testing.expect(std.mem.find(u8, wide, "nested-abcdefghijklmnopqrstuvwxyz") != null);
const narrow = try renderEntriesToBytes(alloc, entries.items, 20, .{}); defer alloc.free(narrow); const first = std.mem.indexOf(u8, narrow, "first-").?; const completed = std.mem.indexOf(u8, narrow, "completed-").?; const nested = std.mem.indexOf(u8, narrow, "nested-").?; try std.testing.expect(first < completed and completed < nested); try std.testing.expect(std.mem.find(u8, narrow, "\n ") != null); try std.testing.expect(std.mem.find(u8, narrow, "\n ") != null); try std.testing.expect(std.mem.find(u8, narrow, "\n ") != null);
var lines = std.mem.splitScalar(u8, narrow, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 20); }}
test "renderEntriesToBytes avoids singleton intermediate task continuations" { const alloc = std.testing.allocator; var processor = assistant_presentation.MarkdownProcessor{}; defer processor.deinit(alloc); var source: std.ArrayList(u8) = .empty; defer source.deinit(alloc); try processor.push( alloc, " - [ ] nested observer task that wraps across a narrow terminal without losing its continuation indentation\n", &source, ); try processor.flush(alloc, &source);
var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendAssistantTestEntry(&entries, alloc, 1, source.items);
const rendered = try renderEntriesToBytes(alloc, entries.items, 32, .{}); defer alloc.free(rendered); try std.testing.expect(std.mem.find(u8, rendered, "\n its continuation\n indentation") != null); try std.testing.expect(std.mem.find(u8, rendered, "\n its\n continuation indentation") == null);
var lines = std.mem.splitScalar(u8, rendered, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 32); }}
test "renderEntriesToBytes indents semantic table and code rows" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
const table = try assistant_presentation.parseTablePayload( alloc, "| Name | Status |\n" ++ "|------|--------|\n" ++ "| api | Complete |\n", ); try entries.append(alloc, .{ .assistant_table = .{ .id = 1, .table = table } }); try entries.append(alloc, .{ .assistant_code_block = .{ .id = 2, .block = .{ .language = try alloc.dupe(u8, "zig"), .code = try alloc.dupe(u8, "const value = 1;"), }, } });
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out); try std.testing.expect(std.mem.startsWith(u8, out, " ┌")); try std.testing.expect(std.mem.find(u8, out, "\n │") != null); try std.testing.expect(std.mem.find(u8, out, "\n\n ┌ \x1b[2mzig") != null);
for (1..6) |width| { const cols: u16 = @intCast(width); const narrow = try renderEntriesToBytes(alloc, entries.items, cols, .{}); defer alloc.free(narrow); var lines = std.mem.splitScalar(u8, narrow, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= cols); } const gutter_tail = if (cols >= 3) "\n " else "\n "; try std.testing.expect(!std.mem.endsWith(u8, narrow, gutter_tail)); }}
test "semantic thematic rule fills assistant content width and keeps provenance" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try entries.append(alloc, .{ .assistant_thematic_rule = .{ .id = 7 } });
const zero = try renderEntriesToBytes(alloc, entries.items, 0, .{}); defer alloc.free(zero); try std.testing.expectEqual(@as(usize, 0), zero.len);
for ([_]u16{ 1, 2, 3, 48, 120 }) |cols| { const out = try renderEntriesToBytes(alloc, entries.items, cols, .{}); defer alloc.free(out);
try std.testing.expectEqual(@as(usize, 1), renderedHardLineCount(out)); try std.testing.expectEqual(cols, display_width.visibleWidthIgnoringAnsi(out)); try std.testing.expect(std.mem.endsWith(u8, out, "\x1b[22m")); try std.testing.expectEqual(@as(usize, cols - assistant_wrap.gutterWidth(cols)), std.mem.count(u8, out, "\xe2\x94\x80")); }
var prepared = try renderEntriesForPreparation( alloc, entries.items, 48, .{}, .{ .capture_provenance = true }, ); defer prepared.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), prepared.line_provenance.len); try std.testing.expectEqualDeep( LineProvenance{ .entry = .{ .entry_id = 7, .entry_class = .assistant_thematic_rule, } }, prepared.line_provenance[0], );}
test "semantic table renderer frees rendered output when row prefixing fails" { const base = std.testing.allocator;
var table_entry = TranscriptEntry{ .assistant_table = .{ .id = 1, .table = try assistant_presentation.parseTablePayload( base, "| Name | Status |\n" ++ "|------|--------|\n" ++ "| api | Complete |\n", ), } }; defer table_entry.deinit(base);
var reached_success = false; for (0..128) |fail_index| { var failing = std.testing.FailingAllocator.init(base, .{ .fail_index = fail_index }); const alloc = failing.allocator(); if (renderEntryToBlock(alloc, table_entry, 80, .{})) |block| { block.deinit(alloc); reached_success = true; break; } else |err| { try std.testing.expectEqual(error.OutOfMemory, err); } } try std.testing.expect(reached_success);}
fn checkCodeBlockRenderAllocationFailures(alloc: Allocator) !void { const base = std.testing.allocator; var entry = TranscriptEntry{ .assistant_code_block = .{ .id = 1, .block = .{ .language = try base.dupe(u8, "zig"), .code = try base.dupe(u8, "const value = \"ready\"; // highlighted\n"), }, } }; defer entry.deinit(base);
const block = try renderEntryToBlock(alloc, entry, 80, .{}); defer block.deinit(alloc);}
test "renderEntryToBlock frees all highlighted code allocation failures" { try std.testing.checkAllAllocationFailures( std.testing.allocator, checkCodeBlockRenderAllocationFailures, .{}, );}
test "renderEntriesToBytes keeps a semantic table as its own assistant entry" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "Before table.\n"); const table = try assistant_presentation.parseTablePayload( alloc, "| Name | Count |\n" ++ "|------|------:|\n" ++ "| api | 7 |\n", ); try entries.append(alloc, .{ .assistant_table = .{ .id = 2, .table = table } }); try appendAssistantTestEntry(&entries, alloc, 3, "After table.\n");
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out); try std.testing.expect(std.mem.find(u8, out, "Name") != null); try std.testing.expect(std.mem.indexOf(u8, out, "Before table.").? < std.mem.indexOf(u8, out, "Name").?); try std.testing.expect(std.mem.indexOf(u8, out, "Name").? < std.mem.indexOf(u8, out, "After table.").?);}
test "renderTableForTranscript reasserts bold after an inline strong header span" { const alloc = std.testing.allocator; var table = try assistant_presentation.parseTablePayload( alloc, "| prefix __strong__ suffix |\n" ++ "|------|\n" ++ "| value |\n", ); defer table.deinit(alloc);
const out = try renderTableForTranscript(alloc, table, 80); defer alloc.free(out); try std.testing.expect(std.mem.indexOf( u8, out, "\x1b[1mprefix \x1b[1mstrong\x1b[22m\x1b[1m suffix\x1b[22m", ) != null);}
test "Unicode display units remain atomic in three-column vertical tables" { const alloc = std.testing.allocator; const sequences = [_][]const u8{ "\u{2600}\u{FE0F}", "\u{231A}\u{FE0E}", "\u{1F44D}\u{1F3FD}", "\u{1F1FA}\u{1F1F8}", "#\u{FE0F}\u{20E3}", "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", "\u{1F469}\u{200D}\u{1F4BB}", };
for (sequences) |sequence| { const source = try std.fmt.allocPrint(alloc, "| S |\n|---|\n| {s} |\n", .{sequence}); defer alloc.free(source); var table = try assistant_presentation.parseTablePayload(alloc, source); defer table.deinit(alloc);
const out = try renderTableForTranscript(alloc, table, 3); defer alloc.free(out); try std.testing.expect(std.unicode.utf8ValidateSlice(out)); try std.testing.expect(std.mem.find(u8, out, sequence) == null); try std.testing.expect(std.mem.find(u8, out, "?") != null);
var lines = std.mem.splitScalar(u8, out, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 3); } }}
test "renderEntriesToBytes keeps semantic code as its own assistant entry" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendAssistantTestEntry(&entries, alloc, 1, "Before code.\n"); const block = assistant_presentation.CodeBlockPayload{ .language = try alloc.dupe(u8, "zig"), .code = try alloc.dupe(u8, " const value = 1;\n"), }; try entries.append(alloc, .{ .assistant_code_block = .{ .id = 2, .block = block } }); try appendAssistantTestEntry(&entries, alloc, 3, "After code.\n");
const out = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(out); try std.testing.expect(std.mem.indexOf(u8, out, "Before code.").? < std.mem.indexOf(u8, out, "const").?); try std.testing.expect(std.mem.indexOf(u8, out, "const").? < std.mem.indexOf(u8, out, "After code.").?); try std.testing.expect(std.mem.find(u8, out, "┌ \x1b[2mzig\x1b[22m ─") != null); try std.testing.expect(std.mem.find(u8, out, "\x1b[2mzig\x1b[22m\n┌") == null);
const narrow = try renderEntriesToBytes(alloc, entries.items, 6, .{}); defer alloc.free(narrow); try std.testing.expect(std.mem.find(u8, narrow, "┌") == null); var lines = std.mem.splitScalar(u8, narrow, '\n'); while (lines.next()) |line| { try std.testing.expect(display_width.visibleWidthIgnoringAnsi(line) <= 6); }}
test "renderEntriesToBytes rebuilds user card at paint-time cols" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
try appendUserTestEntry(&entries, alloc, 1, "this is a long user prompt that should wrap at narrow widths but not at a wide width");
const wide = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(wide); const narrow = try renderEntriesToBytes(alloc, entries.items, 30, .{}); defer alloc.free(narrow);
try std.testing.expect(narrow.len > wide.len); try std.testing.expect(std.mem.startsWith(u8, wide, user_message_card.promptMarkerStyle())); try std.testing.expect(std.mem.find(u8, wide, "┃") != null); try std.testing.expect(std.mem.find(u8, wide, "this is") != null); try std.testing.expect(std.mem.find(u8, narrow, "this is") != null);}
test "renderEntriesToBytes preserves selected skill token spans through user card reflow" { const alloc = std.testing.allocator; var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc);
const text = "raw $review then $review after"; const text_dup = try alloc.dupe(u8, text); var handed_off = false; errdefer if (!handed_off) alloc.free(text_dup); const images = try alloc.alloc(types.ImageAttachment, 0); errdefer if (!handed_off) alloc.free(images); const skill_tokens = try alloc.alloc(input_visual_layout.SkillTokenSpan, 1); errdefer if (!handed_off) alloc.free(skill_tokens); const token_name = try alloc.dupe(u8, "review"); errdefer if (!handed_off) alloc.free(token_name); const token_path = try alloc.dupe(u8, "/tmp/review"); errdefer if (!handed_off) alloc.free(token_path); skill_tokens[0] = .{ .raw_start = "raw $review then ".len, .raw_end = "raw $review then $review".len, .name = token_name, .path = token_path, }; try entries.append(alloc, .{ .user_turn = .{ .id = 1, .turn = .{ .text = text_dup, .images = images }, .skill_tokens = skill_tokens, } }); handed_off = true;
const wide = try renderEntriesToBytes(alloc, entries.items, 80, .{}); defer alloc.free(wide); const narrow = try renderEntriesToBytes(alloc, entries.items, 24, .{}); defer alloc.free(narrow);
try std.testing.expect(std.mem.find(u8, wide, "$review then ") != null); try std.testing.expect(std.mem.find(u8, wide, "\x1b[38;5;252mreview") != null); try std.testing.expect(std.mem.find(u8, wide, "$review then $review") == null); try std.testing.expect(std.mem.find(u8, narrow, "$review") != null); try std.testing.expect(std.mem.find(u8, narrow, "\x1b[38;5;252mreview") != null);}
test "renderEntriesToBytes keeps current user rails scoped through reflow" { const alloc = std.testing.allocator; user_message_card.setStyle(false, null); user_message_card.setStyle(false, .{ .r = 20, .g = 80, .b = 140 }); defer user_message_card.setStyle(false, null);
var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendUserTestEntry(&entries, alloc, 1, "this submitted prompt should wrap at narrow widths"); try appendRawTestEntry(&entries, alloc, 2, "Z", .unknown_raw);
for ([_]u16{ 80, 30 }) |cols| { const out = try renderEntriesToBytes(alloc, entries.items, cols, .{}); defer alloc.free(out); var grid = try vt_emulator.Grid.init(alloc, cols, 12); defer grid.deinit(); try grid.feed(out);
var prompt_found = false; var following_entry_found = false; var row: u16 = 1; while (row <= grid.rows) : (row += 1) { var col: u16 = 1; while (col <= grid.cols) : (col += 1) { const cell = grid.cellAt(row, col).?; if (cell.codepoint == '┃') { prompt_found = true; try std.testing.expect(cell.style.bg.eql(.default)); try std.testing.expect(cell.style.fg.eql(.{ .indexed = 255 })); } if (cell.codepoint == 'Z') { following_entry_found = true; try std.testing.expect(cell.style.bg.eql(.default)); try std.testing.expect(cell.style.fg.eql(.default)); } } } try std.testing.expect(prompt_found); try std.testing.expect(following_entry_found); }}
test "current compact rendering reflows user prompts without card background" { const alloc = std.testing.allocator; user_message_card.setStyle(false, .{ .r = 20, .g = 80, .b = 140 }); defer user_message_card.setStyle(false, null);
var entries: std.ArrayList(TranscriptEntry) = .empty; defer deinitTestEntries(&entries, alloc); try appendUserTestEntry(&entries, alloc, 1, "minimal prompt");
const out = try renderEntriesWithOverridesToBytes(alloc, entries.items, 80, .{}, &.{}); defer alloc.free(out);
try std.testing.expect(std.mem.startsWith(u8, out, "\x1b[38;5;255m┃\x1b[0m \x1b[1mminimal prompt")); try std.testing.expect(std.mem.find(u8, out, "\x1b[48;") == null); try std.testing.expect(std.mem.find(u8, out, "\x1b[K") == null);}