Repository navigation
Expand file tree
/
Copy pathapp.go
More file actions
10886 lines (10403 loc) · 492 KB
/
Copy pathapp.go
File metadata and controls
10886 lines (10403 loc) · 492 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package tui3
import (
"context"
"errors"
"fmt"
"github.com/Agent-Field/codeaf/internal/chatlist"
"github.com/Agent-Field/codeaf/internal/dirwatch"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync/atomic"
"time"
tea "charm.land/bubbletea/v2"
"github.com/Agent-Field/codeaf/internal/atlas"
"github.com/Agent-Field/codeaf/internal/buildinfo"
"github.com/Agent-Field/codeaf/internal/config"
"github.com/Agent-Field/codeaf/internal/connect"
"github.com/Agent-Field/codeaf/internal/credits"
internalenv "github.com/Agent-Field/codeaf/internal/env"
"github.com/Agent-Field/codeaf/internal/knowledge"
"github.com/Agent-Field/codeaf/internal/modelsource"
"github.com/Agent-Field/codeaf/internal/remote"
"github.com/Agent-Field/codeaf/internal/session"
"github.com/Agent-Field/codeaf/internal/skills"
"github.com/Agent-Field/codeaf/internal/subharness"
"github.com/Agent-Field/codeaf/internal/tui2/tokens"
codeupdate "github.com/Agent-Field/codeaf/internal/update"
)
// frameInterval is the repaint ceiling: at most one frame is BUILT per 33ms,
// no matter how many deltas arrive inside it. A provider that streams token by
// token would otherwise re-wrap the live paragraph a hundred times a second to
// show the reader thirty of them, and the thirty it showed would be the same
// thirty.
//
// It is also the only clock on this surface. The spinner and the ellipsis step
// on multiples of it (see spinnerStep, pulseStep) rather than on schedules of
// their own: two animation clocks means two wakeups per second per animation
// and rows that drift out of phase with each other.
//
// It is the LOCAL cadence, and it is the unit the animations are counted in
// either way: a surface being read over a connection builds fewer frames and
// steps the same distance through each one (link.go's [app.frameEvery] and
// [app.frameStride]).
const frameInterval = 33 * time.Millisecond
// markdownThrottle is how often a streaming reply's settled prefix is promoted
// from plain wrapped text to rendered markdown. See [app.assistantRows].
const markdownThrottle = 1500 * time.Millisecond
// usageEvery is how many frame slots pass between asks for the session's
// running cost. The agent answers under a lock, and a lock taken thirty times a
// second to move a figure that changes once a turn is a lock taken for nothing.
//
// SLOTS AND NOT FRAMES, which is a third of a second either way: the question
// is how often the lock is worth taking in wall time, and that answer does not
// change because the frames arrived over a wire (link.go's [app.dueEvery]).
const usageEvery = 10
// compactStillRunning is what /compact says when the engine is still working
// after the surface's wait ran out: the pass lands on its own.
const compactStillRunning = "still compacting — it is taking longer than usual and finishes on its own; the token count in the status line drops when it lands"
// quietBeforeEllipsis is how long the stream has to be silent before the
// ellipsis appears under a reply that is already streaming. Text arriving in
// chunks a few hundred milliseconds apart is a working model, not a stalled
// one, and a dot that blinks in between every chunk is noise.
const quietBeforeEllipsis = 700 * time.Millisecond
// streamFreshFor is how long after the last delta a reply still counts as
// arriving, and so keeps the paint clock at its full cadence. Deltas land a
// few hundred milliseconds apart in a working stream; past this the only
// thing moving is the spinner, which changes glyph every spinnerStep-th
// paint, and the wait steps at that cadence instead.
const streamFreshFor = 250 * time.Millisecond
// runState is the word in the status line.
type runState int
const (
stateIdle runState = iota
stateWorking
stateInterrupted
)
func (s runState) String() string {
switch s {
case stateWorking:
return "working"
case stateInterrupted:
return "interrupted"
default:
return "idle"
}
}
// entryKind is what one block of the conversation is.
type entryKind int
const (
entryUser entryKind = iota
entryAssistant
entryTool
entryDivider
// entryCompact is ONE compaction pass, from the moment it starts to the
// moment it stops — a row with a clock rather than a mark left behind.
//
// It is a kind of its own rather than a divider written twice because a
// compaction is the one thing this surface waits on that is neither the
// model speaking nor a tool running: a paid summarizer call that can hold a
// turn for ten seconds while the screen says nothing. It opens as a spinning
// line ("compacting ~84k tokens · 6s") and SETTLES IN PLACE into the rule the
// divider always drew, so the finished conversation reads exactly as it did
// before this wave and the running one stops being a silence.
entryCompact
entryNote
// entryThinking is one turn's reasoning (thinking.go): streamed while it
// arrives, collapsed to a single row the moment the turn says anything else.
entryThinking
// entrySteer is ONE CORRECTION TYPED INTO A RUNNING TURN, drawn at the point
// in the transcript where it was said (steerelbow.go).
//
// It is a block of its own rather than a list hanging off the question for
// the reason entryCompact is not a divider: WHERE a thing happened is part of
// what happened. A correction belongs between the tool rows it interrupted,
// because that is the moment the person said it and that is the order the
// engine's own journal keeps it in — and a page that gathered every
// correction back up under the question would put them above every row of
// the work, which on a turn long enough to scroll is above the screen.
entrySteer
// entryTask is ONE TASK PROPOSAL — the decision moment, drawn where it
// happened (task.go). It keeps its verdict afterwards, the way a consent
// row does, because "the model asked to go and do this and you said yes" is
// the only record of where a running node came from.
entryTask
// entryDone is ONE TASK THAT CAME HOME (taskdone.go): the card that lands
// when a node reaches its final state, minutes after the turn that proposed
// it and usually with no turn open at all.
//
// It is a kind of its own rather than the note it used to be for the reason
// entryCompact is not a divider: a note is the surface muttering about its
// own housekeeping, and this is the only statement of an outcome a person
// delegated ten minutes of work to get.
entryDone
// entryHarness is one harness design, live and settled, in the transcript.
// The same entry changes shape so progress never leaves a dead note behind.
entryHarness
// entryConnect is ONE SIGN-IN (connect.go): the browser opening, the link
// under it for whoever is not sitting at that browser, and the one line it
// settles into.
//
// It is a kind of its own rather than a note for the reason entryCompact is
// not a divider: a note is a static sentence, and this is a block that opens
// waiting — with a spinner, and a link a person may need to copy — and closes
// minutes later on an event nobody typed.
entryConnect
// entrySeam is the one line at the boundary a compaction left behind, drawn
// when scrolling up crosses from the conversation the model still carries
// into the conversation only the journal does (replay.go's [seamMark]).
//
// IT IS A BLOCK AND NOT A ROW, which is the opposite of the choice
// [earlierMark] makes two doors down, and the difference is what each one is
// about. That marker is a fact about the SCREEN — "the top of the frame is
// not the top of the conversation" — and it moves as the frame does, so it is
// painted at the frame's edge and belongs to nobody. This is a fact about the
// CONVERSATION: it names the moment the model's memory of it was shortened,
// and that moment stays where it happened while the reader scrolls on past
// it. A line that could only be drawn at the top edge could not say it.
//
// It is still the surface's own line and not the session's: the journal holds
// no such message, [fromTranscript] steps over it so a rewind cannot cut it,
// and a rebuild earns it again from scratch.
entrySeam
// entryAutomation is ONE AUTOMATION touching the conversation
// (automation.go): the card a proposal or a typed line raises while it is a
// question, and — in its other shape — one dim line of news about an
// automation or a run of one. The lines are the surface's own and never the
// model's.
//
// It is a kind of its own rather than a second [entryTask] because the two
// blocks are answered by different doors and settle into different records,
// and rather than a note because a note is a static sentence where this
// opens as a question.
entryAutomation
// entryTeam is ONE NOTE A CONVERSATION'S TEAM SENT IT, drawn as the quoted
// cards it is (teamcard.go) rather than as the session's dim lane or the
// person's own line.
entryTeam
)
// toolState is where one call is in its life, and it is the whole of what the
// left of a tool line says (toolview.go draws it):
//
// ◌ toolForming the model is still SPELLING IT OUT — a dim pulse
// ◌ toolQueued the model finished asking; NOTHING has started
// ? toolConsent it is blocked on a person — the question hue, and the row
// with it
// ⠋ toolRunning it is executing: the braille spinner, and only here
// toolOK it finished, quietly, with its elapsed time
// ✗ toolFailed it failed, loudly, and its detail is already open
//
// The middle two states are the fix for one defect: a mutating call sat spinning
// while the RESPONSE streamed (nothing had started), and a call waiting for an
// answer spun identically to one doing work. A spinner is a claim about
// motion, so it is now spent on motion alone.
//
// The zero value is toolQueued and that is deliberate: an entry built from an
// announcement carries no status field, and the only honest default for a call
// nobody has said anything else about is "asked for".
type toolState int
const (
toolQueued toolState = iota
toolConsent
toolRunning
toolOK
toolFailed
// toolForming is the phase BEFORE queued: the call is still ARRIVING, one
// fragment at a time, and what is on screen is the model writing it rather
// than anything the harness is doing (session.EventToolForming).
//
// It is LAST in this block rather than first, where the life of a call would
// have put it, because the zero value above is load-bearing: a row built
// from an announcement sets no status, and "forming" is the one state such
// a row must never default to — it would claim the model is still typing a
// call it has finished asking for.
toolForming
)
// live reports whether a call has not resolved yet — queued, waiting on a
// person, or executing. Every "find the row this event is about" walk asks
// this rather than comparing against toolRunning, which was the whole test back
// when running was the only unresolved state there was.
//
// A FORMING CALL IS NOT LIVE, and that is the deliberate half of this: the
// walks that ask this question are looking for the row a consent question, a
// begin or an end belongs to, and every one of those events is about a call the
// model has FINISHED asking for. A half-sent call cannot be any of them, and a
// row that answered to them would take an event belonging to the call beside it.
func (s toolState) live() bool { return s == toolQueued || s == toolConsent || s == toolRunning }
// entry is one block of the conversation, and its rendered rows.
//
// Tool activity is an entry rather than a decoration on the assistant's block
// because it lands in place, between two paragraphs of a reply, and reads in
// the order it happened.
//
// The cached rows are the second half of the snappiness contract: an entry
// renders when its own content or the width changes, and a frame joins what is
// already there. Nothing in here holds a blank row — spacing is [app.layout]'s
// and only [app.layout]'s.
// A shared response identity keeps prose and its private reasoning together.
// It is pending until the response boundary confirms an answer.
type responseConfirmation struct{ done bool }
type entry struct {
// Clarification entries are owned by their independent feed.
discussionID string
discussionIndex int
// team is what a team delivery handed the conversation, line by line, on
// an [entryTeam] (teamcard.go); nil on every other entry.
team []session.TeamLine
kind entryKind
text string
turn int
// plainTags are the slash words the person demoted with backspace before
// sending, as rune ranges into the displayed message text (entry.text). A
// demoted word stays plain in the transcript exactly as it did in the box:
// [transcriptCommandSpans] subtracts these ranges before painting a chip,
// and an edit before the word carries the range along with it. They are
// empty on every message with no demotion.
plainTags []segment
// replyTags are the finished tasks this assistant block answers. They are
// empty for every ordinary person-prompted reply.
replyTags []session.TaskReplyTag
// facts are the LOAD-BEARING DATA inside a note's own words, named by the
// text they are spelled with and in the order they appear in it — the model
// ids in a crew line, the figures in /status, the key chords in /help.
//
// THE PAYLOAD RULE (payload.go) is what reads them: a note's prose stays in
// the dim tier this surface says everything about itself in, and each of
// these steps up one rung so the answer inside the sentence reads at a
// glance. It is empty on every note whose builder named nothing, and such a
// note is drawn exactly as it was drawn before the rule existed.
facts []string
// block says this note's OWN LINE STRUCTURE is what it means, so the frame
// fits each line to the width rather than re-flowing the paragraph
// ([app.noteBlock] says why, and a subharness card is the only shape that
// asks for it). It is false on every other note, which is nearly all of them.
block bool
// sheet says this note is a column, so a wrapped row keeps the column its
// first line already uses ([wrapSheet]). /help is the one note that asks
// for it: a continuation that started at the margin read as another key.
sheet bool
// told says this note is ADDRESSED TO THE PERSON rather than narration
// about the machinery, so the work chip may not swallow it (workfold.go's
// [deriveWorkfolds]).
//
// THE LAW THE CHIP ALREADY KEEPS, SAID PRECISELY. A chip hides what the
// turn DID between a question and its answer, and a line asking the person
// to do something is not that — the interrupt's own lines have always been
// held out of one for exactly this reason. Until this field the only way to
// ask was to look at the note's first word, so a sentence about a person's
// own pin being refused was folded away whole: measured on 2026-09-13,
// `@deepseek` gone from the model word and `▸ worked 1.6s · ctrl+e` where
// the explanation should have been (session's EventRowNews).
told bool
// summarized says an [entryCompact] pass replaced some of the conversation
// with a summary ([session.Event.Summarized]). Only such a pass stands
// outside the turn's fold (workfold.go); a free one folds with the work.
summarized bool
// carried marks the note naming the skills a turn carried (session's
// turnSkillsNotice). It is not addressed to the person, so it does not hold
// a turn open the way [entry.told] does; it is a record of what the
// person's message took with it, so it sits under that message and a chip
// starts below it rather than swallowing it ([countWork]). Folded, the line
// vanished for good: an opened chip lists calls, not notes, so the only
// screen evidence that a skill reached a turn lasted as long as the turn.
carried bool
// context is the NAMED WORKING CONTEXT this turn was routed into, in the
// engine's own person-facing words (session's TaskNotice.Context) — and empty
// for every ordinary turn, which is nearly all of them. It is set on the
// person's own block and read by nothing else (turncontext.go states the law).
//
// IT IS TAKEN AT THE MOMENT THE TURN STARTS AND NEVER DERIVED AFTERWARDS. The
// context a sentence went into is a fact about the past, and a transcript that
// asked the live session where its old lines had gone would re-label a whole
// history every time the person walked into a different room.
context string
// pictures are the resolved paths of the PICTURES this message carried, in
// the tray order its `[image #n]` tokens and `[#n name]` markers use.
// picturesHere says those paths name this machine rather than the engine's.
//
// IT IS A SLICE OF PATHS AND NOT CHIPS FOR [entry.hung]'s BUDGET REASON. A
// chip's file flag is a fact about the tray and every path here is already a
// picture; retaining that larger staging shape on every transcript block
// would make the hot entry carry a distinction no renderer can read.
pictures []string
picturesHere bool
// pictureExpanded is a one-based attachment index; zero keeps every image collapsed.
// Only one attachment in a message expands at a time, and replay resets it.
pictureExpanded int
// steer is THE ONE CORRECTION this block is, on [entrySteer] and nil on every
// other kind (steerelbow.go). It is a pointer for the reason [entry.card] and
// [entry.auto] are: the outcome lands on the block minutes after it was
// drawn, and the lane that lands it holds the block by index rather than
// copying it.
//
// Nil on every block of every conversation nobody steered, which is nearly
// all of them.
steer *steerElbow
// Tool fields.
tool string
status toolState
detail toolDetail
// caption is the most recent narrator override for the step this call
// belongs to. It stays on the call because captions are derived from the
// entry list on every page, and the list is the one fact both pages share.
caption string
// captionCat is the FAMILY the narrator named for that same step
// (session's actioncategory.go), and it travels with [entry.caption] for
// the same reason and on the same terms: one event carries both, so the
// sentence and the mark beside it can never be one repaint out of step.
//
// It is empty on every call the narrator never spoke about — which is most
// of them, and every call in a conversation reopened from a file — and the
// gutter derives the family from the tool names instead ([stepCategory]).
captionCat session.ActionCategory
open bool // this call's expansion is showing inline
// full lifts the expansion's per-tool cap: it is set by a click on the
// "… N more lines" foot, which is the person saying they want the rest.
//
// AND IT IS THE INSTRUCTION'S FOLD on a block that carries [entry.brief]
// (brieffold.go), because it is the same fact about a block — this one is
// shown whole — and a second flag saying it would be a second thing to keep
// in step. The two never meet: one is a tool call and the other is a message.
full bool
// brief says this block is THE INSTRUCTION A NODE WAS GIVEN — the first thing
// said on a task's own page, and the only message on this surface that folds
// (brieffold.go). It is false on every other block, which is nearly all of
// them.
//
// IT IS SET WHERE THE FACT IS KNOWN (replay.go's [roomReplay]) and
// never worked out at render time, because [app.renderEntry] paints one block
// at a time and must not ask what surrounds it — the same law the answer
// hierarchy is stamped under ([app.deckRows]).
brief bool
// decision is what the person answered when this call was asked about —
// "allowed" or "denied", dim, beside the row's stat (consent.go). It is
// empty for every call the policy did not stop.
decision string
// bg is set when this running call was kept in the background — by ctrl+g,
// the row's pointer door, or the session's clock (background.go): `job 3`,
// dim, beside the row's stat, in the slot [entry.decision] already uses
// because it is the same kind of fact. It is empty for every call nobody
// promoted.
bg string
// callID is the PROVIDER's id for this call (session.Event.CallID), taken
// off the forming events and kept so the announcement lands on the row that
// was already drawing the same call. It is empty for every row this surface
// never saw form — a non-streaming provider announces calls with no id in
// front of them — and empty on a forming row until the wire says one.
callID string
// bytes is how much of a FORMING call's arguments has arrived. It is the
// row's only figure while nothing else about the call is known yet, and it
// stops mattering the moment the call is whole.
//
// THE ARGUMENTS THEMSELVES ARE NOT KEPT. Half a JSON object is not a
// payload; the size of it is a fact, and the fact is what the row shows
// (session.go's ArgsText contract).
bytes int
// formed is the TAIL of the one argument a still-arriving call is previewed
// by — a write's file body, and nothing else on the belt ([formingPreviewField]).
// It is what the row draws its live block from (toolview.go's [app.formingRows]).
//
// IT IS NOT THE ARGUMENTS AND IT IS NOT PARSED. session's [session.PartialString]
// scans the streamed text for one field's value with the same tolerant scanner
// the forming hints are built from, and what lands here is the decoded text
// that scan returned — already bounded by session, already only the end of
// it. The field above still says how much has arrived; this says what it
// looks like. It is dropped the moment the announcement brings the whole
// payload, because from then on [entry.detail] is the better answer.
formed string
// began and ended are the block's clock, and the two entry kinds that have
// one read it the same way — the moment the thing started and the moment it
// stopped:
//
// entryThinking the first and last reasoning delta (thinking.go)
// entryTool EXECUTION started (EventToolBegin) and ended, which is
// what the line's trailing "1.2s" measures. A call's
// announcement is deliberately not in it: the time a
// response spent streaming is not time the tool ran.
//
// settled collapses a thinking block and open expands it again.
began, ended time.Time
// ran is what THIS call's own work took, measured where it ran and reported
// the instant it was over (session.EventToolFinished). It is zero on every
// row that has not had that news yet, and on every kind but entryTool.
//
// It exists because began-to-ended is the BATCH's span, not the call's: the
// calls of one batch start together and their results are delivered together
// after the last of them returns, so a five-millisecond `cd` beside a
// fifty-second build spun under a climbing clock and then wrote fifty
// seconds on its own row. When this is set it is the row's figure and the
// row's clock stops (toolview.go's [elapsedWord], [app.countClock]).
ran time.Duration
// latched says THE PERSON decided this block's expansion, rather than the
// stream deciding it for them (thinking.go). It exists because a reasoning
// block is the one thing on this surface whose open/closed state is written
// by two authors: the reader, with ctrl+e or a click, and the turn, which
// collapses the block the moment it says anything that is not reasoning.
// Without the latch the second author always wins — a person opens a think
// mid-stream, the next delta settles the block, and their choice is gone —
// which is the defect this field exists to make impossible.
latched bool
// Assistant fields. settled means the block is finished and renders
// through renderMarkdown; mdCut is how many bytes of a still-streaming
// block have already been promoted to markdown.
settled bool
mdCut int
// edge is how far the LIVE EDGE has been drawn (reveal.go). Zero means the
// block is not pacing — settled history, anything the wire delivered
// fine-grained, anything the clock has not been asked to walk. A block with
// a lump still arriving holds the received bytes in [entry.text] and this
// cursor is what the frame paints.
edge int
// mdHead is the PROMOTED HALF of a still-streaming block, already rendered,
// kept beside the cut and the width it was rendered at (render.go's
// [promotedHead]). It is a pointer for [entry.hung]'s reason: at most one
// block on a deck is streaming, and every other entry would be carrying the
// fields past three whole-deck passes per frame to say nothing with them.
//
// It exists because the two clocks on a streaming reply run at wildly
// different speeds. A delta lands a hundred times a second and marks the
// block stale; the promotion that moves [entry.mdCut] runs once every
// [markdownThrottle], which is a second and a half. Every frame in between
// re-ran [app.renderMarkdown] over the WHOLE settled prefix — sanitize,
// goldmark, chroma, the lot — to arrive at rows identical to the ones it drew
// on the last frame, when the only thing that had actually changed was the
// plain tail underneath them ([app.assistantRows]).
//
// THE CUT AND THE WIDTH ARE THE WHOLE KEY, because they are the whole of what
// decides these rows. The prefix is `text[:mdCut]` and a prefix cannot change
// without the cut moving: text only ever GROWS at its end, and a promotion
// only ever moves the cut forward ([promoteBlock] refuses a cut that did not).
//
// The one thing the key cannot see is the PALETTE, for the reason codeview.go's
// block cache cannot: these are finished strings with escape sequences already
// inside them, and a re-measured ground changes neither the text nor the width.
// [app.repaintPalette] drops it by hand, with everything else that holds paint.
mdHead *promotedHead
// hung is the memo of the block a TOOL row hangs — its diff, its source, its
// output — and toolview.go's [app.toolBlock] is the whole of its story.
//
// IT IS A POINTER TO KEEP THIS STRUCT SMALL. Every deck is a slice of these
// and three passes walk the whole of one on every frame ([stampHierarchy],
// [app.deckRows], THE INDENT LAW's own loop), so a hundred and fifty bytes
// added to an entry is a hundred and fifty bytes of cache line spent by every
// block on the screen to carry a memo only tool rows ever read. Measured: as
// a value it cost the idle frame seven percent, which is most of what the
// memo was buying.
hung *toolBlock
// thread is the memo of a thread card: what a manager's team_send row
// hangs, or the answers under a team note's quoted line
// (teamthreadcard.go). A pointer for the reason hung is one.
thread *threadMemo
// demoted says THIS PROSE WAS NARRATION AND NOT THE ANSWER, and it is the
// whole of THE ANSWER HIERARCHY as far as a renderer is concerned
// (hierarchy.go states the law and [stampHierarchy] writes this field).
//
// It is DERIVED and never authored: a block is narration exactly when more
// work opened after it inside the same turn, or its streaming response is
// not yet classified. Those facts come from entries and response boundaries.
// It is re-derived on every layout from the list itself — a resumed conversation, a rewound one and the live
// one all reach the same answer — and stored here only because
// [app.renderEntry] paints one block at a time and must not walk the list to
// find out which kind of block it is holding.
//
// FLIPPING IT INVALIDATES THE ROW CACHE, which is why nothing sets it by
// hand: the demoted rendering and the promoted one are different rows, and a
// block that changed tier while holding the rows it drew in the other one
// would keep them ([app.entryRows] hands back the cache unless [entry.stale]
// says otherwise).
demoted bool
// Provisional prose has arrived, but the response has not yet confirmed
// whether it ends in an answer or a tool call. It stays in the work view.
provisional bool
// addressed is an explicit assistant update, visible while its body streams.
// Completion remains a separate response confirmation, so stopping a partial
// update cannot present it as a finished reply.
addressed bool
// The same identity can be attached while the reply is still pending. Its
// done flag confirms ownership before any private tail receives a work fold.
confirmed *responseConfirmation
// capHead says this demoted block lent its first line to the step heading,
// and capCut is the byte immediately after that line. Both are derived with
// the hierarchy and invalidate the block when they move.
capHead bool
capCut int
// cut says THE TURN THIS BLOCK BELONGS TO WAS STOPPED BY THE PERSON, and it
// is the one part of the hierarchy that cannot be read off the list's shape:
// a stopped turn and a finished one end with exactly the same blocks in
// exactly the same order, and only the moment of the interrupt knows which
// happened ([app.cutTurn] writes it, [app.interrupt] and [app.settle] call
// it).
//
// AN INTERRUPTED TURN PROMOTES NOTHING. The turn ended without producing a
// structural answer, so its trailing prose keeps the working tier for good —
// the absence of a flush, full-ink block under the work is itself the
// statement that no answer was reached.
//
// IT IS A FACT ABOUT THIS WINDOW. The journal keeps the words a stopped turn
// managed to say and keeps no mark saying it was stopped, so a session
// resumed later rebuilds that turn from its shape alone and reads its last
// paragraph as an answer. That is the honest limit of a structural rule: the
// alternative is a heuristic over the text, and this surface does not sniff
// text to decide what a block is.
cut bool
// tables is which of this answer's markdown tables the person has opened,
// by the ordinal they appear in (mdtable.go). Nil means every one of them is
// closed, which is what an answer with no table in it stays.
//
// It is state on the ENTRY rather than on the surface because it belongs to
// the block: an answer scrolls, a room is opened and closed, and a table a
// person opened has to still be open when they come back to the sentence
// they opened it for.
tables map[int]bool
// feet are the table affordances this block drew, keyed by their index in
// its own rows. They are written by the same render that writes [entry.rows]
// and cached beside them, because the columns a foot occupies are a fact
// about a row that has already been laid out — see [tableFoot].
feet map[int]tableFoot
// card is the proposal this entry draws, for kind entryTask and for nothing
// else (task.go). It is a POINTER because the answer lane holds the same
// card: a row and the verdict on it must not be able to disagree.
card *taskCard
// conn is the sign-in this entry draws, for kind entryConnect and for
// nothing else (connect.go). It is a POINTER for the reason the card above
// is: the row opens waiting and a later event settles it in place, and the
// row and that state must never be able to disagree.
conn *connectCard
// done is the card a landed node writes, for kind entryDone and for nothing
// else (taskdone.go). Like [entry.card] it is a POINTER, because the card
// carries the one piece of state a person can change about it — whether its
// full context is showing — and the row and that state must never disagree.
// completion is operational history disclosed under the request that owns it.
completion *workCompletion
done *taskDone
harness *harnessCard
// auto is the automation card or line this entry draws, for kind
// entryAutomation and nothing else (automation.go). It is a POINTER for
// [entry.card]'s reason: the answer lane holds the same card, and the row
// and the verdict on it must never be able to disagree.
auto *automationCard
// pending says this is the PERSON'S OWN LINE, echoed before the engine has
// agreed to take it — the gap a connection puts between pressing enter and
// the far end answering (echo.go). It is false on every block of every
// conversation held at this machine, and false again the instant the engine
// confirms.
//
// It changes ONE thing about the block: the tier its words are painted in
// (render.go's entryUser). No badge, no spinner, no colour of its own — the
// ordinary case is a confirmation a few frames later, and a mark loud enough
// to notice would read as something having gone wrong every time a person
// sent a message.
pending bool
// The row cache. Its key names the three facts that can change painted rows:
// which block owns them, the width they wrap at, and the ladder that supplied
// their ink. Staleness is the block itself changing between two readings of
// that key; built distinguishes "no rows yet" from "renders to no rows".
rows []string
rowKey renderedEntryKey
identity uint64
width int
built bool
stale bool
}
// renderedEntryKey keeps the cache contract explicit. The cache lives on the
// entry, so identity looks redundant until a block value is moved or reused;
// keeping it in the key makes a cached slice incapable of answering for the
// next block that happens to occupy the same storage.
type renderedEntryKey struct {
identity uint64
width int
ink uint64
}
// forming reports whether this row is a call the model is STILL SPELLING OUT.
//
// The ended clock is what takes it back out again: a forming row whose stream
// died is resolved rather than removed ([app.dropForming]), and the row that is
// left says the call was cancelled — so "still arriving" has to mean the state
// AND an unstopped clock, or a dead row would keep pulsing at a model that has
// stopped speaking.
func (e *entry) forming() bool {
return e.kind == entryTool && e.status == toolForming && e.ended.IsZero()
}
// The messages the surface moves on. Every stream message carries the
// generation of the stream it came from: a turn that was interrupted while its
// channel still had events in flight must not paint over the turn that
// replaced it.
type (
submittedMsg struct {
call *hostCall
ch <-chan session.Event
err error
// echo names WHICH echoed line this answer settles, and ZERO when none
// was drawn (echo.go). It is stamped where the line was drawn rather
// than looked up when the answer lands, because by then the person may
// have typed again: an answer that settled "whatever is marked now"
// would take the mark off a message the engine has not seen.
echo uint64
}
streamEventMsg struct {
gen int
ev session.Event
// then is the event that ENDED a fold and had to travel with it.
//
// [waitEvent] coalesces a run of deltas by taking them off the channel,
// and the only way to find out whether an event folds is to have it in
// hand — a channel cannot be put back. So the one event that stopped a
// run rides beside the run it stopped, and [app.Update] applies the two
// in the order they arrived. It is nil on every message that folded
// nothing, which is every message a test builds and most of the rest.
then *session.Event
// lump says the run this message carries contained a single wire event
// big enough to be one ([isLump]). It is decided HERE, in the only place
// that can see the run's parts, because a fold of short deltas is not a
// lump however long the fold is — the surface paces the wire's lumps and
// never its own fold (reveal.go).
lump bool
}
streamClosedMsg struct{ gen int }
compactedMsg struct {
err error
before, after int
agent Agent
}
// frameMsg is the paint clock: it promotes whatever streamed since the
// last one into a frame, and steps the animations.
frameMsg struct{}
// pointerMsg is the POINTER's clock, and it is a second one on purpose: it
// spends the sweep that piled up while the last one was being answered and
// then stops, where [frameMsg] dirties the frame every time it fires. A
// pointer crossing a row it is already on must cost no frame at all, so the
// thing that wakes it up may not draw one (coalesce.go).
pointerMsg struct{}
// historyPageMsg is one local page of the mirrored transcript returning to
// the update loop. Even a hosted session therefore never waits on ssh in the
// key or wheel path, and a conversation replaced while the command was out
// rejects the old generation rather than prepending somebody else's words.
historyPageMsg struct {
gen int
entries []session.DisplayEntry
from int
to int
earlier bool
seam bool
}
// gitMsg is what the workspace's repository answered (see [gitHead]). It is
// a message rather than a call because `git status` on a large tree is tens
// of milliseconds and the model loop is not a place to wait.
gitMsg struct {
branch string
dirty bool
ok bool
}
// taskEventMsg is one event off the STANDING task subscription (task.go),
// which is a second stream and not the turn's: a node's "done" lands
// minutes after the turn that proposed it, when there is no stream left to
// land on. Its generation is the same device the turn stream's is — a lane
// belonging to an agent that has been replaced must not paint into the one
// that replaced it.
taskEventMsg struct {
gen int
ev session.Event
}
taskLaneClosedMsg struct{ gen int }
// designEventMsg is one event off the STANDING harness-design subscription
// (harness.go), which is a lane of its own for the task lane's reason: a
// design finishes after the turn that asked for it ended, when there is no
// stream left for the card to land on.
designEventMsg struct {
gen int
ev session.Event
}
designLaneClosedMsg struct{ gen int }
// titleEventMsg is one event off the STANDING naming subscription
// (names.go's [app.watchTitles]): the name this conversation gave itself,
// arriving after the turn that bought it had ended. gen is the lane's
// generation, on designEventMsg's own terms.
titleEventMsg struct {
gen int
ev session.Event
}
titleLaneClosedMsg struct{ gen int }
// orchEventMsg is one event off the STANDING adaptive-run subscription
// (the run lane below), which is a lane of its own for the design lane's
// reason: a run's notes, its gauge and its fuel gate all arrive long after
// the turn that started it ended.
orchEventMsg struct {
gen int
ev session.Event
}
orchLaneClosedMsg struct{ gen int }
// questionEventMsg is one event off THE QUESTION LANE (question.go): a
// decision raised, withdrawn, or answered — here or in another window. It
// is a lane of its own for the reason [app.watchQuestions] states: a
// question outlives the turn that raised it, and half the lanes that raise
// one are not in a turn at all.
questionEventMsg struct {
gen int
ev session.Event
}
questionLaneClosedMsg struct{ gen int }
// wokenMsg is one turn THE SESSION STARTED ON ITS OWN, arriving as the
// stream it will speak on (followup.go). It is the turn stream's shape and
// not the standing lane's: what comes off the wake lane is a channel, and
// the events are on it.
wokenMsg struct {
gen int
ch <-chan session.Event
}
wakeLaneClosedMsg struct{ gen int }
// The THIRD lane on this surface (room.go): one node's own events, while a
// person is standing in its room. It is neither the turn's stream nor the
// standing task subscription — those carry what the CONVERSATION is doing and
// what a node's life has come to; this carries what one node is doing right
// now, and it exists only for as long as somebody is watching.
roomEventMsg struct {
gen int
ev session.Event
}
roomClosedMsg struct{ gen int }
// orchPollMsg is a run's page asking to re-read its run (roomorch.go). An
// adaptive run publishes a SNAPSHOT rather than streaming its shape, so the
// page that draws the shape has a clock where the others have a lane — and it
// carries the room's generation for those lanes' own reason: a tick still in
// flight when a page closes must not poll on behalf of the page that
// replaced it.
orchPollMsg struct{ gen int }
// The FOURTH lane, and the only one nobody asked for: one watcher per
// RUNNING node, held for as long as the node runs, so the rail can say what
// a node is doing and for how long without a person having to walk into its
// room to find out (task.go's [taskPilot]). Its generation is per-pilot,
// because pilots come and go with the nodes and the surface holds several.
taskPilotMsg struct {
gen int
id uint64
ev session.Event
}
taskPilotClosedMsg struct {
gen int
id uint64
}
// The two messages one sign-in takes (connectpanel.go). Both are messages
// rather than calls for the reason [gitMsg] is: BeginAuth reaches the
// network and Wait blocks until somebody finishes in a browser, and the
// model loop is not a place to do either.
connectFlowMsg struct {
service string
name string
flow *connect.Flow
err error
}
connectResultMsg struct {
service string
name string
status connect.Status
keyed bool
err error
}
modelConnectResultMsg struct {
setupAttempt *setupProviderAttempt
service string
name string
written string
keyEnv string
outcome modelsource.Outcome
models []Model
err error
// browser says this result owns a waiting browser card. Word is the
// shared terminal-and-panel sentence that settles that card.
browser bool
word string
// renamedFrom is the Written name the row carried when the draft
// opened, and empty unless this connect was an edit that changed it.
// The adopt side re-prefixes every model id already picked under the
// old name, or they strand onto the default service (modelservices.go).
renamedFrom string
}
codexFlowMsg struct {
draft modelConnectDraft
flow CodexFlow
err error
}
// The two messages the default model provider's browser connection takes
// (firstrun.go). The first carries the listener after it is standing, so the
// link can be opened before the second waits for the browser to come back.
openRouterFlowMsg struct {
id uint64
flow OpenRouterFlow
err error
}
openRouterKeyMsg struct {
id uint64
key string
err error
}
// hudFadeMsg is a BOUNDED catch-up tick: the two wakeups a settled turn
// schedules so its fresh numbers can go quiet on time (render.go's fade).
// There is deliberately no idle ticker behind it — a surface with nothing
// happening on it wakes twice and then stops.
hudFadeMsg struct{}
)
type app struct {
taskComputerHere string
taskHerePending tasksKey
taskComputerRows []session.TaskComputer
taskComputers session.TaskComputers
waitForRelease func() bool
chatStarts map[string]bool
conversationRequest uint64
conversationOpening bool
conversationPreview *conversationPreview
openingHomeDraft composerState
homeReading homeReading
homeReadingPending bool
hostReplayLoading bool
hostReplayPending []followingMsg
hostReplayWaiting bool
hostCalls int
hostDeferred []func() tea.Cmd
questionReplacement *questionReplacement
discussionFeeds map[string]*discussionFeed
// ruler measures a string the way the RENDERER will draw it rather than the
// way this package would prefer to read it. The two disagree about a
// variation-selector emoji and a flag, and the rail bent two cells wherever
// one appeared (cellwidth.go).
ruler cellRuler
ctx context.Context
agent Agent
fresh func() (Agent, string, error)
// start and open are the agent-building seam (tui3.go's [Conversation]):
// they hand back the agent AND the closures minted around it, so /new and a
// resume rebind the approval trio, the recent list, the draft and the recall
// list in the same breath as the agent. They are preferred over fresh and
// resume wherever both are wired; the pair below is what a door that cannot
// answer the seam still gets ([app.nextConversation], [app.openConversation]).
start func(workspace string) (Conversation, error)
open func(workspace, transcript string) (Conversation, error)
// engineAnswers is [Options.EngineAnswers]: whether a workspace has an engine
// holding it, asked on the keystroke that opens a held row and nowhere near a
// frame (home.go's [app.engineHolds]).
engineAnswers func(workspace string) bool
// engines is which of the projects with a held row on home turned out to be
// held by an ENGINE rather than by a window, from the last round of asking
// (homeengine.go). It is read on the frame and written on home's beat.
engines map[string]bool
// movedFrom is the conversation another window has just opened out from under
// this one, kept until home's list has the row to point at (takeover.go's
// [app.pointMovedRow]).
movedFrom string
anchorWorkspace func(path string) (string, error)
// shared is [Options.SharedAgent]: this door's fresh and resume seams select
// a conversation IN PLACE on one handle rather than building a second agent.
// It gates the keeper (keeper.go's [app.stow]) and the close that follows a
// swap (welcome.go's [app.openSession], [app.renew]); the option states the
// whole contract and why each of those two would otherwise be wrong.
shared bool
// openTaskOwner attaches a second view onto a conversation that is already
// running, for the length of one task page (tui3.go's [Options.OpenTaskOwner]
// states the whole contract, taskowner.go is the only caller). Nil is a
// window with no engine road, which answers with the card instead.
openTaskOwner func(TaskOwnerAsk) (TaskOwnerView, error)
// elsewhereOf reads the other conversations' presence off this machine's
// disk for a window whose agent cannot (tui3.go's [Options.Elsewhere];
// taskview.go's [app.refreshElsewhere] is the only caller).
elsewhereOf func(transcript string, now time.Time) session.Elsewhere
// taskOwnerGen numbers the attaches this window has asked for and taskOwnerAt
// is the one still in flight. An answer carrying an older number is a view
// nobody wants any more: it is CLOSED on arrival rather than drawn, which is
// what keeps a slow engine from opening a page over whatever the person went
// to instead ([app.tookTaskOwner]).
taskOwnerGen uint64
taskOwnerAt taskOwnerAsk
// workspace is the directory this conversation is about, whole; place is
// its base name, which is what the status line has room for. The whole path
// is what history is keyed by and what the @ completion walks.
workspace string
place string
file string
build string
resumed bool
updateCheck func(context.Context) (codeupdate.Available, bool)
resolveUpdate func(context.Context, codeupdate.Choice) (codeupdate.Release, error)
installUpdate func(context.Context, codeupdate.InstallOptions) (codeupdate.InstallResult, error)
updateRunning string
updateCurl string
updateArgs []string
// updateInFlight is any install running, hand-run or background, and it is
// the guard against a second installer in this window. It stops nothing
// else: an install never owns a turn on this surface.
updateInFlight bool
// updateAuto is the durable half of the automatic updater: the setting, the
// remembered dismissal, the failure count and the cross-terminal lock. Nil
// means this surface has no automatic updater and the launch check is the
// one dim note it always was.
updateAuto *UpdateCoordinator
// offer is the launch offer's state machine (updateoffer.go).
offer updateOffer
// restart is the plan a quitting surface used to fill for the door. Nothing
// on this surface quits for an update any more, so it is read by no code
// here; it stays a field because the door hands one in (tui3.Options).
restart *codeupdate.Plan
// THE OPENROUTER BALANCE (credits.go). readCredits is the door's reader and
// nil on every surface that cannot ask; the rest is what the loop keeps so
// the keys row never reads a file or scans a catalog while it draws.
readCredits func(context.Context) (credits.Reading, error)
paymentRefusals func(func()) func()
creditHookStop func()
creditWake *doorbell
creditPending atomic.Uint32
creditRecordPending atomic.Bool
creditTrigger *credits.Trigger