Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
178 kB · 4585 lines
Zig
at main
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586const 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 shared_theme = @import("../../core/shared/theme.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("../../core/agent/presentation/code_highlight.zig");const code_highlight_languages = @import("../../core/agent/presentation/code_highlight_languages.zig");const ui_render = @import("../render.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 = "", 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, // Recorded identities are archival and cannot pin live output. origin: enum { live, recorded } = .live, created_at_ms: i64 = 0, tool_name: []u8, captured_command: bool = false, activity_kind: ?types.ToolActivityKind = null, arguments_json: ?[]u8 = null, command_display: ?[]u8 = null, command_action_label: ?[]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.command_display) |value| alloc.free(value); if (self.command_action_label) |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, 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, turn_cancellation, 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, projection_part: enum { body, group_header, group_child, group_cancel } = .body, }, 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, .turn_cancellation => .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) { .semantic_notice => |notice| !notice.inline_hidden and notice.visibility == .compact_and_full, inline else => |payload| !payload.inline_hidden, };}
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, .turn_cancellation => .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, line_provenance: []const LineProvenance = &.{},};
pub const EntryRenderAction = union(enum) { keep, hide, override: struct { kind: TranscriptBlockKind, bytes: []const u8, line_provenance: []const LineProvenance = &.{}, },};
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, inline_hidden: bool = false, bytes: []const u8, class: RawEntryClass = .unknown_raw, lifecycle_pinned: bool = false, };
pub const SemanticNoticeEntry = struct { id: u32, created_at_ms: i64 = 0, inline_hidden: bool = false, 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, inline_hidden: bool = false, turn: types.UserTurn, skill_tokens: []input_visual_layout.SkillTokenSpan = &.{}, };
pub const AssistantTurnEntry = struct { id: u32, created_at_ms: i64 = 0, inline_hidden: bool = false, segments: AssistantTurnSegments, };
pub const AssistantTableEntry = struct { id: u32, created_at_ms: i64 = 0, inline_hidden: bool = false, table: assistant_presentation.TablePayload, };
pub const AssistantCodeBlockEntry = struct { id: u32, created_at_ms: i64 = 0, inline_hidden: bool = false, block: assistant_presentation.CodeBlockPayload, };
pub const AssistantThematicRuleEntry = struct { id: u32, created_at_ms: i64 = 0, inline_hidden: bool = false, };
pub fn hideInline(self: *TranscriptEntry) void { switch (self.*) { inline else => |*payload| payload.inline_hidden = true, } }
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 renderCodeBlockForTranscriptWithTheme(alloc, block, cols, .dark);}
fn renderCodeBlockForTranscriptWithTheme( alloc: Allocator, block: assistant_presentation.CodeBlockPayload, cols: u16, theme: CodeHighlightTheme,) ![]u8 { var rendered: std.ArrayList(u8) = .empty; errdefer rendered.deinit(alloc); if (cols == 0) return 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| if (resolved.diff_lines) try code_highlight.highlightDiff(alloc, block.code, theme, ui_render.diff_added_marker_style, ui_render.diff_removed_marker_style) else try code_highlight.highlight(alloc, block.code, resolved, theme, null) else null; defer if (styled_code) |code| alloc.free(code); const code = styled_code orelse block.code;
const max_code_width = maxCodeLineWidth(block.code); if (cols <= 5) { try renderUnboxedCode(alloc, code, cols, &rendered); return rendered.toOwnedSlice(alloc); }
const panel_width = codePanelWidth(max_code_width, language, cols); if (language.len > 0) { try appendCodePanelHeader(alloc, &rendered, panel_width, language); } else { try appendCodePanelRule(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], panel_width); emitted_line = true; if (end == code.len) break; start = end + 1; } if (!emitted_line) try appendCodePanelLine(alloc, &rendered, "", panel_width); try appendCodePanelRule(alloc, &rendered, panel_width); return rendered.toOwnedSlice(alloc);}
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 (shared_theme.sgrHasParam(sequence, "38")) { 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, cols: u16) usize { const label_width = if (language.len == 0) 0 else @min( display_width.visibleWidth(language), @as(usize, cols) - 4, ); return @min(@as(usize, cols), @max(@as(usize, 6), @max(max_code_width, label_width + 4)));}
fn appendCodePanelHeader( alloc: Allocator, out: *std.ArrayList(u8), panel_width: usize, language: []const u8,) !void { const label_limit = panel_width - 4; 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, "\x1b[2m─ "); try out.appendSlice(alloc, label); try out.append(alloc, ' '); var edge: usize = 0; while (edge < panel_width - 3 - label_width) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, "\x1b[22m\n");}
fn appendCodePanelRule( alloc: Allocator, out: *std.ArrayList(u8), panel_width: usize,) !void { try out.appendSlice(alloc, "\x1b[2m"); var edge: usize = 0; while (edge < panel_width) : (edge += 1) try out.appendSlice(alloc, "─"); try out.appendSlice(alloc, "\x1b[22m\n");}
fn appendCodePanelLine( alloc: Allocator, out: *std.ArrayList(u8), line: []const u8, panel_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 out.append(alloc, '\n'); return; } while (remaining.len > 0) { const continuation_indent = if (continuation and indent_width < panel_width) indent else ""; const available_width = panel_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 appendCodeRow(alloc, out, continuation_indent, fallback_style, "?", fallback_style); 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 appendCodeRow(alloc, out, continuation_indent, row_style, prefix, style); 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 appendCodeRow( alloc: Allocator, out: *std.ArrayList(u8), leading: []const u8, before: CodeStyle, content: []const u8, after: CodeStyle,) !void { 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.append(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; const line_start = cursor > 0 and (text[cursor - 1] == '\n' or text[cursor - 1] == '\r'); if (line_start and (std.mem.startsWith(u8, text[cursor..], "├ ") or std.mem.startsWith(u8, text[cursor..], "└ "))) 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, types.noticeGlyph(notice.tone)); try logical.append(alloc, ' '); // An empty topic drops the "topic:" label and renders the body alone. if (notice.topic.len > 0) { try logical.appendSlice(alloc, notice.topic); 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.wrapTranscriptAssistantTextWithFinalityInterruptible( alloc, e.segments.text.items, cols, checkpoint, ); const trimmed = trimAssistantBlockHead(wrapped.bytes); const trimmed_head_bytes = wrapped.bytes.len - trimmed.len; var block = try normalizeOwnedRenderedBlockWithAllocation( alloc, kind, trimmed, wrapped.bytes, ); block.assistant_finalized_prefix_bytes = wrapped.finalized_prefix_bytes -| trimmed_head_bytes; break :blk block; }, .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, ); defer alloc.free(rendered); const prefixed = try assistant_wrap.prefixStructuralRows(alloc, rendered, gutter); break :blk try normalizeOwnedRenderedBlock(alloc, kind, prefixed); }, .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); 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( "i 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_floor_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, .line_provenance = override.line_provenance, }, .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, block_provenance: []const LineProvenance, 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_floor_bytes[index] = self.out.items.len + (block.assistant_finalized_prefix_bytes orelse 0); } } 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); if (block_provenance.len > 0) { if (options.line_provenance) |lines| { std.debug.assert(block_provenance.len >= renderedHardLineCount(block.bytes)); try lines.appendSlice(alloc, block_provenance[0..renderedHardLineCount(block.bytes)]); } } else 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 { std.debug.assert(options.finality_entry_ids.len == options.finality_entry_floor_bytes.len); 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 (!isEntryVisibleInCompactPresentation(entry) or 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, override.line_provenance, 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 = &.{}, /// Parallel to `finality_entry_ids`. Each rendered nomination writes the /// first byte that remains mutable; immutable entries use their start. finality_entry_floor_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_floor_bytes = options.finality_entry_floor_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( TranscriptBlockKind.cancel_notice, blockKindForRawClass(.turn_cancellation), ); 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, assistant_finalized_prefix_bytes: ?usize = null,
pub fn deinit(self: RenderedBlock, alloc: Allocator) void { if (self.owned) alloc.free(self.allocation); }};
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" }; const glyphs = [_][]const u8{ "i", "✓", "!", "✗", "⊘" };
for (tones, label_styles, glyphs) |tone, label_style, glyph| { 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}{s} topic:\x1b[0m\x1b[37m body\x1b[0m", .{ label_style, glyph }, ); defer alloc.free(expected); try std.testing.expectEqualStrings(expected, rendered); try std.testing.expect(std.mem.find(u8, rendered, "●") == null); 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 glyph grid regression locks tone markers and lowercase topic" { 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 cases = [_]struct { tone: types.NoticeTone, topic: []const u8, body: []const u8, row_text: []const u8, label_fg: u8, }{ .{ .tone = .neutral, .topic = "session", .body = "renamed to \"custom models\"", .row_text = "* session: renamed to \"custom models\"", .label_fg = 7 }, .{ .tone = .information, .topic = "background", .body = "command #7 started", .row_text = "i background: command #7 started", .label_fg = 6 }, .{ .tone = .success, .topic = "upgrade", .body = "fx has been updated to v9.9.9", .row_text = "✓ upgrade: fx has been updated to v9.9.9", .label_fg = 2 }, .{ .tone = .warning, .topic = "skills", .body = "1 discovery issue", .row_text = "! skills: 1 discovery issue", .label_fg = 3 }, .{ .tone = .@"error", .topic = "session", .body = "usage: /rename <title>", .row_text = "✗ session: usage: /rename <title>", .label_fg = 1 }, .{ .tone = .cancelled, .topic = "system", .body = "cancelled", .row_text = "⊘ system: cancelled", .label_fg = 8 }, };
for (cases) |case| { const rendered = try renderSemanticNotice(alloc, .{ .topic = case.topic, .tone = case.tone, .body = case.body, }, styles, 80); defer alloc.free(rendered);
var grid = try vt_emulator.Grid.init(alloc, 80, 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(case.row_text, row.items);
// The glyph and lowercase topic carry the tone color; the body is grey. // Columns are 1-based: glyph, space, topic cells, then the colon. const colon_col: u16 = @intCast(3 + case.topic.len); const fg_index = struct { fn get(cell: vt_emulator.Cell) u8 { return switch (cell.style.fg) { .indexed => |ix| ix, else => 255, }; } }.get; try std.testing.expectEqual(case.label_fg, fg_index(grid.cellAt(1, 1).?)); try std.testing.expectEqual(case.label_fg, fg_index(grid.cellAt(1, colon_col).?)); try std.testing.expectEqual(@as(u8, 7), fg_index(grid.cellAt(1, colon_col + 2).?)); }}
test "semantic notice topics never render the tool-activity bullet or forced capitalization" { const alloc = std.testing.allocator; for ([_]types.NoticeTone{ .neutral, .information, .success, .warning, .@"error", .cancelled }) |tone| { const rendered = try renderSemanticNotice(alloc, .{ .topic = "mcp", .tone = tone, .body = "body", }, .{}, 80); defer alloc.free(rendered); try std.testing.expect(std.mem.find(u8, rendered, "●") == null); try std.testing.expect(std.mem.find(u8, rendered, "Mcp") == null); try std.testing.expect(std.mem.find(u8, rendered, "mcp:") != null); }}
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( "i 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 tree branches align with the header while wrapped prose stays indented" { const alloc = std.testing.allocator; const cases = [_]struct { body: []const u8, cols: u16, expected: []const u8 }{ .{ .body = "2 skills loaded\n├ Loaded alpha\n└ Loaded beta", .cols = 40, .expected = "* 2 skills loaded\n├ Loaded alpha\n└ Loaded beta" }, .{ .body = "Ready\n└ alpha beta gamma", .cols = 12, .expected = "* Ready\n└ alpha beta\n gamma" }, .{ .body = "alpha └ beta", .cols = 8, .expected = "* alpha\n └ beta" }, .{ .body = "Ready\nordinary prose", .cols = 40, .expected = "* Ready\n ordinary prose" }, }; for (cases) |case| { const rendered = try renderSemanticNotice(alloc, .{ .topic = "", .tone = .neutral, .body = case.body }, .{}, case.cols); defer alloc.free(rendered); try std.testing.expectEqualStrings(case.expected, rendered); } const styled = try renderSemanticNotice(alloc, .{ .topic = "", .tone = .neutral, .body = cases[0].body, }, .{ .system_notice_text_style = "\x1b[37m", .reset_style = "\x1b[0m" }, 40); defer alloc.free(styled); var grid = try vt_emulator.Grid.init(alloc, 40, 4); defer grid.deinit(); try grid.feed(styled); try std.testing.expectEqual(@as(u21, '*'), grid.cellAt(1, 1).?.codepoint); try std.testing.expectEqual(@as(u21, '├'), grid.cellAt(2, 1).?.codepoint); try std.testing.expectEqual(@as(u21, '└'), grid.cellAt(3, 1).?.codepoint);}
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, "i 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("i 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("i 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\ni 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 dims solid horizontal rules without side rails" { 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[2m─ zig ─\x1b[22m\n" ++ "x\n" ++ "\x1b[2m───────\x1b[22m\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( "\x1b[2m──────\x1b[22m\n" ++ "x\n" ++ "\x1b[2m──────\x1b[22m\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[2m─ type ─\x1b[22m\n" ++ "x\n" ++ "\x1b[2m────────\x1b[22m\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" ++ "\x1b[2m──────\x1b[22m\n", wide_rune, );}
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[2m─ python ─") != 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[2m─ ts ─") != 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[2m─ json ─") != 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[2m─ brainfuck ─") != null); try std.testing.expect(std.mem.indexOf(u8, explicit_unknown, "\x1b[38;5;") == null);}
test "renderCodeBlockForTranscript contains CJK fallback color in ruled 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 ruled = try renderCodeBlockForTranscript(alloc, block, 6); defer alloc.free(ruled); var ruled_grid = try vt_emulator.Grid.init(alloc, 6, 24); defer ruled_grid.deinit(); try ruled_grid.feed(ruled); try ruled_grid.feed("z");
var ruled_wide_rune: ?vt_emulator.Cell = null; var row: u16 = 1; while (row <= 24) : (row += 1) { var col: u16 = 1; while (col <= 6) : (col += 1) { const cell = ruled_grid.cellAt(row, col).?; if (cell.codepoint == '\u{6f22}') ruled_wide_rune = cell; if (cell.codepoint == '\u{2508}' 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(ruled_wide_rune != null); try std.testing.expect(ruled_wide_rune.?.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 preserves parser-rendered list paragraph indentation" { 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, "1. Heading\n alpha beta gamma delta\n\n- Heading\n alpha beta gamma delta\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 narrow = try renderEntriesToBytes(alloc, entries.items, 18, .{}); defer alloc.free(narrow); try std.testing.expect(std.mem.find(u8, narrow, " alpha beta\n gamma delta") != null); try std.testing.expect(std.mem.find(u8, narrow, " alpha beta\n gamma delta") != null);}
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[2m─ zig") != 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[2m─ zig ─") != 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);}