@leo000001/opencode-quota-sidebar 4.0.16 → 4.1.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/dist/tui.d.ts +1 -1
- package/dist/tui.tsx +476 -481
- package/dist/tui_helpers.d.ts +4 -4
- package/dist/tui_helpers.js +36 -36
- package/package.json +1 -1
package/dist/tui.d.ts
CHANGED
package/dist/tui.tsx
CHANGED
|
@@ -1,481 +1,476 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
import type {
|
|
3
|
-
TuiPlugin,
|
|
4
|
-
TuiPluginApi,
|
|
5
|
-
TuiPluginModule,
|
|
6
|
-
} from
|
|
7
|
-
import { createMemo, createSignal, For, onCleanup, Show } from
|
|
8
|
-
|
|
9
|
-
import { fitLine, renderSidebarUsageLines } from
|
|
10
|
-
import {
|
|
11
|
-
fallbackQuotaGroupsFromTitle,
|
|
12
|
-
mergeLiveAndPersistedPanelUsage,
|
|
13
|
-
quotaGroupsAreCollapsible,
|
|
14
|
-
quotaGroupsSummary,
|
|
15
|
-
quotaGroupsUseBullets,
|
|
16
|
-
renderSidebarQuotaGroups,
|
|
17
|
-
sidebarPanelQuotaSnapshots,
|
|
18
|
-
type SidebarQuotaGroup,
|
|
19
|
-
} from
|
|
20
|
-
import {
|
|
21
|
-
loadConfig,
|
|
22
|
-
loadState,
|
|
23
|
-
quotaConfigPaths,
|
|
24
|
-
resolveOpencodeDataDir,
|
|
25
|
-
stateFilePath,
|
|
26
|
-
} from
|
|
27
|
-
import { looksDecorated, normalizeBaseTitle } from
|
|
28
|
-
import type { QuotaSidebarConfig } from
|
|
29
|
-
import {
|
|
30
|
-
fromCachedSessionUsage,
|
|
31
|
-
mergeUsage,
|
|
32
|
-
summarizeMessages,
|
|
33
|
-
} from
|
|
34
|
-
|
|
35
|
-
const id =
|
|
36
|
-
const INTERNAL_CONTEXT_PLUGIN_ID =
|
|
37
|
-
const SECTION_INDENT = 2
|
|
38
|
-
const DEFAULT_WIDTH = 36
|
|
39
|
-
|
|
40
|
-
type SidebarPanelData = {
|
|
41
|
-
enabled: boolean
|
|
42
|
-
width: number
|
|
43
|
-
usageLines: string[]
|
|
44
|
-
quotaGroups: SidebarQuotaGroup[]
|
|
45
|
-
compactTitle?: string
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const latestCompactTitles = new Map<string, string>()
|
|
49
|
-
const [compactTitleVersion, setCompactTitleVersion] = createSignal(0)
|
|
50
|
-
|
|
51
|
-
function directoryPath(api: TuiPluginApi) {
|
|
52
|
-
return api.state.path.directory || process.cwd()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function worktreePath(api: TuiPluginApi) {
|
|
56
|
-
return api.state.path.worktree || directoryPath(api)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function panelConfig(config: QuotaSidebarConfig): QuotaSidebarConfig {
|
|
60
|
-
return {
|
|
61
|
-
...config,
|
|
62
|
-
sidebar: {
|
|
63
|
-
...config.sidebar,
|
|
64
|
-
width: Math.max(8, config.sidebar.width - SECTION_INDENT),
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function resolveCompactTitle(sessionID: string, persistedTitle?: string) {
|
|
70
|
-
const liveTitle = latestCompactTitles.get(sessionID)
|
|
71
|
-
if (liveTitle && looksDecorated(liveTitle)) return liveTitle
|
|
72
|
-
if (persistedTitle && looksDecorated(persistedTitle)) return persistedTitle
|
|
73
|
-
return liveTitle || persistedTitle
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function loadSidebarPanel(
|
|
77
|
-
api: TuiPluginApi,
|
|
78
|
-
sessionID: string,
|
|
79
|
-
): Promise<SidebarPanelData> {
|
|
80
|
-
const statePath = stateFilePath(resolveOpencodeDataDir())
|
|
81
|
-
const config = await loadConfig(
|
|
82
|
-
quotaConfigPaths(worktreePath(api), directoryPath(api)),
|
|
83
|
-
)
|
|
84
|
-
// Session payload lives in day chunks that the server updates from a
|
|
85
|
-
// separate process, so TUI should re-read persisted state instead of keeping
|
|
86
|
-
// an extra full-state cache here.
|
|
87
|
-
const state = await loadState(statePath)
|
|
88
|
-
const session = state.sessions[sessionID]
|
|
89
|
-
const enabled = config.sidebar.enabled
|
|
90
|
-
const width = Math.max(8, config.sidebar.width - SECTION_INDENT)
|
|
91
|
-
const liveEntries = api.state.session.messages(sessionID).map((info) => ({
|
|
92
|
-
info,
|
|
93
|
-
})) as Parameters<typeof summarizeMessages>[0]
|
|
94
|
-
|
|
95
|
-
const liveUsage = summarizeMessages(liveEntries, 0, 1)
|
|
96
|
-
const cachedUsage = session?.sidebarPanel?.usage || session?.usage
|
|
97
|
-
const persistedUsage = cachedUsage
|
|
98
|
-
? fromCachedSessionUsage(cachedUsage)
|
|
99
|
-
: undefined
|
|
100
|
-
const usage = mergeLiveAndPersistedPanelUsage(
|
|
101
|
-
liveUsage.assistantMessages > 0 ? liveUsage : undefined,
|
|
102
|
-
persistedUsage,
|
|
103
|
-
)
|
|
104
|
-
const compactTitle = resolveCompactTitle(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
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
|
-
group
|
|
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
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
return
|
|
326
|
-
})
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
const
|
|
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
|
-
api
|
|
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
|
-
id,
|
|
478
|
-
tui,
|
|
479
|
-
};
|
|
480
|
-
|
|
481
|
-
export default plugin;
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
import type {
|
|
3
|
+
TuiPlugin,
|
|
4
|
+
TuiPluginApi,
|
|
5
|
+
TuiPluginModule,
|
|
6
|
+
} from '@opencode-ai/plugin/tui'
|
|
7
|
+
import { createMemo, createSignal, For, onCleanup, Show } from 'solid-js'
|
|
8
|
+
|
|
9
|
+
import { fitLine, renderSidebarUsageLines } from './format.js'
|
|
10
|
+
import {
|
|
11
|
+
fallbackQuotaGroupsFromTitle,
|
|
12
|
+
mergeLiveAndPersistedPanelUsage,
|
|
13
|
+
quotaGroupsAreCollapsible,
|
|
14
|
+
quotaGroupsSummary,
|
|
15
|
+
quotaGroupsUseBullets,
|
|
16
|
+
renderSidebarQuotaGroups,
|
|
17
|
+
sidebarPanelQuotaSnapshots,
|
|
18
|
+
type SidebarQuotaGroup,
|
|
19
|
+
} from './tui_helpers.js'
|
|
20
|
+
import {
|
|
21
|
+
loadConfig,
|
|
22
|
+
loadState,
|
|
23
|
+
quotaConfigPaths,
|
|
24
|
+
resolveOpencodeDataDir,
|
|
25
|
+
stateFilePath,
|
|
26
|
+
} from './storage.js'
|
|
27
|
+
import { looksDecorated, normalizeBaseTitle } from './title.js'
|
|
28
|
+
import type { QuotaSidebarConfig } from './types.js'
|
|
29
|
+
import {
|
|
30
|
+
fromCachedSessionUsage,
|
|
31
|
+
mergeUsage,
|
|
32
|
+
summarizeMessages,
|
|
33
|
+
} from './usage.js'
|
|
34
|
+
|
|
35
|
+
const id = 'leo.quota-sidebar'
|
|
36
|
+
const INTERNAL_CONTEXT_PLUGIN_ID = 'internal:sidebar-context'
|
|
37
|
+
const SECTION_INDENT = 2
|
|
38
|
+
const DEFAULT_WIDTH = 36
|
|
39
|
+
|
|
40
|
+
type SidebarPanelData = {
|
|
41
|
+
enabled: boolean
|
|
42
|
+
width: number
|
|
43
|
+
usageLines: string[]
|
|
44
|
+
quotaGroups: SidebarQuotaGroup[]
|
|
45
|
+
compactTitle?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const latestCompactTitles = new Map<string, string>()
|
|
49
|
+
const [compactTitleVersion, setCompactTitleVersion] = createSignal(0)
|
|
50
|
+
|
|
51
|
+
function directoryPath(api: TuiPluginApi) {
|
|
52
|
+
return api.state.path.directory || process.cwd()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function worktreePath(api: TuiPluginApi) {
|
|
56
|
+
return api.state.path.worktree || directoryPath(api)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function panelConfig(config: QuotaSidebarConfig): QuotaSidebarConfig {
|
|
60
|
+
return {
|
|
61
|
+
...config,
|
|
62
|
+
sidebar: {
|
|
63
|
+
...config.sidebar,
|
|
64
|
+
width: Math.max(8, config.sidebar.width - SECTION_INDENT),
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveCompactTitle(sessionID: string, persistedTitle?: string) {
|
|
70
|
+
const liveTitle = latestCompactTitles.get(sessionID)
|
|
71
|
+
if (liveTitle && looksDecorated(liveTitle)) return liveTitle
|
|
72
|
+
if (persistedTitle && looksDecorated(persistedTitle)) return persistedTitle
|
|
73
|
+
return liveTitle || persistedTitle
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function loadSidebarPanel(
|
|
77
|
+
api: TuiPluginApi,
|
|
78
|
+
sessionID: string,
|
|
79
|
+
): Promise<SidebarPanelData> {
|
|
80
|
+
const statePath = stateFilePath(resolveOpencodeDataDir())
|
|
81
|
+
const config = await loadConfig(
|
|
82
|
+
quotaConfigPaths(worktreePath(api), directoryPath(api)),
|
|
83
|
+
)
|
|
84
|
+
// Session payload lives in day chunks that the server updates from a
|
|
85
|
+
// separate process, so TUI should re-read persisted state instead of keeping
|
|
86
|
+
// an extra full-state cache here.
|
|
87
|
+
const state = await loadState(statePath)
|
|
88
|
+
const session = state.sessions[sessionID]
|
|
89
|
+
const enabled = config.sidebar.enabled
|
|
90
|
+
const width = Math.max(8, config.sidebar.width - SECTION_INDENT)
|
|
91
|
+
const liveEntries = api.state.session.messages(sessionID).map((info) => ({
|
|
92
|
+
info,
|
|
93
|
+
})) as Parameters<typeof summarizeMessages>[0]
|
|
94
|
+
|
|
95
|
+
const liveUsage = summarizeMessages(liveEntries, 0, 1)
|
|
96
|
+
const cachedUsage = session?.sidebarPanel?.usage || session?.usage
|
|
97
|
+
const persistedUsage = cachedUsage
|
|
98
|
+
? fromCachedSessionUsage(cachedUsage)
|
|
99
|
+
: undefined
|
|
100
|
+
const usage = mergeLiveAndPersistedPanelUsage(
|
|
101
|
+
liveUsage.assistantMessages > 0 ? liveUsage : undefined,
|
|
102
|
+
persistedUsage,
|
|
103
|
+
)
|
|
104
|
+
const compactTitle = resolveCompactTitle(sessionID, session?.lastAppliedTitle)
|
|
105
|
+
|
|
106
|
+
if (!enabled) {
|
|
107
|
+
return {
|
|
108
|
+
enabled,
|
|
109
|
+
width,
|
|
110
|
+
usageLines: [],
|
|
111
|
+
quotaGroups: [],
|
|
112
|
+
compactTitle: session?.lastAppliedTitle,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const usageLines = usage
|
|
117
|
+
? renderSidebarUsageLines(usage, panelConfig(config))
|
|
118
|
+
: []
|
|
119
|
+
const quotaGroups = renderSidebarQuotaGroups(
|
|
120
|
+
sidebarPanelQuotaSnapshots(session?.sidebarPanel),
|
|
121
|
+
panelConfig(config),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
enabled,
|
|
126
|
+
width,
|
|
127
|
+
usageLines,
|
|
128
|
+
quotaGroups,
|
|
129
|
+
compactTitle,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function useSidebarPanelData(api: TuiPluginApi, sessionID: () => string) {
|
|
134
|
+
const [panel, setPanel] = createSignal<SidebarPanelData | undefined>()
|
|
135
|
+
let disposed = false
|
|
136
|
+
let loadVersion = 0
|
|
137
|
+
|
|
138
|
+
const reload = () => {
|
|
139
|
+
const currentVersion = ++loadVersion
|
|
140
|
+
const currentSessionID = sessionID()
|
|
141
|
+
void loadSidebarPanel(api, currentSessionID)
|
|
142
|
+
.then((next) => {
|
|
143
|
+
if (disposed || currentVersion !== loadVersion) return
|
|
144
|
+
setPanel(next)
|
|
145
|
+
})
|
|
146
|
+
.catch((error) => {
|
|
147
|
+
if (disposed || currentVersion !== loadVersion) return
|
|
148
|
+
void error
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
reload()
|
|
153
|
+
|
|
154
|
+
const timers = new Set<ReturnType<typeof setTimeout>>()
|
|
155
|
+
const queueRefresh = (delay = 250) => {
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
timers.delete(timer)
|
|
158
|
+
reload()
|
|
159
|
+
}, delay)
|
|
160
|
+
timers.add(timer)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const scheduleRefresh = () => {
|
|
164
|
+
queueRefresh(150)
|
|
165
|
+
queueRefresh(600)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Bulk session sync populates messages asynchronously without emitting the
|
|
169
|
+
// real-time message.updated events we listen to below. Retry a few times on
|
|
170
|
+
// mount so historical sessions can render usage once the sync finishes.
|
|
171
|
+
queueRefresh(500)
|
|
172
|
+
queueRefresh(1_500)
|
|
173
|
+
queueRefresh(4_000)
|
|
174
|
+
|
|
175
|
+
const unsubscribers = [
|
|
176
|
+
api.event.on('session.updated', (event) => {
|
|
177
|
+
if (event.properties.info.id === sessionID()) {
|
|
178
|
+
scheduleRefresh()
|
|
179
|
+
}
|
|
180
|
+
}),
|
|
181
|
+
api.event.on('message.updated', (event) => {
|
|
182
|
+
if (event.properties.info.sessionID === sessionID()) {
|
|
183
|
+
scheduleRefresh()
|
|
184
|
+
}
|
|
185
|
+
}),
|
|
186
|
+
api.event.on('message.removed', (event) => {
|
|
187
|
+
if (event.properties.sessionID === sessionID()) {
|
|
188
|
+
scheduleRefresh()
|
|
189
|
+
}
|
|
190
|
+
}),
|
|
191
|
+
api.event.on('tui.session.select', (event) => {
|
|
192
|
+
if (event.properties.sessionID === sessionID()) {
|
|
193
|
+
scheduleRefresh()
|
|
194
|
+
}
|
|
195
|
+
}),
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
onCleanup(() => {
|
|
199
|
+
disposed = true
|
|
200
|
+
for (const timer of timers) clearTimeout(timer)
|
|
201
|
+
timers.clear()
|
|
202
|
+
for (const unsubscribe of unsubscribers) unsubscribe()
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
return panel
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function SectionHeading(props: {
|
|
209
|
+
api: TuiPluginApi
|
|
210
|
+
value: string
|
|
211
|
+
collapsible?: boolean
|
|
212
|
+
open?: boolean
|
|
213
|
+
summary?: string
|
|
214
|
+
onToggle?: () => void
|
|
215
|
+
}) {
|
|
216
|
+
const clickable = () => props.collapsible === true && props.onToggle
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<box
|
|
220
|
+
flexDirection="row"
|
|
221
|
+
gap={1}
|
|
222
|
+
onMouseDown={() => {
|
|
223
|
+
if (!clickable()) return
|
|
224
|
+
props.onToggle?.()
|
|
225
|
+
}}
|
|
226
|
+
>
|
|
227
|
+
<Show when={props.collapsible}>
|
|
228
|
+
<text fg={props.api.theme.current.text}>{props.open ? '▼' : '▶'}</text>
|
|
229
|
+
</Show>
|
|
230
|
+
<text fg={props.api.theme.current.text}>
|
|
231
|
+
<b>{props.value}</b>
|
|
232
|
+
<Show when={props.summary}>
|
|
233
|
+
<span style={{ fg: props.api.theme.current.textMuted }}>
|
|
234
|
+
{' '}
|
|
235
|
+
{props.summary}
|
|
236
|
+
</span>
|
|
237
|
+
</Show>
|
|
238
|
+
</text>
|
|
239
|
+
</box>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function quotaToneColor(api: TuiPluginApi, tone: SidebarQuotaGroup['tone']) {
|
|
244
|
+
const theme = api.theme.current
|
|
245
|
+
if (tone === 'success') return theme.success
|
|
246
|
+
if (tone === 'warning') return theme.warning
|
|
247
|
+
if (tone === 'error') return theme.error
|
|
248
|
+
return theme.textMuted
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function QuotaGroupBlock(props: {
|
|
252
|
+
api: TuiPluginApi
|
|
253
|
+
group: SidebarQuotaGroup
|
|
254
|
+
bullet: boolean
|
|
255
|
+
}) {
|
|
256
|
+
const detailColor = quotaToneColor(props.api, props.group.tone)
|
|
257
|
+
const content = (
|
|
258
|
+
<box gap={0}>
|
|
259
|
+
<text>
|
|
260
|
+
<span style={{ fg: props.api.theme.current.text }}>
|
|
261
|
+
{props.group.shortLabel}
|
|
262
|
+
</span>
|
|
263
|
+
<Show when={props.group.detail}>
|
|
264
|
+
<span style={{ fg: detailColor }}> {props.group.detail}</span>
|
|
265
|
+
</Show>
|
|
266
|
+
</text>
|
|
267
|
+
<For each={props.group.continuationLines}>
|
|
268
|
+
{(line) => <text fg={detailColor}>{line}</text>}
|
|
269
|
+
</For>
|
|
270
|
+
</box>
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return (
|
|
274
|
+
<Show when={props.bullet} fallback={content}>
|
|
275
|
+
<box flexDirection="row" gap={1}>
|
|
276
|
+
<text flexShrink={0} fg={quotaToneColor(props.api, props.group.tone)}>
|
|
277
|
+
•
|
|
278
|
+
</text>
|
|
279
|
+
{content}
|
|
280
|
+
</box>
|
|
281
|
+
</Show>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function fallbackUsageCostLineFromTitle(title: string, width: number) {
|
|
286
|
+
// Legacy compatibility: historical sessions may only have compact-title cost
|
|
287
|
+
// tokens and no persisted sidebar panel payload yet.
|
|
288
|
+
const apiCost = (title || '')
|
|
289
|
+
.split(' | ')
|
|
290
|
+
.map((part) => part.trim())
|
|
291
|
+
.find(
|
|
292
|
+
(part) =>
|
|
293
|
+
/^API\$/.test(part) ||
|
|
294
|
+
/^API\s+\$/.test(part) ||
|
|
295
|
+
/^Est\$/.test(part) ||
|
|
296
|
+
/^Est\s+\$/.test(part),
|
|
297
|
+
)
|
|
298
|
+
if (!apiCost) return undefined
|
|
299
|
+
return fitLine(
|
|
300
|
+
apiCost.replace(/^Est\$/, 'API $').replace(/^API\$/, 'API $'),
|
|
301
|
+
width,
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function SidebarContentView(props: { api: TuiPluginApi; sessionID: string }) {
|
|
306
|
+
const panel = useSidebarPanelData(props.api, () => props.sessionID)
|
|
307
|
+
const [quotaOpen, setQuotaOpen] = createSignal(true)
|
|
308
|
+
const width = createMemo(
|
|
309
|
+
() => panel()?.width || DEFAULT_WIDTH - SECTION_INDENT,
|
|
310
|
+
)
|
|
311
|
+
const compactTitle = createMemo(() => {
|
|
312
|
+
compactTitleVersion()
|
|
313
|
+
return resolveCompactTitle(props.sessionID, panel()?.compactTitle) || ''
|
|
314
|
+
})
|
|
315
|
+
const usageLines = createMemo(() => {
|
|
316
|
+
const liveLines = panel()?.usageLines || []
|
|
317
|
+
const hasCostLine = liveLines.some((line) => /^(?:API|Est)\b/.test(line))
|
|
318
|
+
if (hasCostLine) return liveLines
|
|
319
|
+
const costLine = fallbackUsageCostLineFromTitle(compactTitle(), width())
|
|
320
|
+
return costLine ? [...liveLines, costLine] : liveLines
|
|
321
|
+
})
|
|
322
|
+
const quotaGroups = createMemo(() => {
|
|
323
|
+
const liveGroups = panel()?.quotaGroups || []
|
|
324
|
+
if (liveGroups.length > 0) return liveGroups
|
|
325
|
+
return fallbackQuotaGroupsFromTitle(compactTitle(), width())
|
|
326
|
+
})
|
|
327
|
+
const hasUsage = createMemo(() => usageLines().length > 0)
|
|
328
|
+
const hasQuota = createMemo(() => quotaGroups().length > 0)
|
|
329
|
+
const quotaBullets = createMemo(() => quotaGroupsUseBullets(quotaGroups()))
|
|
330
|
+
const quotaCollapsible = createMemo(() =>
|
|
331
|
+
quotaGroupsAreCollapsible(quotaGroups()),
|
|
332
|
+
)
|
|
333
|
+
const quotaSummary = createMemo(() => {
|
|
334
|
+
if (!quotaCollapsible() || quotaOpen()) return undefined
|
|
335
|
+
return quotaGroupsSummary(quotaGroups())
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<box gap={0}>
|
|
340
|
+
<Show when={hasUsage()}>
|
|
341
|
+
<box gap={0}>
|
|
342
|
+
<SectionHeading api={props.api} value="Usage" />
|
|
343
|
+
<box gap={0}>
|
|
344
|
+
<For each={usageLines()}>
|
|
345
|
+
{(line) => (
|
|
346
|
+
<text fg={props.api.theme.current.textMuted}>{line}</text>
|
|
347
|
+
)}
|
|
348
|
+
</For>
|
|
349
|
+
</box>
|
|
350
|
+
</box>
|
|
351
|
+
</Show>
|
|
352
|
+
|
|
353
|
+
<Show when={hasQuota()}>
|
|
354
|
+
<box paddingTop={hasUsage() ? 1 : 0} gap={0}>
|
|
355
|
+
<SectionHeading
|
|
356
|
+
api={props.api}
|
|
357
|
+
value="Quota"
|
|
358
|
+
collapsible={quotaCollapsible()}
|
|
359
|
+
open={quotaOpen()}
|
|
360
|
+
summary={quotaSummary()}
|
|
361
|
+
onToggle={() => setQuotaOpen((value) => !value)}
|
|
362
|
+
/>
|
|
363
|
+
<Show when={!quotaCollapsible() || quotaOpen()}>
|
|
364
|
+
<box gap={0}>
|
|
365
|
+
<For each={quotaGroups()}>
|
|
366
|
+
{(group) => (
|
|
367
|
+
<QuotaGroupBlock
|
|
368
|
+
api={props.api}
|
|
369
|
+
group={group}
|
|
370
|
+
bullet={quotaBullets()}
|
|
371
|
+
/>
|
|
372
|
+
)}
|
|
373
|
+
</For>
|
|
374
|
+
</box>
|
|
375
|
+
</Show>
|
|
376
|
+
</box>
|
|
377
|
+
</Show>
|
|
378
|
+
</box>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function SidebarTitleView(props: {
|
|
383
|
+
api: TuiPluginApi
|
|
384
|
+
sessionID: string
|
|
385
|
+
title: string
|
|
386
|
+
shareURL?: string
|
|
387
|
+
}) {
|
|
388
|
+
const panel = useSidebarPanelData(props.api, () => props.sessionID)
|
|
389
|
+
const width = createMemo(
|
|
390
|
+
() => panel()?.width || DEFAULT_WIDTH - SECTION_INDENT,
|
|
391
|
+
)
|
|
392
|
+
const titleLines = createMemo(() => {
|
|
393
|
+
const baseTitle = normalizeBaseTitle(props.title || 'Session') || 'Session'
|
|
394
|
+
return baseTitle
|
|
395
|
+
.split(/\r?\n/)
|
|
396
|
+
.filter(Boolean)
|
|
397
|
+
.map((line) => fitLine(line, width()))
|
|
398
|
+
})
|
|
399
|
+
const shareLine = createMemo(() =>
|
|
400
|
+
props.shareURL ? fitLine(props.shareURL, width()) : undefined,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
return (
|
|
404
|
+
<box gap={0} paddingRight={1}>
|
|
405
|
+
<box gap={0}>
|
|
406
|
+
<For each={titleLines()}>
|
|
407
|
+
{(line) => (
|
|
408
|
+
<text fg={props.api.theme.current.text}>
|
|
409
|
+
<b>{line}</b>
|
|
410
|
+
</text>
|
|
411
|
+
)}
|
|
412
|
+
</For>
|
|
413
|
+
<Show when={shareLine()}>
|
|
414
|
+
<text fg={props.api.theme.current.textMuted}>{shareLine()}</text>
|
|
415
|
+
</Show>
|
|
416
|
+
</box>
|
|
417
|
+
</box>
|
|
418
|
+
)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const tui: TuiPlugin = async (api) => {
|
|
422
|
+
const config = await loadConfig(
|
|
423
|
+
quotaConfigPaths(worktreePath(api), directoryPath(api)),
|
|
424
|
+
)
|
|
425
|
+
let didDeactivateContext = false
|
|
426
|
+
if (config.sidebar.enabled) {
|
|
427
|
+
const contextPlugin = api.plugins
|
|
428
|
+
.list()
|
|
429
|
+
.find((item) => item.id === INTERNAL_CONTEXT_PLUGIN_ID)
|
|
430
|
+
if (contextPlugin?.active) {
|
|
431
|
+
didDeactivateContext = await api.plugins
|
|
432
|
+
.deactivate(INTERNAL_CONTEXT_PLUGIN_ID)
|
|
433
|
+
.catch(() => false)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
api.lifecycle.onDispose(() => {
|
|
437
|
+
if (!didDeactivateContext) return
|
|
438
|
+
return api.plugins
|
|
439
|
+
.activate(INTERNAL_CONTEXT_PLUGIN_ID)
|
|
440
|
+
.then(() => undefined)
|
|
441
|
+
.catch(() => undefined)
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
api.slots.register({
|
|
445
|
+
order: 100,
|
|
446
|
+
slots: {
|
|
447
|
+
sidebar_title(
|
|
448
|
+
_ctx: unknown,
|
|
449
|
+
props: { session_id: string; title: string; share_url?: string },
|
|
450
|
+
) {
|
|
451
|
+
if (latestCompactTitles.get(props.session_id) !== props.title) {
|
|
452
|
+
latestCompactTitles.set(props.session_id, props.title)
|
|
453
|
+
setCompactTitleVersion((value) => value + 1)
|
|
454
|
+
}
|
|
455
|
+
return (
|
|
456
|
+
<SidebarTitleView
|
|
457
|
+
api={api}
|
|
458
|
+
sessionID={props.session_id}
|
|
459
|
+
title={props.title}
|
|
460
|
+
shareURL={props.share_url}
|
|
461
|
+
/>
|
|
462
|
+
)
|
|
463
|
+
},
|
|
464
|
+
sidebar_content(_ctx: unknown, props: { session_id: string }) {
|
|
465
|
+
return <SidebarContentView api={api} sessionID={props.session_id} />
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const plugin: TuiPluginModule & { id: string } = {
|
|
472
|
+
id,
|
|
473
|
+
tui,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export default plugin
|
package/dist/tui_helpers.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { QuotaSidebarConfig, QuotaSnapshot, SidebarPanelState } from
|
|
2
|
-
import type { UsageSummary } from
|
|
3
|
-
export type SidebarQuotaTone =
|
|
1
|
+
import type { QuotaSidebarConfig, QuotaSnapshot, SidebarPanelState } from './types.js';
|
|
2
|
+
import type { UsageSummary } from './usage.js';
|
|
3
|
+
export type SidebarQuotaTone = 'success' | 'warning' | 'error' | 'muted';
|
|
4
4
|
export type SidebarQuotaGroup = {
|
|
5
5
|
providerID: string;
|
|
6
|
-
status: QuotaSnapshot[
|
|
6
|
+
status: QuotaSnapshot['status'];
|
|
7
7
|
tone: SidebarQuotaTone;
|
|
8
8
|
shortLabel: string;
|
|
9
9
|
detail: string;
|
package/dist/tui_helpers.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { fitLine, renderSidebarQuotaLineGroups } from
|
|
2
|
-
import { collapseQuotaSnapshots } from
|
|
3
|
-
import { isSupportedQuotaSnapshot, isSupportedQuotaTitleLabel, } from
|
|
1
|
+
import { fitLine, renderSidebarQuotaLineGroups } from './format.js';
|
|
2
|
+
import { collapseQuotaSnapshots } from './quota_render.js';
|
|
3
|
+
import { isSupportedQuotaSnapshot, isSupportedQuotaTitleLabel, } from './supported_quota.js';
|
|
4
4
|
const VISIBLE_QUOTA_STATUSES = new Set([
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
'ok',
|
|
6
|
+
'error',
|
|
7
|
+
'unsupported',
|
|
8
|
+
'unavailable',
|
|
9
9
|
]);
|
|
10
10
|
function parseQuotaLineParts(lines) {
|
|
11
|
-
const firstLine = lines[0]?.trimStart() ||
|
|
11
|
+
const firstLine = lines[0]?.trimStart() || '';
|
|
12
12
|
const match = /^(\S+)(?:\s+(.*))?$/.exec(firstLine);
|
|
13
|
-
const shortLabel = match?.[1] || firstLine ||
|
|
14
|
-
const detail = match?.[2] ||
|
|
13
|
+
const shortLabel = match?.[1] || firstLine || 'Quota';
|
|
14
|
+
const detail = match?.[2] || '';
|
|
15
15
|
const continuationLines = lines
|
|
16
16
|
.slice(1)
|
|
17
17
|
.map((line) => line.trimEnd())
|
|
@@ -37,56 +37,56 @@ function quotaPercents(quota) {
|
|
|
37
37
|
return values;
|
|
38
38
|
}
|
|
39
39
|
function quotaTone(quota) {
|
|
40
|
-
if (quota.status ===
|
|
41
|
-
return
|
|
42
|
-
if (quota.status ===
|
|
43
|
-
return
|
|
40
|
+
if (quota.status === 'error')
|
|
41
|
+
return 'error';
|
|
42
|
+
if (quota.status === 'unsupported' || quota.status === 'unavailable') {
|
|
43
|
+
return 'muted';
|
|
44
44
|
}
|
|
45
|
-
if (quota.status !==
|
|
46
|
-
return
|
|
45
|
+
if (quota.status !== 'ok')
|
|
46
|
+
return 'muted';
|
|
47
47
|
const percents = quotaPercents(quota);
|
|
48
48
|
if (percents.length === 0) {
|
|
49
49
|
if (quota.balance && Number.isFinite(quota.balance.amount)) {
|
|
50
50
|
if (quota.balance.amount < 0)
|
|
51
|
-
return
|
|
52
|
-
return
|
|
51
|
+
return 'error';
|
|
52
|
+
return 'muted';
|
|
53
53
|
}
|
|
54
|
-
return
|
|
54
|
+
return 'muted';
|
|
55
55
|
}
|
|
56
56
|
const remaining = Math.min(...percents);
|
|
57
57
|
if (remaining <= 5)
|
|
58
|
-
return
|
|
58
|
+
return 'error';
|
|
59
59
|
if (remaining <= 20)
|
|
60
|
-
return
|
|
61
|
-
return
|
|
60
|
+
return 'warning';
|
|
61
|
+
return 'success';
|
|
62
62
|
}
|
|
63
63
|
function fallbackQuotaTone(detail) {
|
|
64
64
|
const safe = detail.trim();
|
|
65
65
|
if (!safe)
|
|
66
|
-
return
|
|
66
|
+
return 'muted';
|
|
67
67
|
if (/\b(?:unsupported|unavailable)\b/i.test(safe))
|
|
68
|
-
return
|
|
68
|
+
return 'muted';
|
|
69
69
|
if (/\berror\b/i.test(safe) || /^\?$/.test(safe))
|
|
70
|
-
return
|
|
70
|
+
return 'error';
|
|
71
71
|
if (/\bB-/.test(safe))
|
|
72
|
-
return
|
|
72
|
+
return 'error';
|
|
73
73
|
const percents = [
|
|
74
74
|
...safe.matchAll(/\b(?:\d+[hdw]|[DWM]|S7d|O7d|OA7d|Co7d|Sk5h|SkW)(\d{1,3})\b/gi),
|
|
75
75
|
]
|
|
76
76
|
.map((match) => Number(match[1]))
|
|
77
77
|
.filter((value) => Number.isFinite(value));
|
|
78
78
|
if (percents.length === 0)
|
|
79
|
-
return
|
|
79
|
+
return 'muted';
|
|
80
80
|
const remaining = Math.min(...percents);
|
|
81
81
|
if (remaining <= 5)
|
|
82
|
-
return
|
|
82
|
+
return 'error';
|
|
83
83
|
if (remaining <= 20)
|
|
84
|
-
return
|
|
85
|
-
return
|
|
84
|
+
return 'warning';
|
|
85
|
+
return 'success';
|
|
86
86
|
}
|
|
87
87
|
export function renderSidebarQuotaGroups(quotas, config) {
|
|
88
88
|
const visibleQuotaCount = collapseQuotaSnapshots(quotas).filter((quota) => VISIBLE_QUOTA_STATUSES.has(quota.status)).length;
|
|
89
|
-
const renderConfig = visibleQuotaCount >
|
|
89
|
+
const renderConfig = visibleQuotaCount > 0
|
|
90
90
|
? {
|
|
91
91
|
...config,
|
|
92
92
|
sidebar: {
|
|
@@ -139,8 +139,8 @@ export function mergeLiveAndPersistedPanelUsage(liveUsage, persistedUsage) {
|
|
|
139
139
|
}
|
|
140
140
|
export function fallbackQuotaGroupsFromTitle(title, width) {
|
|
141
141
|
// Legacy compatibility: old sessions may only have compact title fragments.
|
|
142
|
-
const parts = (title ||
|
|
143
|
-
.split(
|
|
142
|
+
const parts = (title || '')
|
|
143
|
+
.split(' | ')
|
|
144
144
|
.map((part) => part.trim())
|
|
145
145
|
.filter(Boolean);
|
|
146
146
|
const quotaParts = parts
|
|
@@ -148,7 +148,7 @@ export function fallbackQuotaGroupsFromTitle(title, width) {
|
|
|
148
148
|
.filter((part) => !/^Cd\d/.test(part) && !/^API\b/.test(part) && !/^Est\b/.test(part));
|
|
149
149
|
if (quotaParts.length === 0)
|
|
150
150
|
return [];
|
|
151
|
-
const contentWidth =
|
|
151
|
+
const contentWidth = Math.max(1, width - 2);
|
|
152
152
|
const groups = [];
|
|
153
153
|
for (const [index, part] of quotaParts.entries()) {
|
|
154
154
|
const line = fitLine(part, contentWidth);
|
|
@@ -157,7 +157,7 @@ export function fallbackQuotaGroupsFromTitle(title, width) {
|
|
|
157
157
|
continue;
|
|
158
158
|
groups.push({
|
|
159
159
|
providerID: `fallback:${index}`,
|
|
160
|
-
status:
|
|
160
|
+
status: 'ok',
|
|
161
161
|
tone: fallbackQuotaTone(parsed.detail),
|
|
162
162
|
shortLabel: parsed.shortLabel,
|
|
163
163
|
detail: parsed.detail,
|
|
@@ -167,7 +167,7 @@ export function fallbackQuotaGroupsFromTitle(title, width) {
|
|
|
167
167
|
return groups;
|
|
168
168
|
}
|
|
169
169
|
export function quotaGroupsUseBullets(groups) {
|
|
170
|
-
return groups.length >
|
|
170
|
+
return groups.length > 0;
|
|
171
171
|
}
|
|
172
172
|
export function quotaGroupsAreCollapsible(groups) {
|
|
173
173
|
return groups.length > 2;
|
package/package.json
CHANGED