opencode-tui-stats 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/tracker.ts +172 -16
- package/tui.tsx +110 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "opencode-tui-stats",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"description": "OpenCode 1 TUI plugin that shows live TPS, average TPS, and average time to first token in the session prompt.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"exports": {
|
package/tracker.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { TuiPluginApi } from "@opencode-ai/plugin/tui"
|
|
|
2
2
|
|
|
3
3
|
export type StreamSample = {
|
|
4
4
|
at: number
|
|
5
|
+
messageID: string
|
|
5
6
|
tokens: number
|
|
6
7
|
}
|
|
7
8
|
|
|
@@ -28,12 +29,37 @@ export type SessionStats = {
|
|
|
28
29
|
lastAttempt: number
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Per-model aggregate within a session, keyed by `providerID/modelID`.
|
|
33
|
+
* `cacheRead`/`cachePrompt`/`cost`/`steps` mirror {@link SessionStats}; the `total*`/`stepCount`
|
|
34
|
+
* fields mirror {@link SessionAverage} so the existing rate/TTFT helpers can be reused as-is. */
|
|
35
|
+
export type ModelAggregate = {
|
|
36
|
+
cacheRead: number
|
|
37
|
+
cachePrompt: number
|
|
38
|
+
cost: number
|
|
39
|
+
steps: number
|
|
40
|
+
totalTokens: number
|
|
41
|
+
totalDurationMs: number
|
|
42
|
+
totalTtftMs: number
|
|
43
|
+
stepCount: number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SessionModels = {
|
|
47
|
+
/** Most recently observed assistant `providerID/modelID` for the session. */
|
|
48
|
+
active?: string
|
|
49
|
+
/** Model keys in observation order (oldest first, most recent last). */
|
|
50
|
+
order: string[]
|
|
51
|
+
aggregates: Record<string, ModelAggregate>
|
|
52
|
+
/** `messageID -> providerID/modelID`, used to attribute live samples to a model. */
|
|
53
|
+
messageModels: Map<string, string>
|
|
54
|
+
}
|
|
55
|
+
|
|
31
56
|
export type Tracker = {
|
|
32
57
|
samples: Record<string, StreamSample[]>
|
|
33
58
|
timings: Record<string, MessageTiming>
|
|
34
59
|
averages: Record<string, SessionAverage>
|
|
35
60
|
stats: Record<string, SessionStats>
|
|
36
61
|
dedup: Record<string, Set<string>>
|
|
62
|
+
models: Record<string, SessionModels>
|
|
37
63
|
}
|
|
38
64
|
|
|
39
65
|
const STREAM_WINDOW_MS = 5_000
|
|
@@ -43,12 +69,33 @@ const PART_CACHE_LIMIT = 1_024
|
|
|
43
69
|
/** Per-session dedup window: only the most recent completed messages are deduplicated;
|
|
44
70
|
* a repeated completed event beyond this window accumulates again. */
|
|
45
71
|
const DEDUP_WINDOW_LIMIT = 64
|
|
72
|
+
/** Cap on `messageID -> model` entries retained per session (FIFO). */
|
|
73
|
+
const MESSAGE_MODEL_LIMIT = 256
|
|
46
74
|
const SESSION_LIMIT = 64
|
|
75
|
+
/** Maximum number of model rows rendered in the sidebar panel. */
|
|
76
|
+
export const MODEL_ROW_LIMIT = 4
|
|
47
77
|
|
|
48
78
|
function emptyStats(): SessionStats {
|
|
49
79
|
return { cacheRead: 0, cachePrompt: 0, cost: 0, steps: 0, retries: 0, lastAttempt: 0 }
|
|
50
80
|
}
|
|
51
81
|
|
|
82
|
+
function emptyAggregate(): ModelAggregate {
|
|
83
|
+
return {
|
|
84
|
+
cacheRead: 0,
|
|
85
|
+
cachePrompt: 0,
|
|
86
|
+
cost: 0,
|
|
87
|
+
steps: 0,
|
|
88
|
+
totalTokens: 0,
|
|
89
|
+
totalDurationMs: 0,
|
|
90
|
+
totalTtftMs: 0,
|
|
91
|
+
stepCount: 0,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function modelKey(providerID: string, modelID: string) {
|
|
96
|
+
return `${providerID}/${modelID}`
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
export function estimateTokens(delta: string) {
|
|
53
100
|
return Math.max(1, Math.ceil(Buffer.byteLength(delta, "utf8") / 5))
|
|
54
101
|
}
|
|
@@ -90,13 +137,14 @@ export function averageTtft(average: SessionAverage | undefined) {
|
|
|
90
137
|
return formatTtft(average.totalTtftMs / average.stepCount / 1_000)
|
|
91
138
|
}
|
|
92
139
|
|
|
93
|
-
|
|
94
|
-
|
|
140
|
+
/** Cache hit percentage (e.g. `31%`) or `undefined` when nothing promptable has been seen. */
|
|
141
|
+
export function cacheText(stats: { cacheRead: number; cachePrompt: number } | undefined) {
|
|
142
|
+
if (!stats || stats.cachePrompt <= 0) return undefined
|
|
95
143
|
return `${Math.round((stats.cacheRead / stats.cachePrompt) * 100)}%`
|
|
96
144
|
}
|
|
97
145
|
|
|
98
|
-
export function costText(stats:
|
|
99
|
-
if (!stats || stats.steps <= 0) return
|
|
146
|
+
export function costText(stats: { cost: number; steps: number } | undefined) {
|
|
147
|
+
if (!stats || stats.steps <= 0) return undefined
|
|
100
148
|
return `$${stats.cost >= 1 ? stats.cost.toFixed(2) : stats.cost.toFixed(4)}`
|
|
101
149
|
}
|
|
102
150
|
|
|
@@ -120,6 +168,25 @@ function boundedSet<K, V>(map: Map<K, V>, key: K, value: V, limit: number) {
|
|
|
120
168
|
map.set(key, value)
|
|
121
169
|
}
|
|
122
170
|
|
|
171
|
+
export type ModelRow = {
|
|
172
|
+
key: string
|
|
173
|
+
label: string
|
|
174
|
+
active: boolean
|
|
175
|
+
tps?: string
|
|
176
|
+
avg?: string
|
|
177
|
+
ttft?: string
|
|
178
|
+
cache?: string
|
|
179
|
+
cost?: string
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export type ModelPanel = {
|
|
183
|
+
rows: ModelRow[]
|
|
184
|
+
overflow: number
|
|
185
|
+
retries: number
|
|
186
|
+
/** Session-level cache hit percentage (e.g. `31%`), or `undefined` when nothing promptable was seen. */
|
|
187
|
+
cache?: string
|
|
188
|
+
}
|
|
189
|
+
|
|
123
190
|
export type TrackerInternals = {
|
|
124
191
|
sizes(): {
|
|
125
192
|
parts: number
|
|
@@ -134,10 +201,11 @@ export type TrackerInternals = {
|
|
|
134
201
|
dedupSize(sessionID: string): number
|
|
135
202
|
retries(sessionID: string): number
|
|
136
203
|
mounted(): string[]
|
|
204
|
+
activeModel(sessionID: string): string | undefined
|
|
137
205
|
}
|
|
138
206
|
|
|
139
207
|
export type TrackerHandle = {
|
|
140
|
-
|
|
208
|
+
modelRows(sessionID: string): ModelPanel
|
|
141
209
|
mountSession(sessionID: string): void
|
|
142
210
|
unmountSession(sessionID: string): void
|
|
143
211
|
prune(now?: number): void
|
|
@@ -153,7 +221,7 @@ export function getLastInternals(): TrackerInternals | undefined {
|
|
|
153
221
|
}
|
|
154
222
|
|
|
155
223
|
export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerHandle {
|
|
156
|
-
const tracker: Tracker = { samples: {}, timings: {}, averages: {}, stats: {}, dedup: {} }
|
|
224
|
+
const tracker: Tracker = { samples: {}, timings: {}, averages: {}, stats: {}, dedup: {}, models: {} }
|
|
157
225
|
const partDecisions = new Map<string, boolean>()
|
|
158
226
|
const mounted = new Set<string>()
|
|
159
227
|
const order: string[] = []
|
|
@@ -182,6 +250,7 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
182
250
|
delete tracker.averages[sessionID]
|
|
183
251
|
delete tracker.samples[sessionID]
|
|
184
252
|
delete tracker.dedup[sessionID]
|
|
253
|
+
delete tracker.models[sessionID]
|
|
185
254
|
clearTimings(sessionID)
|
|
186
255
|
}
|
|
187
256
|
|
|
@@ -203,6 +272,13 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
203
272
|
}
|
|
204
273
|
}
|
|
205
274
|
|
|
275
|
+
const touchModel = (models: SessionModels, key: string) => {
|
|
276
|
+
const at = models.order.indexOf(key)
|
|
277
|
+
if (at !== -1) models.order.splice(at, 1)
|
|
278
|
+
models.order.push(key)
|
|
279
|
+
if (!models.aggregates[key]) models.aggregates[key] = emptyAggregate()
|
|
280
|
+
}
|
|
281
|
+
|
|
206
282
|
const prune = (now = Date.now()) => {
|
|
207
283
|
let changed = false
|
|
208
284
|
|
|
@@ -228,12 +304,16 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
228
304
|
bump()
|
|
229
305
|
}
|
|
230
306
|
|
|
231
|
-
|
|
307
|
+
/** Live rate restricted to samples attributed to `key` via the `messageID -> model` mapping. */
|
|
308
|
+
const liveRateFor = (sessionID: string, key: string) => {
|
|
232
309
|
const status = api.state.session.status(sessionID)
|
|
233
310
|
if (!status || status.type === "idle") return undefined
|
|
234
311
|
|
|
235
312
|
const now = Date.now()
|
|
236
|
-
const
|
|
313
|
+
const mapping = tracker.models[sessionID]?.messageModels
|
|
314
|
+
const samples = (tracker.samples[sessionID] ?? []).filter(
|
|
315
|
+
(sample) => now - sample.at <= STREAM_WINDOW_MS && mapping?.get(sample.messageID) === key,
|
|
316
|
+
)
|
|
237
317
|
const last = samples[samples.length - 1]
|
|
238
318
|
if (!last || now - last.at > LIVE_STALE_MS) return undefined
|
|
239
319
|
|
|
@@ -241,13 +321,38 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
241
321
|
return formatRate(tokens / (activeDuration(samples, now) / 1_000))
|
|
242
322
|
}
|
|
243
323
|
|
|
244
|
-
const
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
324
|
+
const modelRows = (sessionID: string): ModelPanel => {
|
|
325
|
+
const models = tracker.models[sessionID]
|
|
326
|
+
const retries = tracker.stats[sessionID]?.retries ?? 0
|
|
327
|
+
const cache = cacheText(tracker.stats[sessionID])
|
|
328
|
+
if (!models) return { rows: [], overflow: 0, retries, cache }
|
|
329
|
+
|
|
330
|
+
const active = models.active
|
|
331
|
+
// `order` is oldest→newest; reverse for most-recent-first ordering of the non-active rows.
|
|
332
|
+
const recency = [...models.order].reverse().filter((key) => key in models.aggregates)
|
|
333
|
+
const ordered =
|
|
334
|
+
active && active in models.aggregates ? [active, ...recency.filter((key) => key !== active)] : recency
|
|
335
|
+
|
|
336
|
+
const visible = ordered.slice(0, MODEL_ROW_LIMIT)
|
|
337
|
+
const overflow = Math.max(0, ordered.length - visible.length)
|
|
338
|
+
|
|
339
|
+
const rows = visible.map<ModelRow>((key) => {
|
|
340
|
+
const aggregate = models.aggregates[key]
|
|
341
|
+
const isActive = key === active
|
|
342
|
+
const cost = costText(aggregate)
|
|
343
|
+
return {
|
|
344
|
+
key,
|
|
345
|
+
label: key,
|
|
346
|
+
active: isActive,
|
|
347
|
+
tps: isActive ? liveRateFor(sessionID, key) : undefined,
|
|
348
|
+
avg: averageRate(aggregate),
|
|
349
|
+
ttft: averageTtft(aggregate),
|
|
350
|
+
cache: cacheText(aggregate),
|
|
351
|
+
cost: cost === undefined ? undefined : cost,
|
|
352
|
+
}
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
return { rows, overflow, retries, cache }
|
|
251
356
|
}
|
|
252
357
|
|
|
253
358
|
listeners.push(
|
|
@@ -269,6 +374,7 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
269
374
|
|
|
270
375
|
appendSample(sessionID, messageID, {
|
|
271
376
|
at: Date.now(),
|
|
377
|
+
messageID,
|
|
272
378
|
tokens: estimateTokens(event.properties.delta),
|
|
273
379
|
})
|
|
274
380
|
}),
|
|
@@ -279,6 +385,15 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
279
385
|
const info = event.properties.info
|
|
280
386
|
if (info.role !== "assistant") return
|
|
281
387
|
const sessionID = event.properties.sessionID
|
|
388
|
+
const key = modelKey(info.providerID, info.modelID)
|
|
389
|
+
|
|
390
|
+
// Observe the model as soon as a message appears so the sidebar can attribute live samples
|
|
391
|
+
// and mark the most recently used model as active.
|
|
392
|
+
const models =
|
|
393
|
+
tracker.models[sessionID] ?? (tracker.models[sessionID] = { order: [], aggregates: {}, messageModels: new Map() })
|
|
394
|
+
touchModel(models, key)
|
|
395
|
+
models.active = key
|
|
396
|
+
boundedSet(models.messageModels, info.id, key, MESSAGE_MODEL_LIMIT)
|
|
282
397
|
|
|
283
398
|
if (!info.time.completed) {
|
|
284
399
|
const existing = tracker.timings[info.id]
|
|
@@ -308,6 +423,14 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
308
423
|
cost: stats.cost + info.cost,
|
|
309
424
|
steps: stats.steps + 1,
|
|
310
425
|
}
|
|
426
|
+
const aggregate = models.aggregates[key] ?? emptyAggregate()
|
|
427
|
+
models.aggregates[key] = {
|
|
428
|
+
...aggregate,
|
|
429
|
+
cacheRead: aggregate.cacheRead + cacheRead,
|
|
430
|
+
cachePrompt: aggregate.cachePrompt + info.tokens.input + cacheRead + cacheWrite,
|
|
431
|
+
cost: aggregate.cost + info.cost,
|
|
432
|
+
steps: aggregate.steps + 1,
|
|
433
|
+
}
|
|
311
434
|
}
|
|
312
435
|
|
|
313
436
|
const timing = tracker.timings[info.id]
|
|
@@ -328,6 +451,14 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
328
451
|
totalTtftMs: totals.totalTtftMs + Math.max(timing.firstResponseAt - timing.requestStartAt, 0),
|
|
329
452
|
stepCount: totals.stepCount + 1,
|
|
330
453
|
}
|
|
454
|
+
const aggregate = models.aggregates[key] ?? emptyAggregate()
|
|
455
|
+
models.aggregates[key] = {
|
|
456
|
+
...aggregate,
|
|
457
|
+
totalTokens: aggregate.totalTokens + tokens,
|
|
458
|
+
totalDurationMs: aggregate.totalDurationMs + durationMs,
|
|
459
|
+
totalTtftMs: aggregate.totalTtftMs + Math.max(timing.firstResponseAt - timing.requestStartAt, 0),
|
|
460
|
+
stepCount: aggregate.stepCount + 1,
|
|
461
|
+
}
|
|
331
462
|
}
|
|
332
463
|
}
|
|
333
464
|
delete tracker.timings[info.id]
|
|
@@ -396,6 +527,7 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
396
527
|
}),
|
|
397
528
|
)
|
|
398
529
|
|
|
530
|
+
// The 1s tick re-evaluates live TPS staleness and prunes stale stream samples.
|
|
399
531
|
const timer = setInterval(() => {
|
|
400
532
|
prune()
|
|
401
533
|
bump()
|
|
@@ -415,11 +547,12 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
415
547
|
dedupSize: (sessionID) => tracker.dedup[sessionID]?.size ?? 0,
|
|
416
548
|
retries: (sessionID) => tracker.stats[sessionID]?.retries ?? 0,
|
|
417
549
|
mounted: () => [...mounted],
|
|
550
|
+
activeModel: (sessionID) => tracker.models[sessionID]?.active,
|
|
418
551
|
}
|
|
419
552
|
lastInternals = internals
|
|
420
553
|
|
|
421
554
|
return {
|
|
422
|
-
|
|
555
|
+
modelRows,
|
|
423
556
|
mountSession(sessionID) {
|
|
424
557
|
mounted.add(sessionID)
|
|
425
558
|
},
|
|
@@ -437,3 +570,26 @@ export function createTracker(api: TuiPluginApi, onChange: () => void): TrackerH
|
|
|
437
570
|
},
|
|
438
571
|
}
|
|
439
572
|
}
|
|
573
|
+
|
|
574
|
+
/** Test-only fold controller. Kept in this non-entry module: the plugin entry (`tui.tsx`)
|
|
575
|
+
* may only expose a `default` export, because the legacy loader treats every runtime export
|
|
576
|
+
* as a plugin. */
|
|
577
|
+
export type PanelController = {
|
|
578
|
+
toggle(key: string): void
|
|
579
|
+
isExpanded(key: string): boolean
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
let lastPanelController: PanelController | undefined
|
|
583
|
+
|
|
584
|
+
export function setLastPanelController(controller: PanelController) {
|
|
585
|
+
lastPanelController = controller
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Clear the controller only when it is still the current one. */
|
|
589
|
+
export function clearLastPanelController(controller: PanelController) {
|
|
590
|
+
if (lastPanelController === controller) lastPanelController = undefined
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function getLastPanelController(): PanelController | undefined {
|
|
594
|
+
return lastPanelController
|
|
595
|
+
}
|
package/tui.tsx
CHANGED
|
@@ -1,17 +1,68 @@
|
|
|
1
1
|
/** @jsxImportSource @opentui/solid */
|
|
2
2
|
import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
3
|
-
import { createMemo, createSignal, onCleanup, type Accessor } from "solid-js"
|
|
4
|
-
import {
|
|
3
|
+
import { For, Show, createEffect, createMemo, createSignal, onCleanup, type Accessor } from "solid-js"
|
|
4
|
+
import {
|
|
5
|
+
clearLastPanelController,
|
|
6
|
+
createTracker,
|
|
7
|
+
setLastPanelController,
|
|
8
|
+
type ModelRow,
|
|
9
|
+
type TrackerHandle,
|
|
10
|
+
} from "./tracker"
|
|
5
11
|
|
|
6
|
-
|
|
12
|
+
/** A single model row: fold marker + summary line, plus a detail line when expanded. */
|
|
13
|
+
function ModelRowView(props: { api: TuiPluginApi; row: ModelRow; expanded: boolean; onToggle: () => void }) {
|
|
14
|
+
// Guard against a drag-select being read as a click: only toggle when the press and the
|
|
15
|
+
// release land on the same cell.
|
|
16
|
+
let pressed: { x: number; y: number } | undefined
|
|
17
|
+
|
|
18
|
+
const handleMouseDown = (event: { x: number; y: number }) => {
|
|
19
|
+
pressed = { x: event.x, y: event.y }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const handleMouseUp = (event: { x: number; y: number }) => {
|
|
23
|
+
const start = pressed
|
|
24
|
+
pressed = undefined
|
|
25
|
+
if (start && (start.x !== event.x || start.y !== event.y)) return
|
|
26
|
+
props.onToggle()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const theme = () => props.api.theme.current
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<box flexDirection="column" onMouseDown={handleMouseDown} onMouseUp={handleMouseUp}>
|
|
33
|
+
<text wrapMode="none" truncate>
|
|
34
|
+
<span style={{ fg: theme().textMuted } as Record<string, unknown>}>{`${props.expanded ? "▼" : "▶"} `}</span>
|
|
35
|
+
<span style={{ fg: theme().text } as Record<string, unknown>}>{props.row.label}</span>
|
|
36
|
+
<Show when={props.row.active}>
|
|
37
|
+
<span style={{ fg: theme().success } as Record<string, unknown>}>{" active"}</span>
|
|
38
|
+
</Show>
|
|
39
|
+
</text>
|
|
40
|
+
<Show when={props.expanded}>
|
|
41
|
+
<text fg={theme().text} wrapMode="none" truncate>
|
|
42
|
+
{`TPS ${props.row.tps ?? "—"} · AVG ${props.row.avg ?? "—"}`}
|
|
43
|
+
</text>
|
|
44
|
+
<text fg={theme().text} wrapMode="none" truncate>
|
|
45
|
+
{`TTFT ${props.row.ttft ?? "—"} · CACHE ${props.row.cache ?? "—"} · ${props.row.cost ?? "—"}`}
|
|
46
|
+
</text>
|
|
47
|
+
</Show>
|
|
48
|
+
</box>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Per-session model panel, mounted at `sidebar_content`. */
|
|
53
|
+
function ModelPanelView(props: {
|
|
7
54
|
api: TuiPluginApi
|
|
8
55
|
sessionID: string
|
|
9
56
|
tracker: TrackerHandle
|
|
10
57
|
revision: Accessor<number>
|
|
11
58
|
}) {
|
|
12
59
|
let mounted: string | undefined
|
|
60
|
+
const initialActive = props.tracker.internals.activeModel(props.sessionID)
|
|
61
|
+
const [expanded, setExpanded] = createSignal<Set<string>>(new Set(initialActive ? [initialActive] : []))
|
|
62
|
+
let seededActive: string | undefined = initialActive
|
|
63
|
+
let lastSession: string | undefined = props.sessionID
|
|
13
64
|
|
|
14
|
-
const
|
|
65
|
+
const panel = createMemo(() => {
|
|
15
66
|
props.revision()
|
|
16
67
|
const sessionID = props.sessionID
|
|
17
68
|
if (mounted !== sessionID) {
|
|
@@ -19,14 +70,65 @@ function SessionPromptRight(props: {
|
|
|
19
70
|
props.tracker.mountSession(sessionID)
|
|
20
71
|
mounted = sessionID
|
|
21
72
|
}
|
|
22
|
-
return props.tracker.
|
|
73
|
+
return props.tracker.modelRows(sessionID)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
// Fold state is in-memory only and resets per session. The active model starts expanded and
|
|
77
|
+
// every other row starts collapsed; when the active model changes we return to that default
|
|
78
|
+
// so the panel cannot accumulate open rows as models are switched.
|
|
79
|
+
createEffect(() => {
|
|
80
|
+
props.revision()
|
|
81
|
+
const sessionID = props.sessionID
|
|
82
|
+
const active = props.tracker.internals.activeModel(sessionID)
|
|
83
|
+
if (sessionID !== lastSession) {
|
|
84
|
+
lastSession = sessionID
|
|
85
|
+
seededActive = active
|
|
86
|
+
setExpanded(new Set<string>(active ? [active] : []))
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
if (active && active !== seededActive) {
|
|
90
|
+
seededActive = active
|
|
91
|
+
setExpanded(new Set<string>([active]))
|
|
92
|
+
}
|
|
23
93
|
})
|
|
24
94
|
|
|
95
|
+
const toggle = (key: string) => {
|
|
96
|
+
setExpanded((prev) => {
|
|
97
|
+
const next = new Set(prev)
|
|
98
|
+
if (next.has(key)) next.delete(key)
|
|
99
|
+
else next.add(key)
|
|
100
|
+
return next
|
|
101
|
+
})
|
|
102
|
+
props.api.renderer?.requestRender?.()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const controller = { toggle, isExpanded: (key: string) => expanded().has(key) }
|
|
106
|
+
setLastPanelController(controller)
|
|
107
|
+
|
|
25
108
|
onCleanup(() => {
|
|
26
109
|
if (mounted !== undefined) props.tracker.unmountSession(mounted)
|
|
110
|
+
clearLastPanelController(controller)
|
|
27
111
|
})
|
|
28
112
|
|
|
29
|
-
return
|
|
113
|
+
return (
|
|
114
|
+
<box flexDirection="column">
|
|
115
|
+
<text fg={props.api.theme.current.textMuted}>Performance · observed</text>
|
|
116
|
+
<Show when={panel().rows.length === 0}>
|
|
117
|
+
<text fg={props.api.theme.current.textMuted}>—</text>
|
|
118
|
+
</Show>
|
|
119
|
+
<For each={panel().rows}>
|
|
120
|
+
{(row) => (
|
|
121
|
+
<ModelRowView api={props.api} row={row} expanded={expanded().has(row.key)} onToggle={() => toggle(row.key)} />
|
|
122
|
+
)}
|
|
123
|
+
</For>
|
|
124
|
+
<Show when={panel().overflow > 0}>
|
|
125
|
+
<text fg={props.api.theme.current.textMuted}>{`+${panel().overflow} more models`}</text>
|
|
126
|
+
</Show>
|
|
127
|
+
<text fg={props.api.theme.current.text}>
|
|
128
|
+
{`Session CACHE ${panel().cache ?? "—"}${panel().retries > 0 ? ` · retries ${panel().retries}` : ""}`}
|
|
129
|
+
</text>
|
|
130
|
+
</box>
|
|
131
|
+
)
|
|
30
132
|
}
|
|
31
133
|
|
|
32
134
|
const tui: TuiPlugin = async (api) => {
|
|
@@ -37,8 +139,8 @@ const tui: TuiPlugin = async (api) => {
|
|
|
37
139
|
|
|
38
140
|
api.slots.register({
|
|
39
141
|
slots: {
|
|
40
|
-
|
|
41
|
-
return <
|
|
142
|
+
sidebar_content(_context, props) {
|
|
143
|
+
return <ModelPanelView api={api} sessionID={props.session_id} tracker={tracker} revision={revision} />
|
|
42
144
|
},
|
|
43
145
|
},
|
|
44
146
|
})
|