@hienlh/ppm 0.17.44 → 0.17.46
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/CHANGELOG.md +25 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-resource-editor-CQWFrDeG.js → ai-resource-editor-C6whUvqU.js} +1 -1
- package/dist/web/assets/{audio-preview-Cyb8SLCu.js → audio-preview-zCaVGICs.js} +1 -1
- package/dist/web/assets/chat-tab-34EBK6a-.js +13 -0
- package/dist/web/assets/{code-editor-DTqFVL_2.js → code-editor-B-3Ys0pn.js} +2 -2
- package/dist/web/assets/{conflict-editor-BFiY6VyT.js → conflict-editor-CmbzavrH.js} +1 -1
- package/dist/web/assets/{csv-preview-DpeN7hH8.js → csv-preview-DSe1UKLQ.js} +1 -1
- package/dist/web/assets/{database-viewer-D9OT6V9B.js → database-viewer-BC3tMeSZ.js} +1 -1
- package/dist/web/assets/{diff-viewer-LqH4ARjy.js → diff-viewer-CH7HtuY_.js} +1 -1
- package/dist/web/assets/{docx-preview-Dqzlw47y.js → docx-preview-CXw0EiKN.js} +1 -1
- package/dist/web/assets/{extension-webview-DsNuZtJO.js → extension-webview-BKNPTGJ4.js} +1 -1
- package/dist/web/assets/{git-log-panel-mcn-Atil.js → git-log-panel-4at7vGet.js} +1 -1
- package/dist/web/assets/{glide-data-grid-CyZAiAgS.js → glide-data-grid-BJBoXCv5.js} +1 -1
- package/dist/web/assets/{group-chat-tab-CRkPysZw.js → group-chat-tab-XNIsuLbW.js} +1 -1
- package/dist/web/assets/{image-preview-DnNyBpmU.js → image-preview-Hqu7-Pbv.js} +1 -1
- package/dist/web/assets/{index-D4KFhwmL.js → index-BR5KVuwx.js} +3 -3
- package/dist/web/assets/index-hIIXGoXQ.css +2 -0
- package/dist/web/assets/{keybindings-store-CtrEuZpU.js → keybindings-store-B_JMJIhN.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DDFElf1k.js → markdown-renderer-CPm8QbIv.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BFwR1QYD.js → markdown-renderer-WnJVcJ2v.js} +1 -1
- package/dist/web/assets/{notification-store-BTo6li6s.js → notification-store-CD8j6i20.js} +1 -1
- package/dist/web/assets/{pdf-preview-DDRKbO4z.js → pdf-preview-CA71gMQU.js} +1 -1
- package/dist/web/assets/{postgres-viewer-0UqxqUYu.js → postgres-viewer-CkZu5Ary.js} +1 -1
- package/dist/web/assets/{settings-tab-0VKOVbt2.js → settings-tab-Cy2JvRdN.js} +1 -1
- package/dist/web/assets/{sql-query-editor-Cf76aKfw.js → sql-query-editor-DxaQ6kWk.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-CO6dK1CU.js → sqlite-viewer-1fDZK9Jx.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-BFBwiME3.js → system-monitor-tab-B64gFhnE.js} +1 -1
- package/dist/web/assets/{terminal-tab-lu0ZsZmQ.js → terminal-tab-Dl-I2wBS.js} +1 -1
- package/dist/web/assets/{tool-cards-CMQowijv.js → tool-cards-HHgeNBqk.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-DU3LWaPu.js → use-monaco-theme-uP8s-9Qb.js} +1 -1
- package/dist/web/assets/{video-preview-CHUzaq4s.js → video-preview-sGT5PM0H.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +2 -2
- package/scripts/check-ppm-dir-usage.sh +0 -0
- package/scripts/install.sh +0 -0
- package/scripts/release.sh +0 -0
- package/src/cli/commands/stop.ts +6 -3
- package/src/providers/claude-agent-sdk.ts +66 -2
- package/src/server/routes/accounts.ts +6 -2
- package/src/server/routes/chat.ts +45 -2
- package/src/server/ws/chat.ts +110 -12
- package/src/services/account-selector.service.ts +98 -11
- package/src/services/autostart-generator.ts +5 -5
- package/src/services/autostart-register.ts +14 -0
- package/src/services/claude-usage.service.ts +29 -3
- package/src/services/db.service.ts +120 -1
- package/src/services/ppm-dir.ts +16 -0
- package/src/services/subprocess-retention.ts +63 -0
- package/src/services/supervisor.ts +87 -34
- package/src/services/windows-process-tree.ts +118 -4
- package/src/shared/turn-usage.ts +230 -0
- package/src/types/chat.ts +3 -1
- package/src/web/components/chat/chat-tab.tsx +3 -2
- package/src/web/components/chat/message-action-bar.tsx +0 -3
- package/src/web/components/chat/message-list.tsx +5 -1
- package/src/web/components/chat/session-debug-dialog.tsx +125 -0
- package/src/web/components/chat/turn-change-rollup.tsx +1 -3
- package/src/web/components/chat/turn-cost-warning.tsx +85 -0
- package/src/web/hooks/use-chat.ts +3 -0
- package/src/web/hooks/use-usage.ts +8 -3
- package/dist/web/assets/chat-tab-uBmb2rFB.js +0 -13
- package/dist/web/assets/index-BEB87kj8.css +0 -2
- package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/vscode-compat/node_modules/.bin/tsserver.exe +0 -0
|
@@ -6,6 +6,19 @@ import { BottomSheet } from "@/components/ui/mobile-bottom-sheet";
|
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { useIsMobile } from "@/hooks/use-is-mobile";
|
|
8
8
|
import { copyToClipboard } from "@/lib/clipboard";
|
|
9
|
+
import type { TurnUsage } from "../../../shared/turn-usage";
|
|
10
|
+
import {
|
|
11
|
+
assessTurnCost,
|
|
12
|
+
fmtTokens,
|
|
13
|
+
prefixTokens,
|
|
14
|
+
uncachedPrefixTokens,
|
|
15
|
+
} from "../../../shared/turn-usage";
|
|
16
|
+
|
|
17
|
+
interface TurnRecord {
|
|
18
|
+
id: number;
|
|
19
|
+
recordedAt: string;
|
|
20
|
+
usage: TurnUsage;
|
|
21
|
+
}
|
|
9
22
|
|
|
10
23
|
interface DebugInfo {
|
|
11
24
|
ppmSessionId: string;
|
|
@@ -160,6 +173,115 @@ function TranscriptImages({ sessionId, projectName }: { sessionId: string; proje
|
|
|
160
173
|
);
|
|
161
174
|
}
|
|
162
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Per-turn token history for the open session.
|
|
178
|
+
*
|
|
179
|
+
* Cost per turn is dominated by the replayed transcript, so a session that suddenly got
|
|
180
|
+
* expensive is diagnosed by comparing its turns against each other: the cheap ones read
|
|
181
|
+
* their prefix from cache, the expensive ones paid for it again.
|
|
182
|
+
*/
|
|
183
|
+
function TurnUsageHistory({ sessionId, projectName }: { sessionId: string; projectName: string }) {
|
|
184
|
+
const [turns, setTurns] = useState<TurnRecord[] | null>(null);
|
|
185
|
+
const [error, setError] = useState<string | null>(null);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
let live = true;
|
|
189
|
+
api.get<{ turns: TurnRecord[] }>(
|
|
190
|
+
`${projectUrl(projectName)}/chat/sessions/${sessionId}/usage?project=${encodeURIComponent(projectName)}`,
|
|
191
|
+
).then((r) => { if (live) setTurns(r.turns); })
|
|
192
|
+
.catch((e: Error) => { if (live) setError(e.message); });
|
|
193
|
+
return () => { live = false; };
|
|
194
|
+
}, [sessionId, projectName]);
|
|
195
|
+
|
|
196
|
+
if (error) return <p className="text-[11px] text-error">{error}</p>;
|
|
197
|
+
if (!turns) return <p className="text-[11px] text-text-subtle">Loading token history...</p>;
|
|
198
|
+
|
|
199
|
+
if (turns.length === 0) {
|
|
200
|
+
return (
|
|
201
|
+
<div className="flex flex-col gap-2">
|
|
202
|
+
<h3 className="text-xs font-semibold text-foreground">Token & cache</h3>
|
|
203
|
+
<p className="text-[11px] text-text-subtle">
|
|
204
|
+
No turns recorded yet. Send a message and the token split for each turn shows up here.
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const cold = turns.filter((t) => t.usage.coldStart).length;
|
|
211
|
+
const totalCost = turns.reduce((s, t) => s + t.usage.costUsd, 0);
|
|
212
|
+
const wasted = turns.reduce((s, t) => s + uncachedPrefixTokens(t.usage), 0);
|
|
213
|
+
// More than one account over these turns explains cold prefixes on its own: the cache
|
|
214
|
+
// does not follow a session across accounts.
|
|
215
|
+
const accountsUsed = new Set(turns.map((t) => t.usage.accountId).filter(Boolean)).size;
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div className="flex flex-col gap-2">
|
|
219
|
+
<h3 className="text-xs font-semibold text-foreground">Token & cache</h3>
|
|
220
|
+
<p className="text-[11px] text-text-secondary">
|
|
221
|
+
Last {turns.length} turn{turns.length === 1 ? "" : "s"} · {cold} restarted the session ·{" "}
|
|
222
|
+
{fmtTokens(wasted)} of transcript re-sent uncached · ${totalCost.toFixed(2)}
|
|
223
|
+
{accountsUsed > 1 && ` · spread over ${accountsUsed} accounts`}
|
|
224
|
+
</p>
|
|
225
|
+
|
|
226
|
+
<div className="max-h-[30vh] overflow-y-auto rounded-md border border-border">
|
|
227
|
+
<table className="w-full font-mono text-[10.5px]">
|
|
228
|
+
<thead className="sticky top-0 bg-surface text-text-subtle">
|
|
229
|
+
<tr>
|
|
230
|
+
<th className="px-2 py-1 text-left font-normal">When</th>
|
|
231
|
+
<th className="px-2 py-1 text-left font-normal">Account</th>
|
|
232
|
+
<th className="px-2 py-1 text-right font-normal">Prefix</th>
|
|
233
|
+
<th className="px-2 py-1 text-right font-normal">Cached</th>
|
|
234
|
+
<th className="px-2 py-1 text-right font-normal">Cost</th>
|
|
235
|
+
<th className="px-2 py-1 text-left font-normal">Start</th>
|
|
236
|
+
</tr>
|
|
237
|
+
</thead>
|
|
238
|
+
<tbody>
|
|
239
|
+
{turns.map((t) => {
|
|
240
|
+
const hit = Math.round(t.usage.cacheHitRate * 100);
|
|
241
|
+
const level = assessTurnCost(t.usage).level;
|
|
242
|
+
return (
|
|
243
|
+
<tr key={t.id} className="border-t border-border">
|
|
244
|
+
<td className="px-2 py-1 text-text-subtle">
|
|
245
|
+
{t.recordedAt.slice(11, 16) || t.recordedAt.slice(0, 10)}
|
|
246
|
+
</td>
|
|
247
|
+
<td
|
|
248
|
+
className="max-w-24 truncate px-2 py-1 text-text-subtle"
|
|
249
|
+
title={t.usage.accountLabel ?? t.usage.accountId ?? ""}
|
|
250
|
+
>
|
|
251
|
+
{t.usage.accountLabel ?? t.usage.accountId?.slice(0, 8) ?? "—"}
|
|
252
|
+
</td>
|
|
253
|
+
<td className="px-2 py-1 text-right text-text-secondary">
|
|
254
|
+
{fmtTokens(prefixTokens(t.usage))}
|
|
255
|
+
</td>
|
|
256
|
+
<td
|
|
257
|
+
className={`px-2 py-1 text-right ${
|
|
258
|
+
level === "bad" ? "text-warning" : level === "warn" ? "text-text-primary" : "text-success"
|
|
259
|
+
}`}
|
|
260
|
+
>
|
|
261
|
+
{hit}%
|
|
262
|
+
</td>
|
|
263
|
+
<td className="px-2 py-1 text-right text-text-secondary">
|
|
264
|
+
${t.usage.costUsd.toFixed(3)}
|
|
265
|
+
</td>
|
|
266
|
+
<td className="px-2 py-1 text-text-subtle">
|
|
267
|
+
{t.usage.coldStart ? (t.usage.coldReason ?? "cold") : "warm"}
|
|
268
|
+
</td>
|
|
269
|
+
</tr>
|
|
270
|
+
);
|
|
271
|
+
})}
|
|
272
|
+
</tbody>
|
|
273
|
+
</table>
|
|
274
|
+
</div>
|
|
275
|
+
<p className="text-[11px] text-text-subtle">
|
|
276
|
+
A high "Cached" share is the cheap case. A turn marked <code>idle_timeout</code> paid for the
|
|
277
|
+
whole transcript again because the session sat idle with no tab open long enough for PPM to
|
|
278
|
+
release its subprocess. The cache is scoped per account, so a turn on a different account
|
|
279
|
+
starts cold no matter how the session was resumed.
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
163
285
|
/** Toolbar button opening the session's debug facts and transcript maintenance. */
|
|
164
286
|
export function SessionDebugButton({ sessionId, projectName }: { sessionId: string; projectName: string }) {
|
|
165
287
|
const [open, setOpen] = useState(false);
|
|
@@ -208,6 +330,9 @@ export function SessionDebugButton({ sessionId, projectName }: { sessionId: stri
|
|
|
208
330
|
<Button size="sm" onClick={copy} disabled={!info} className="w-full">
|
|
209
331
|
{copied ? <><ClipboardCheck className="size-3.5" /> Copied!</> : <><Copy className="size-3.5" /> Copy</>}
|
|
210
332
|
</Button>
|
|
333
|
+
<div className="border-t border-border pt-3">
|
|
334
|
+
{open && <TurnUsageHistory sessionId={sessionId} projectName={projectName} />}
|
|
335
|
+
</div>
|
|
211
336
|
<div className="border-t border-border pt-3">
|
|
212
337
|
{open && <TranscriptImages sessionId={sessionId} projectName={projectName} />}
|
|
213
338
|
</div>
|
|
@@ -13,10 +13,9 @@ import { TurnChangePill, changeTotals } from "./turn-change-pill";
|
|
|
13
13
|
import { TurnChangeTray } from "./turn-change-tray";
|
|
14
14
|
import { TurnChangeSheet } from "./turn-change-sheet";
|
|
15
15
|
|
|
16
|
-
export function TurnChangeRollup({ timestamp, content,
|
|
16
|
+
export function TurnChangeRollup({ timestamp, content, changes, onJumpToEdit }: {
|
|
17
17
|
timestamp: string;
|
|
18
18
|
content: string;
|
|
19
|
-
accountLabel?: string;
|
|
20
19
|
changes?: TurnFileChange[];
|
|
21
20
|
onJumpToEdit?: (editRef: string) => void;
|
|
22
21
|
}) {
|
|
@@ -35,7 +34,6 @@ export function TurnChangeRollup({ timestamp, content, accountLabel, changes, on
|
|
|
35
34
|
<MessageActionBar
|
|
36
35
|
timestamp={timestamp}
|
|
37
36
|
content={content}
|
|
38
|
-
accountLabel={accountLabel}
|
|
39
37
|
className="-mt-1.5"
|
|
40
38
|
>
|
|
41
39
|
{changes && changes.length > 0 && (
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { ChevronDown, TriangleAlert } from "lucide-react";
|
|
3
|
+
import type { TurnUsage } from "../../../shared/turn-usage";
|
|
4
|
+
import {
|
|
5
|
+
assessTurnCost,
|
|
6
|
+
fmtTokens,
|
|
7
|
+
prefixCostMultiplier,
|
|
8
|
+
prefixTokens,
|
|
9
|
+
} from "../../../shared/turn-usage";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Why a turn cost more than the one before it.
|
|
13
|
+
*
|
|
14
|
+
* A session's transcript is replayed to the API every turn, so the bill tracks the cache
|
|
15
|
+
* hit rate on that prefix rather than anything visible in the conversation. When the prefix
|
|
16
|
+
* stops being cached the same message can cost several times more, which is invisible
|
|
17
|
+
* without saying so — hence a notice attached to the turn that paid for it.
|
|
18
|
+
*
|
|
19
|
+
* Only renders for turns worth acting on; a cheap or well-cached turn shows nothing.
|
|
20
|
+
*/
|
|
21
|
+
export function TurnCostWarning({ usage }: { usage: TurnUsage }) {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const verdict = assessTurnCost(usage);
|
|
24
|
+
if (verdict.level === "ok") return null;
|
|
25
|
+
|
|
26
|
+
const multiplier = prefixCostMultiplier(usage);
|
|
27
|
+
const prefix = prefixTokens(usage);
|
|
28
|
+
const hitPct = Math.round(usage.cacheHitRate * 100);
|
|
29
|
+
const isBad = verdict.level === "bad";
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={`rounded-md border px-2.5 py-2 text-[11px] ${
|
|
34
|
+
isBad ? "border-warning/40 bg-warning/10" : "border-border bg-surface"
|
|
35
|
+
}`}
|
|
36
|
+
>
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => setOpen((v) => !v)}
|
|
39
|
+
aria-expanded={open}
|
|
40
|
+
className="flex w-full items-start gap-2 py-1 text-left text-text-secondary transition-colors hover:text-text-primary"
|
|
41
|
+
>
|
|
42
|
+
<TriangleAlert
|
|
43
|
+
className={`mt-px size-3.5 shrink-0 ${isBad ? "text-warning" : "text-text-subtle"}`}
|
|
44
|
+
/>
|
|
45
|
+
<span className="flex-1">
|
|
46
|
+
Costly turn — {hitPct}% of a {fmtTokens(prefix)}-token transcript came from cache,
|
|
47
|
+
so it cost about{" "}
|
|
48
|
+
<strong className="font-semibold">{multiplier.toFixed(1)}x</strong> a fully cached
|
|
49
|
+
turn.
|
|
50
|
+
</span>
|
|
51
|
+
<ChevronDown
|
|
52
|
+
className={`mt-px size-3.5 shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
|
|
53
|
+
/>
|
|
54
|
+
</button>
|
|
55
|
+
|
|
56
|
+
{open && (
|
|
57
|
+
<div className="mt-2 flex flex-col gap-2 border-t border-border pt-2">
|
|
58
|
+
{verdict.reason && <p className="text-text-secondary">{verdict.reason}</p>}
|
|
59
|
+
<dl className="grid grid-cols-2 gap-x-3 gap-y-1 font-mono text-[10.5px] text-text-subtle">
|
|
60
|
+
<Row label="From cache" value={`${fmtTokens(usage.cacheReadTokens)} (${hitPct}%)`} />
|
|
61
|
+
<Row label="Written to cache" value={fmtTokens(usage.cacheWriteTokens)} />
|
|
62
|
+
<Row label="Fresh input" value={fmtTokens(usage.inputTokens)} />
|
|
63
|
+
<Row label="Output" value={fmtTokens(usage.outputTokens)} />
|
|
64
|
+
<Row label="Model" value={usage.model || "unknown"} />
|
|
65
|
+
<Row label="Cost" value={`$${usage.costUsd.toFixed(4)}`} />
|
|
66
|
+
</dl>
|
|
67
|
+
<p className="text-text-subtle">
|
|
68
|
+
A turn stays cached while the session keeps running. Closing the last tab of an idle
|
|
69
|
+
session, switching model, or a retry all restart it, and the next turn re-sends the
|
|
70
|
+
transcript at full price.
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function Row({ label, value }: { label: string; value: string }) {
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<dt className="truncate">{label}</dt>
|
|
82
|
+
<dd className="truncate text-right text-text-secondary">{value}</dd>
|
|
83
|
+
</>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -554,6 +554,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
554
554
|
const finalEvents = [...streamingEventsRef.current];
|
|
555
555
|
const finalAccount = streamingAccountRef.current;
|
|
556
556
|
const doneUuid = ev.lastMessageUuid as string | undefined;
|
|
557
|
+
const doneUsage = ev.usage;
|
|
557
558
|
setMessages((prev) => {
|
|
558
559
|
const last = prev[prev.length - 1];
|
|
559
560
|
if (last?.role === "assistant") {
|
|
@@ -563,6 +564,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
563
564
|
content: finalContent || last.content,
|
|
564
565
|
events: finalEvents.length > 0 ? finalEvents : last.events,
|
|
565
566
|
...(doneUuid && { sdkUuid: doneUuid }),
|
|
567
|
+
...(doneUsage && { usage: doneUsage }),
|
|
566
568
|
}];
|
|
567
569
|
}
|
|
568
570
|
// No assistant message flushed yet (rAF was still pending when cancelled).
|
|
@@ -575,6 +577,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
575
577
|
events: finalEvents,
|
|
576
578
|
timestamp: new Date().toISOString(),
|
|
577
579
|
...(doneUuid && { sdkUuid: doneUuid }),
|
|
580
|
+
...(doneUsage && { usage: doneUsage }),
|
|
578
581
|
...finalAccount,
|
|
579
582
|
}];
|
|
580
583
|
}
|
|
@@ -12,7 +12,11 @@ interface UseUsageReturn {
|
|
|
12
12
|
refreshUsage: () => void;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* `sessionId` scopes the reported account to this session's binding. Without it the header
|
|
17
|
+
* shows whichever account ran last across every open session, which is wrong for all but one.
|
|
18
|
+
*/
|
|
19
|
+
export function useUsage(projectName: string, providerId = "claude", sessionId?: string): UseUsageReturn {
|
|
16
20
|
const [usageInfo, setUsageInfo] = useState<UsageInfo>({});
|
|
17
21
|
const [usageLoading, setUsageLoading] = useState(false);
|
|
18
22
|
const [lastFetchedAt, setLastFetchedAt] = useState<string | null>(null);
|
|
@@ -22,11 +26,12 @@ export function useUsage(projectName: string, providerId = "claude"): UseUsageRe
|
|
|
22
26
|
if (!projectName) return;
|
|
23
27
|
setUsageLoading(true);
|
|
24
28
|
const qs = forceRefresh ? "&refresh=1" : "";
|
|
29
|
+
const sessionQs = sessionId ? `&session=${encodeURIComponent(sessionId)}` : "";
|
|
25
30
|
// Via api.get, not raw fetch: the toolbar's loading state is gated on this
|
|
26
31
|
// settling, and a raw fetch has no timeout to stop it stalling forever.
|
|
27
32
|
api
|
|
28
33
|
.get<(UsageInfo & { lastFetchedAt?: string }) | null>(
|
|
29
|
-
`${projectUrl(projectName)}/chat/usage?providerId=${providerId}${qs}`,
|
|
34
|
+
`${projectUrl(projectName)}/chat/usage?providerId=${providerId}${sessionQs}${qs}`,
|
|
30
35
|
)
|
|
31
36
|
.then((data) => {
|
|
32
37
|
if (!data) return;
|
|
@@ -35,7 +40,7 @@ export function useUsage(projectName: string, providerId = "claude"): UseUsageRe
|
|
|
35
40
|
})
|
|
36
41
|
.catch(() => {})
|
|
37
42
|
.finally(() => setUsageLoading(false));
|
|
38
|
-
}, [projectName, providerId]);
|
|
43
|
+
}, [projectName, providerId, sessionId]);
|
|
39
44
|
|
|
40
45
|
// Read cache on mount + auto-read every POLL_INTERVAL
|
|
41
46
|
useEffect(() => {
|