@hienlh/ppm 0.17.44 → 0.17.45
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 +7 -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-bKRP47RQ.js} +1 -1
- package/dist/web/assets/{audio-preview-Cyb8SLCu.js → audio-preview-DbvnEkRv.js} +1 -1
- package/dist/web/assets/chat-tab-B7M7IENk.js +13 -0
- package/dist/web/assets/{code-editor-DTqFVL_2.js → code-editor-CyDEePNK.js} +2 -2
- package/dist/web/assets/{conflict-editor-BFiY6VyT.js → conflict-editor-BDCVX1rJ.js} +1 -1
- package/dist/web/assets/{csv-preview-DpeN7hH8.js → csv-preview-DVkvCs_r.js} +1 -1
- package/dist/web/assets/{database-viewer-D9OT6V9B.js → database-viewer-DBhqh6Tz.js} +1 -1
- package/dist/web/assets/{diff-viewer-LqH4ARjy.js → diff-viewer-Cz3oQoYm.js} +1 -1
- package/dist/web/assets/{docx-preview-Dqzlw47y.js → docx-preview-Dyq1oJUo.js} +1 -1
- package/dist/web/assets/{extension-webview-DsNuZtJO.js → extension-webview-Cm1IeJIL.js} +1 -1
- package/dist/web/assets/{git-log-panel-mcn-Atil.js → git-log-panel-CLuZ_pOz.js} +1 -1
- package/dist/web/assets/{glide-data-grid-CyZAiAgS.js → glide-data-grid-Br1FTMc6.js} +1 -1
- package/dist/web/assets/{group-chat-tab-CRkPysZw.js → group-chat-tab-DammsouM.js} +1 -1
- package/dist/web/assets/{image-preview-DnNyBpmU.js → image-preview-CtOwlF9d.js} +1 -1
- package/dist/web/assets/{index-D4KFhwmL.js → index-DD6twTiS.js} +3 -3
- package/dist/web/assets/index-DxvtmNdJ.css +2 -0
- package/dist/web/assets/{keybindings-store-CtrEuZpU.js → keybindings-store-BzKxDV_N.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DDFElf1k.js → markdown-renderer-CknfFAEU.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BFwR1QYD.js → markdown-renderer-MZwGAeBK.js} +1 -1
- package/dist/web/assets/{notification-store-BTo6li6s.js → notification-store-CHFDRvsf.js} +1 -1
- package/dist/web/assets/{pdf-preview-DDRKbO4z.js → pdf-preview-DSitNXFC.js} +1 -1
- package/dist/web/assets/{postgres-viewer-0UqxqUYu.js → postgres-viewer-f91idZlH.js} +1 -1
- package/dist/web/assets/{settings-tab-0VKOVbt2.js → settings-tab-C-4pUp32.js} +1 -1
- package/dist/web/assets/{sql-query-editor-Cf76aKfw.js → sql-query-editor-BdS6aIkt.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-CO6dK1CU.js → sqlite-viewer-BVBZkOPi.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-BFBwiME3.js → system-monitor-tab-D9CDMKZJ.js} +1 -1
- package/dist/web/assets/{terminal-tab-lu0ZsZmQ.js → terminal-tab-Bij4L1XF.js} +1 -1
- package/dist/web/assets/{tool-cards-CMQowijv.js → tool-cards-Dg6f_lKl.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-DU3LWaPu.js → use-monaco-theme-PSDuGfjN.js} +1 -1
- package/dist/web/assets/{video-preview-CHUzaq4s.js → video-preview-oW5BA0vt.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- 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 +45 -1
- package/src/server/routes/chat.ts +38 -1
- package/src/server/ws/chat.ts +2 -1
- package/src/services/autostart-generator.ts +5 -5
- package/src/services/autostart-register.ts +14 -0
- package/src/services/db.service.ts +77 -1
- package/src/services/ppm-dir.ts +16 -0
- package/src/services/supervisor.ts +87 -34
- package/src/services/windows-process-tree.ts +118 -4
- package/src/shared/turn-usage.ts +216 -0
- package/src/types/chat.ts +3 -1
- package/src/web/components/chat/message-list.tsx +5 -0
- package/src/web/components/chat/session-debug-dialog.tsx +113 -0
- package/src/web/components/chat/turn-cost-warning.tsx +85 -0
- package/src/web/hooks/use-chat.ts +3 -0
- 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,103 @@ 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
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div className="flex flex-col gap-2">
|
|
216
|
+
<h3 className="text-xs font-semibold text-foreground">Token & cache</h3>
|
|
217
|
+
<p className="text-[11px] text-text-secondary">
|
|
218
|
+
Last {turns.length} turn{turns.length === 1 ? "" : "s"} · {cold} restarted the session ·{" "}
|
|
219
|
+
{fmtTokens(wasted)} of transcript re-sent uncached · ${totalCost.toFixed(2)}
|
|
220
|
+
</p>
|
|
221
|
+
|
|
222
|
+
<div className="max-h-[30vh] overflow-y-auto rounded-md border border-border">
|
|
223
|
+
<table className="w-full font-mono text-[10.5px]">
|
|
224
|
+
<thead className="sticky top-0 bg-surface text-text-subtle">
|
|
225
|
+
<tr>
|
|
226
|
+
<th className="px-2 py-1 text-left font-normal">When</th>
|
|
227
|
+
<th className="px-2 py-1 text-right font-normal">Prefix</th>
|
|
228
|
+
<th className="px-2 py-1 text-right font-normal">Cached</th>
|
|
229
|
+
<th className="px-2 py-1 text-right font-normal">Cost</th>
|
|
230
|
+
<th className="px-2 py-1 text-left font-normal">Start</th>
|
|
231
|
+
</tr>
|
|
232
|
+
</thead>
|
|
233
|
+
<tbody>
|
|
234
|
+
{turns.map((t) => {
|
|
235
|
+
const hit = Math.round(t.usage.cacheHitRate * 100);
|
|
236
|
+
const level = assessTurnCost(t.usage).level;
|
|
237
|
+
return (
|
|
238
|
+
<tr key={t.id} className="border-t border-border">
|
|
239
|
+
<td className="px-2 py-1 text-text-subtle">
|
|
240
|
+
{t.recordedAt.slice(11, 16) || t.recordedAt.slice(0, 10)}
|
|
241
|
+
</td>
|
|
242
|
+
<td className="px-2 py-1 text-right text-text-secondary">
|
|
243
|
+
{fmtTokens(prefixTokens(t.usage))}
|
|
244
|
+
</td>
|
|
245
|
+
<td
|
|
246
|
+
className={`px-2 py-1 text-right ${
|
|
247
|
+
level === "bad" ? "text-warning" : level === "warn" ? "text-text-primary" : "text-success"
|
|
248
|
+
}`}
|
|
249
|
+
>
|
|
250
|
+
{hit}%
|
|
251
|
+
</td>
|
|
252
|
+
<td className="px-2 py-1 text-right text-text-secondary">
|
|
253
|
+
${t.usage.costUsd.toFixed(3)}
|
|
254
|
+
</td>
|
|
255
|
+
<td className="px-2 py-1 text-text-subtle">
|
|
256
|
+
{t.usage.coldStart ? (t.usage.coldReason ?? "cold") : "warm"}
|
|
257
|
+
</td>
|
|
258
|
+
</tr>
|
|
259
|
+
);
|
|
260
|
+
})}
|
|
261
|
+
</tbody>
|
|
262
|
+
</table>
|
|
263
|
+
</div>
|
|
264
|
+
<p className="text-[11px] text-text-subtle">
|
|
265
|
+
A high "Cached" share is the cheap case. A turn marked <code>tab_closed</code> paid for the
|
|
266
|
+
whole transcript again because PPM shut the session's subprocess down when the last tab
|
|
267
|
+
disconnected.
|
|
268
|
+
</p>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
163
273
|
/** Toolbar button opening the session's debug facts and transcript maintenance. */
|
|
164
274
|
export function SessionDebugButton({ sessionId, projectName }: { sessionId: string; projectName: string }) {
|
|
165
275
|
const [open, setOpen] = useState(false);
|
|
@@ -208,6 +318,9 @@ export function SessionDebugButton({ sessionId, projectName }: { sessionId: stri
|
|
|
208
318
|
<Button size="sm" onClick={copy} disabled={!info} className="w-full">
|
|
209
319
|
{copied ? <><ClipboardCheck className="size-3.5" /> Copied!</> : <><Copy className="size-3.5" /> Copy</>}
|
|
210
320
|
</Button>
|
|
321
|
+
<div className="border-t border-border pt-3">
|
|
322
|
+
{open && <TurnUsageHistory sessionId={sessionId} projectName={projectName} />}
|
|
323
|
+
</div>
|
|
211
324
|
<div className="border-t border-border pt-3">
|
|
212
325
|
{open && <TranscriptImages sessionId={sessionId} projectName={projectName} />}
|
|
213
326
|
</div>
|
|
@@ -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
|
}
|