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