@hienlh/ppm 0.17.43 → 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.
Files changed (63) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/cli-reference.md +4 -4
  4. package/assets/skills/ppm/references/http-api.md +1 -1
  5. package/dist/web/assets/{ai-resource-editor-CQWFrDeG.js → ai-resource-editor-bKRP47RQ.js} +1 -1
  6. package/dist/web/assets/{audio-preview-Cyb8SLCu.js → audio-preview-DbvnEkRv.js} +1 -1
  7. package/dist/web/assets/chat-tab-B7M7IENk.js +13 -0
  8. package/dist/web/assets/{code-editor-DTqFVL_2.js → code-editor-CyDEePNK.js} +2 -2
  9. package/dist/web/assets/{conflict-editor-BFiY6VyT.js → conflict-editor-BDCVX1rJ.js} +1 -1
  10. package/dist/web/assets/{csv-preview-DpeN7hH8.js → csv-preview-DVkvCs_r.js} +1 -1
  11. package/dist/web/assets/{database-viewer-D9OT6V9B.js → database-viewer-DBhqh6Tz.js} +1 -1
  12. package/dist/web/assets/{diff-viewer-LqH4ARjy.js → diff-viewer-Cz3oQoYm.js} +1 -1
  13. package/dist/web/assets/{docx-preview-Dqzlw47y.js → docx-preview-Dyq1oJUo.js} +1 -1
  14. package/dist/web/assets/{extension-webview-DsNuZtJO.js → extension-webview-Cm1IeJIL.js} +1 -1
  15. package/dist/web/assets/{git-log-panel-mcn-Atil.js → git-log-panel-CLuZ_pOz.js} +1 -1
  16. package/dist/web/assets/{glide-data-grid-CyZAiAgS.js → glide-data-grid-Br1FTMc6.js} +1 -1
  17. package/dist/web/assets/{group-chat-tab-CRkPysZw.js → group-chat-tab-DammsouM.js} +1 -1
  18. package/dist/web/assets/{image-preview-DnNyBpmU.js → image-preview-CtOwlF9d.js} +1 -1
  19. package/dist/web/assets/{index-D4KFhwmL.js → index-DD6twTiS.js} +3 -3
  20. package/dist/web/assets/index-DxvtmNdJ.css +2 -0
  21. package/dist/web/assets/{keybindings-store-CtrEuZpU.js → keybindings-store-BzKxDV_N.js} +1 -1
  22. package/dist/web/assets/{markdown-renderer-DDFElf1k.js → markdown-renderer-CknfFAEU.js} +1 -1
  23. package/dist/web/assets/{markdown-renderer-BFwR1QYD.js → markdown-renderer-MZwGAeBK.js} +1 -1
  24. package/dist/web/assets/{notification-store-BTo6li6s.js → notification-store-CHFDRvsf.js} +1 -1
  25. package/dist/web/assets/{pdf-preview-DDRKbO4z.js → pdf-preview-DSitNXFC.js} +1 -1
  26. package/dist/web/assets/{postgres-viewer-0UqxqUYu.js → postgres-viewer-f91idZlH.js} +1 -1
  27. package/dist/web/assets/{settings-tab-0VKOVbt2.js → settings-tab-C-4pUp32.js} +1 -1
  28. package/dist/web/assets/{sql-query-editor-Cf76aKfw.js → sql-query-editor-BdS6aIkt.js} +1 -1
  29. package/dist/web/assets/{sqlite-viewer-CO6dK1CU.js → sqlite-viewer-BVBZkOPi.js} +1 -1
  30. package/dist/web/assets/{system-monitor-tab-BFBwiME3.js → system-monitor-tab-D9CDMKZJ.js} +1 -1
  31. package/dist/web/assets/{terminal-tab-lu0ZsZmQ.js → terminal-tab-Bij4L1XF.js} +1 -1
  32. package/dist/web/assets/{tool-cards-CMQowijv.js → tool-cards-Dg6f_lKl.js} +2 -2
  33. package/dist/web/assets/{use-monaco-theme-DU3LWaPu.js → use-monaco-theme-PSDuGfjN.js} +1 -1
  34. package/dist/web/assets/{video-preview-CHUzaq4s.js → video-preview-oW5BA0vt.js} +1 -1
  35. package/dist/web/index.html +2 -2
  36. package/dist/web/sw.js +1 -1
  37. package/package.json +1 -1
  38. package/scripts/check-ppm-dir-usage.sh +0 -0
  39. package/scripts/install.sh +0 -0
  40. package/scripts/release.sh +0 -0
  41. package/src/cli/commands/stop.ts +6 -3
  42. package/src/providers/claude-agent-sdk.ts +45 -1
  43. package/src/server/routes/chat.ts +38 -1
  44. package/src/server/ws/chat.ts +2 -1
  45. package/src/services/autostart-generator.ts +5 -5
  46. package/src/services/autostart-register.ts +14 -0
  47. package/src/services/db.service.ts +77 -1
  48. package/src/services/fs-browse.service.ts +7 -6
  49. package/src/services/ppm-dir.ts +16 -0
  50. package/src/services/supervisor.ts +87 -34
  51. package/src/services/windows-process-tree.ts +118 -4
  52. package/src/shared/turn-usage.ts +216 -0
  53. package/src/types/chat.ts +3 -1
  54. package/src/web/components/chat/message-list.tsx +5 -0
  55. package/src/web/components/chat/session-debug-dialog.tsx +113 -0
  56. package/src/web/components/chat/turn-cost-warning.tsx +85 -0
  57. package/src/web/hooks/use-chat.ts +3 -0
  58. package/dist/web/assets/chat-tab-uBmb2rFB.js +0 -13
  59. package/dist/web/assets/index-BEB87kj8.css +0 -2
  60. package/packages/vscode-compat/node_modules/.bin/tsc.bunx +0 -0
  61. package/packages/vscode-compat/node_modules/.bin/tsc.exe +0 -0
  62. package/packages/vscode-compat/node_modules/.bin/tsserver.bunx +0 -0
  63. 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 &amp; 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 &amp; 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
  }