@@ -13,14 +13,12 @@ import (
1313
1414 "github.com/jack-work/largo"
1515
16- "github.com/jack-work/figaro/internal/compose"
1716 "github.com/jack-work/figaro/internal/config"
1817 "github.com/jack-work/figaro/internal/livedoc"
1918 "github.com/jack-work/figaro/internal/livelog/aria"
2019 "github.com/jack-work/figaro/internal/message"
2120 "github.com/jack-work/figaro/internal/store"
2221 "github.com/jack-work/figaro/internal/term"
23- "github.com/jack-work/figaro/internal/turns"
2422)
2523
2624// runShow handles `figaro show [--id <id>] [N] [-v|-l|-a]`.
@@ -38,6 +36,7 @@ type showOpts struct {
3836 verbose bool
3937 literal bool
4038 details bool // -o: the metadata Ctrl-O shows in the pager
39+ maxBytes int // --max-bytes: paint budget; the OLDEST turns are dropped first. 0 = none
4140}
4241
4342// renderAria prints history for an aria. The default view derives the
@@ -46,65 +45,7 @@ type showOpts struct {
4645// delta compression). N / --last N / --from A [--to B] / --all select a
4746// unit range. --verbose and --literal use the raw IR path.
4847func renderAria (loaded * config.Loaded , id string , args []string ) {
49- opts := showOpts {last : 10 , from : - 1 , to : - 1 , before : - 1 }
50-
51- expanded := make ([]string , 0 , len (args ))
52- for _ , a := range args {
53- if len (a ) > 2 && a [0 ] == '-' && a [1 ] != '-' { // expand bundled bool shorts (-vj)
54- for _ , r := range a [1 :] {
55- expanded = append (expanded , "-" + string (r ))
56- }
57- continue
58- }
59- expanded = append (expanded , a )
60- }
61- needInt := func (i int ) int {
62- if i + 1 >= len (expanded ) {
63- die ("show: %s requires a value" , expanded [i ])
64- }
65- return mustAtoi (expanded [i + 1 ])
66- }
67- for i := 0 ; i < len (expanded ); i ++ {
68- a := expanded [i ]
69- switch {
70- case a == "-o" || a == "--details" :
71- opts .details = true
72- case a == "-v" || a == "--verbose" :
73- opts .verbose = true
74- case a == "-l" || a == "--literal" :
75- opts .literal = true
76- case a == "-a" || a == "--all" :
77- opts .all = true
78- case a == "-j" || a == "--json" :
79- opts .jsonOut = true
80- case a == "--from" :
81- opts .from = needInt (i )
82- i ++
83- case a == "--to" :
84- opts .to = needInt (i )
85- i ++
86- case a == "--last" :
87- opts .last = needInt (i )
88- i ++
89- case strings .HasPrefix (a , "--from=" ):
90- opts .from = mustAtoi (strings .TrimPrefix (a , "--from=" ))
91- case strings .HasPrefix (a , "--to=" ):
92- opts .to = mustAtoi (strings .TrimPrefix (a , "--to=" ))
93- case strings .HasPrefix (a , "--last=" ):
94- opts .last = mustAtoi (strings .TrimPrefix (a , "--last=" ))
95- case a == "--before" :
96- opts .before = needInt (i )
97- i ++
98- case strings .HasPrefix (a , "--before=" ):
99- opts .before = mustAtoi (strings .TrimPrefix (a , "--before=" ))
100- default :
101- n , err := strconv .Atoi (a )
102- if err != nil {
103- die ("usage: figaro show [--id <id>] [N | --last N | --from A [--to B] | -a] [-j|--json] [-o] [-v] [-l]" )
104- }
105- opts .last = n
106- }
107- }
48+ opts := parseShowArgs (args )
10849
10950 ctx , cancel := context .WithTimeout (context .Background (), 30 * time .Second )
11051 defer cancel ()
@@ -123,46 +64,63 @@ func renderAria(loaded *config.Loaded, id string, args []string) {
12364 figaroID = r .FigaroID
12465 }
12566
126- // Read the IR through the angelus's shared LogCache.
67+ // Read the IR through the angelus's shared LogCache, walking BACKWARD from
68+ // the tail until the selector is covered (see show_window.go). One forward
69+ // read from LT 0 is what this used to do, and the angelus caps a single
70+ // read at 1000 entries, so a long aria was shown its own prefix and told
71+ // that was all of it.
12772 //
128- // SEAM (Phase 3): this reads the whole log and pages in memory, because
129- // every selector now speaks turn ids and turns are not derivable from an LT
130- // window: you cannot know which LT a turn starts at without walking. The
131- // paginated turn-aware read (Page/TurnPart, byte budget, bidirectional)
132- // replaces this wholesale; when it lands, `show` becomes a thin client of it
133- // and this block collapses to one call.
134- resp , err := acli .AriaRead (ctx , figaroID , 0 , 0 )
73+ // SEAM (Phase 3): the walk still derives turns client-side, because turns
74+ // are not derivable from an LT window without composing one. The paginated
75+ // turn-aware read (Page/TurnPart, byte budget, bidirectional) replaces the
76+ // walk wholesale; when it lands, `show` becomes a thin client of it.
77+ w , err := gatherShowWindow (ctx , acli , figaroID , opts )
13578 if err != nil {
13679 die ("aria.read: %s" , err )
13780 }
138- if len (resp .Entries ) == 0 && resp .Total == 0 {
81+ // A pre-turn-id aria can only be numbered from its head: see derivedIDs.
82+ if ! w .atHead && derivedIDs (w .entries ) {
83+ resp , rerr := acli .AriaRead (ctx , figaroID , 0 , 0 )
84+ if rerr != nil {
85+ die ("aria.read: %s" , rerr )
86+ }
87+ entries := make ([]store.Entry [message.Message ], len (resp .Entries ))
88+ for i , e := range resp .Entries {
89+ entries [i ].LT = e .LT
90+ if uerr := json .Unmarshal (e .Payload , & entries [i ].Payload ); uerr != nil {
91+ die ("aria.read: parse LT=%d: %s" , e .LT , uerr )
92+ }
93+ }
94+ w = showWindow {entries : entries , total : resp .Total , atHead : len (entries ) >= resp .Total , pages : 1 , fromHead : true }
95+ if ! w .atHead {
96+ showNote ("this aria stores no turn ids (written before they existed), so its turns can only be counted from the head: showing the first %d of %d entries" , len (w .entries ), w .total )
97+ }
98+ }
99+ if len (w .entries ) == 0 && w .total == 0 {
139100 fmt .Fprintln (os .Stderr , "(empty aria)" )
140101 return
141102 }
142- entries := make ([]store.Entry [message.Message ], len (resp .Entries ))
143- for i , e := range resp .Entries {
144- entries [i ].LT = e .LT
145- if err := json .Unmarshal (e .Payload , & entries [i ].Payload ); err != nil {
146- die ("aria.read: parse LT=%d: %s" , e .LT , err )
147- }
148- }
103+ entries := w .entries
149104
150105 // --verbose / --literal: the raw IR path (inline transitions + extras,
151106 // or unrendered IR markdown).
152107 if opts .verbose || opts .literal {
153- renderAriaIR (loaded , figaroID , entries , opts )
108+ renderAriaIR (loaded , figaroID , w , opts )
154109 return
155110 }
156111
157- // Default + --json: conversational units derived from the IR.
158- msgs := make ([]message.Message , len (entries ))
159- for i , e := range entries {
160- msgs [i ] = e .Payload
161- msgs [i ].LogicalTime = e .LT
162- }
163- turns .StampIDs (msgs )
164- turns := compose .Turns (msgs )
112+ // Default + --json: conversational units derived from the IR. A window
113+ // that does not start at the head drops its oldest turn: the page boundary
114+ // fell inside it, and a turn without its prompt renders as an answer to
115+ // nothing.
116+ turns := trimPartialHead (composeTurns (entries ), w .atHead )
165117 lo , hi := selectTurnRange (turns , opts )
118+ if lo < hi && opts .maxBytes > 0 {
119+ if _ , dropped := clipToBudget (turns [lo :hi ], opts .maxBytes ); dropped > 0 {
120+ showNote ("--max-bytes %d dropped the %d oldest turn(s) of this selection; a budget clips the head, never the tail" , opts .maxBytes , dropped )
121+ lo += dropped
122+ }
123+ }
166124
167125 if opts .jsonOut {
168126 // The UI IR wire shape VERBATIM, an aria.Page, exactly what figaro.read
@@ -174,7 +132,9 @@ func renderAria(loaded *config.Loaded, id string, args []string) {
174132 // Whole turns: `show` selects by turn, so no part is ever clipped.
175133 page .Parts = append (page .Parts , aria.TurnPart {Turn : t , From : 0 })
176134 }
177- page .More = aria.More {Before : lo > 0 , After : hi < len (turns )}
135+ // Before is true when anything older exists, whether it was sliced
136+ // off here or simply never walked to.
137+ page .More = aria.More {Before : lo > 0 || ! w .atHead , After : hi < len (turns )}
178138 enc := json .NewEncoder (os .Stdout )
179139 enc .SetIndent ("" , " " )
180140 if err := enc .Encode (page ); err != nil {
@@ -188,10 +148,27 @@ func renderAria(loaded *config.Loaded, id string, args []string) {
188148 // while ids are gapless and 1-based; printing lo+1 here would be a fourth
189149 // coordinate system in the one command that just collapsed three.
190150 if lo >= hi {
191- fmt .Printf ("# aria %s: %d turns (no turn in range)\n \n " , figaroID , len (turns ))
151+ if w .atHead {
152+ fmt .Printf ("# aria %s: %d turns (no turn in range)\n \n " , figaroID , len (turns ))
153+ } else if w .fromHead {
154+ fmt .Printf ("# aria %s: no turn in that range within the first %d turns\n \n " , figaroID , len (turns ))
155+ } else {
156+ // The count is NOT known here: the walk stopped as soon as the
157+ // selector was covered, so len(turns) is the size of the window,
158+ // not of the aria. Printing it as a total restates the old bug.
159+ fmt .Printf ("# aria %s: no turn in that range within the %d turns walked back from the tail\n \n " , figaroID , len (turns ))
160+ }
192161 return
193162 }
194- fmt .Printf ("# aria %s: %d turns (showing %d–%d) · [N] is the turn to fork/send at\n \n " , figaroID , len (turns ), turns [lo ].ID , turns [hi - 1 ].ID )
163+ if w .atHead {
164+ fmt .Printf ("# aria %s: %d turns (showing %d..%d) · [N] is the turn to fork/send at\n \n " , figaroID , len (turns ), turns [lo ].ID , turns [hi - 1 ].ID )
165+ } else if w .fromHead {
166+ // The legacy path: a forward read that stopped at the angelus's cap.
167+ // Say FIRST, because that is what it is.
168+ fmt .Printf ("# aria %s: turns %d..%d, the first %d of %d entries · [N] is the turn to fork/send at\n \n " , figaroID , turns [lo ].ID , turns [hi - 1 ].ID , len (w .entries ), w .total )
169+ } else {
170+ fmt .Printf ("# aria %s: turns %d..%d, walked back from the tail of %d entries · [N] is the turn to fork/send at\n \n " , figaroID , turns [lo ].ID , turns [hi - 1 ].ID , w .total )
171+ }
195172 for i := lo ; i < hi ; i ++ {
196173 u := turns [i ]
197174 fmt .Println (term .Dim (fmt .Sprintf ("[%d]" , u .ID )))
@@ -210,6 +187,80 @@ func renderAria(loaded *config.Loaded, id string, args []string) {
210187 }
211188}
212189
190+ // parseShowArgs turns `show`'s reassembled argv into showOpts.
191+ //
192+ // It is a named function, not a loop inside renderAria, because the flag most
193+ // recently broken here was broken by NOT BEING PARSED: cmdkit consumed
194+ // --max-bytes, the reassembly in cli.go did not pass it on, and `show`
195+ // exited 0 having ignored it. A parser you cannot call is a parser you cannot
196+ // test, and the unit test for the clipper it feeds passed the entire time.
197+ func parseShowArgs (args []string ) showOpts {
198+ opts := showOpts {last : 10 , from : - 1 , to : - 1 , before : - 1 }
199+
200+ expanded := make ([]string , 0 , len (args ))
201+ for _ , a := range args {
202+ if len (a ) > 2 && a [0 ] == '-' && a [1 ] != '-' { // expand bundled bool shorts (-vj)
203+ for _ , r := range a [1 :] {
204+ expanded = append (expanded , "-" + string (r ))
205+ }
206+ continue
207+ }
208+ expanded = append (expanded , a )
209+ }
210+ needInt := func (i int ) int {
211+ if i + 1 >= len (expanded ) {
212+ die ("show: %s requires a value" , expanded [i ])
213+ }
214+ return mustAtoi (expanded [i + 1 ])
215+ }
216+ for i := 0 ; i < len (expanded ); i ++ {
217+ a := expanded [i ]
218+ switch {
219+ case a == "-o" || a == "--details" :
220+ opts .details = true
221+ case a == "-v" || a == "--verbose" :
222+ opts .verbose = true
223+ case a == "-l" || a == "--literal" :
224+ opts .literal = true
225+ case a == "-a" || a == "--all" :
226+ opts .all = true
227+ case a == "-j" || a == "--json" :
228+ opts .jsonOut = true
229+ case a == "--from" :
230+ opts .from = needInt (i )
231+ i ++
232+ case a == "--to" :
233+ opts .to = needInt (i )
234+ i ++
235+ case a == "--max-bytes" :
236+ opts .maxBytes = needInt (i )
237+ i ++
238+ case a == "--last" :
239+ opts .last = needInt (i )
240+ i ++
241+ case strings .HasPrefix (a , "--from=" ):
242+ opts .from = mustAtoi (strings .TrimPrefix (a , "--from=" ))
243+ case strings .HasPrefix (a , "--to=" ):
244+ opts .to = mustAtoi (strings .TrimPrefix (a , "--to=" ))
245+ case strings .HasPrefix (a , "--last=" ):
246+ opts .last = mustAtoi (strings .TrimPrefix (a , "--last=" ))
247+ case a == "--before" :
248+ opts .before = needInt (i )
249+ i ++
250+ case strings .HasPrefix (a , "--before=" ):
251+ opts .before = mustAtoi (strings .TrimPrefix (a , "--before=" ))
252+ default :
253+ n , err := strconv .Atoi (a )
254+ if err != nil {
255+ die ("usage: figaro show [--id <id>] [N | --last N | --from A [--to B] | -a] [-j|--json] [-o] [-v] [-l]" )
256+ }
257+ opts .last = n
258+ }
259+ }
260+
261+ return opts
262+ }
263+
213264func mustAtoi (s string ) int {
214265 v , err := strconv .Atoi (s )
215266 if err != nil {
@@ -280,7 +331,8 @@ func selectTurnRange(turns []aria.Turn, o showOpts) (int, int) {
280331// renderAriaIR is the raw IR path for --verbose / --literal: it renders
281332// each message (markdown via largo, or unrendered when --literal) and, in
282333// verbose mode, appends credo / state transitions / form.
283- func renderAriaIR (loaded * config.Loaded , figaroID string , entries []store.Entry [message.Message ], opts showOpts ) {
334+ func renderAriaIR (loaded * config.Loaded , figaroID string , win showWindow , opts showOpts ) {
335+ entries := win .entries
284336 start := 0
285337 if ! opts .all && len (entries ) > opts .last {
286338 start = len (entries ) - opts .last
@@ -297,7 +349,10 @@ func renderAriaIR(loaded *config.Loaded, figaroID string, entries []store.Entry[
297349 flush = func () { sw .Flush () }
298350 }
299351
300- fmt .Fprintf (w , "# aria %s: showing %d of %d messages\n \n " , figaroID , len (entries )- start , len (entries ))
352+ // The denominator is the ARIA's message count, not the window's. A tail
353+ // window that reported its own size as the total is exactly how `show`
354+ // used to claim a 4000 message aria had 1000 messages in it.
355+ fmt .Fprintf (w , "# aria %s: showing %d of %d messages\n \n " , figaroID , len (entries )- start , win .total )
301356 for _ , e := range entries [start :] {
302357 renderMessage (w , e .Payload , e .LT , opts .verbose )
303358 }
0 commit comments