@hienlh/ppm 0.13.58 → 0.13.60

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 (38) hide show
  1. package/CHANGELOG.md +7 -2
  2. package/assets/skills/ppm/SKILL.md +1 -1
  3. package/assets/skills/ppm/references/http-api.md +1 -1
  4. package/dist/web/assets/{audio-preview-DEiWicib.js → audio-preview-DPUWgkFM.js} +1 -1
  5. package/dist/web/assets/{chat-tab-cTNzjeke.js → chat-tab-jeH2KOyI.js} +3 -3
  6. package/dist/web/assets/{code-editor-DfI-ziOx.js → code-editor-D689bNqg.js} +2 -2
  7. package/dist/web/assets/{conflict-editor-D3O6LScu.js → conflict-editor-BBLBwDP3.js} +1 -1
  8. package/dist/web/assets/{database-viewer--56cGHzb.js → database-viewer-Dn_XmPuG.js} +1 -1
  9. package/dist/web/assets/{diff-viewer-DcodJCRl.js → diff-viewer-BawweIzs.js} +1 -1
  10. package/dist/web/assets/{extension-webview-D4B6-SMy.js → extension-webview-CnW4FmbT.js} +1 -1
  11. package/dist/web/assets/{git-log-panel-DNzozRgD.js → git-log-panel-DOtefVWL.js} +1 -1
  12. package/dist/web/assets/{glide-data-grid-DNkYYw65.js → glide-data-grid-ckYYsHxU.js} +1 -1
  13. package/dist/web/assets/{image-preview-Z_2ryPZN.js → image-preview-vivCL0Rz.js} +1 -1
  14. package/dist/web/assets/{index-BD-x9C-z.js → index-tXhZwam6.js} +3 -3
  15. package/dist/web/assets/keybindings-store-BPLIbuiX.js +1 -0
  16. package/dist/web/assets/{markdown-renderer-CyYPNwWJ.js → markdown-renderer-BiLohd4E.js} +1 -1
  17. package/dist/web/assets/notification-store-DfPnCRYc.js +1 -0
  18. package/dist/web/assets/pdf-preview-CZxInEra.js +1 -0
  19. package/dist/web/assets/{port-forwarding-tab-Do8vqJWR.js → port-forwarding-tab-cEuRXpkT.js} +1 -1
  20. package/dist/web/assets/{postgres-viewer-Caj1bdFn.js → postgres-viewer-DotG-n6G.js} +1 -1
  21. package/dist/web/assets/{settings-tab-Dd85LS_2.js → settings-tab-DCNSHpkk.js} +1 -1
  22. package/dist/web/assets/{sql-query-editor-KIv3CAg8.js → sql-query-editor-wMM7vxn4.js} +1 -1
  23. package/dist/web/assets/{sqlite-viewer-CoeXNWfR.js → sqlite-viewer-fczUQ3pm.js} +1 -1
  24. package/dist/web/assets/{system-monitor-tab-Dax2F-1U.js → system-monitor-tab-DnjKZ6kX.js} +1 -1
  25. package/dist/web/assets/{terminal-tab-DVeEspLf.js → terminal-tab-4VTsRAv_.js} +1 -1
  26. package/dist/web/assets/{video-preview-T9iHZknc.js → video-preview-Aabd59Fv.js} +1 -1
  27. package/dist/web/index.html +1 -1
  28. package/dist/web/sw.js +1 -1
  29. package/package.json +1 -1
  30. package/src/index.ts +0 -0
  31. package/src/providers/claude-agent-sdk.ts +55 -15
  32. package/src/server/middleware/auth.ts +9 -6
  33. package/src/web/components/editor/pdf-preview.tsx +35 -25
  34. package/bun.lock +0 -2129
  35. package/bunfig.toml +0 -2
  36. package/dist/web/assets/keybindings-store-CtCmu4r4.js +0 -1
  37. package/dist/web/assets/notification-store-g7oIcSzX.js +0 -1
  38. package/dist/web/assets/pdf-preview-DP53VE-6.js +0 -1
@@ -102,6 +102,10 @@ interface StreamingSession {
102
102
  meta: Session;
103
103
  query: any;
104
104
  controller: MessageController;
105
+ /** Latest user message content — updated on follow-ups for accurate retry */
106
+ lastUserContent: string;
107
+ /** Latest user message images — updated on follow-ups for accurate retry */
108
+ lastUserImages?: Array<{ data: string; mediaType: string }>;
105
109
  }
106
110
 
107
111
  /**
@@ -550,6 +554,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
550
554
  session_id: sessionId,
551
555
  priority: opts?.priority ?? 'next',
552
556
  });
557
+ // Track latest message for retry paths (fixes stale firstMsg bug)
558
+ ss.lastUserContent = content;
559
+ ss.lastUserImages = opts?.images;
553
560
  console.log(`[sdk] pushMessage: session=${sessionId} priority=${opts?.priority ?? 'next'}`);
554
561
  }
555
562
 
@@ -607,6 +614,9 @@ export class ClaudeAgentSdkProvider implements AIProvider {
607
614
  session_id: sessionId,
608
615
  priority: opts?.priority ?? 'next',
609
616
  });
617
+ // Track latest message for retry paths (fixes stale firstMsg bug)
618
+ existingStream.lastUserContent = message;
619
+ existingStream.lastUserImages = opts?.images;
610
620
  console.log(`[sdk] sendMessage follow-up: session=${sessionId} pushed to generator`);
611
621
  return; // Events flow through first-message's consumer loop
612
622
  }
@@ -864,8 +874,32 @@ export class ClaudeAgentSdkProvider implements AIProvider {
864
874
  session_id: sessionId,
865
875
  };
866
876
 
877
+ // Build a retry message from the LATEST user content (not the stale firstMsg).
878
+ // Follow-ups via pushMessage update lastUserContent on the streaming session,
879
+ // so retries after token refresh correctly replay the current turn.
880
+ // Also returns the raw content/images for re-populating the new streaming session.
881
+ const buildRetryMsg = () => {
882
+ const ss = this.streamingSessions.get(sessionId);
883
+ const content = ss?.lastUserContent ?? message;
884
+ const images = ss?.lastUserImages;
885
+ return {
886
+ msg: {
887
+ type: 'user' as const,
888
+ message: buildMessageParam(content, images),
889
+ parent_tool_use_id: null,
890
+ session_id: sessionId,
891
+ },
892
+ lastUserContent: content,
893
+ lastUserImages: images,
894
+ };
895
+ };
896
+
867
897
  const { generator: streamGen, controller: initialCtrl } = createMessageChannel();
868
- initialCtrl.push(firstMsg);
898
+ // On crash retry, use buildRetryMsg to get the latest user message (not the stale firstMsg)
899
+ const initRetry = crashRetryCount > 0 ? buildRetryMsg() : null;
900
+ initialCtrl.push(initRetry?.msg ?? firstMsg);
901
+ const initContent = initRetry?.lastUserContent ?? message;
902
+ const initImages = initRetry?.lastUserImages ?? opts?.images;
869
903
 
870
904
  const initialQuery = query({
871
905
  prompt: streamGen,
@@ -875,7 +909,7 @@ export class ClaudeAgentSdkProvider implements AIProvider {
875
909
  canUseTool,
876
910
  } as any,
877
911
  });
878
- this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl });
912
+ this.streamingSessions.set(sessionId, { meta, query: initialQuery, controller: initialCtrl, lastUserContent: initContent, lastUserImages: initImages });
879
913
  this.activeQueries.set(sessionId, initialQuery);
880
914
  let eventSource: AsyncIterable<any> = initialQuery;
881
915
  console.log(`[sdk] session=${sessionId} query() created, waiting for first SDK event...`);
@@ -924,16 +958,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
924
958
  retryCount++;
925
959
  console.warn(`[sdk] transient error on first event — retrying (attempt ${retryCount}/${MAX_RETRIES})`);
926
960
  // Close current streaming session (uses streamingSessions, not stale closure refs)
961
+ const retry1 = buildRetryMsg();
927
962
  closeCurrentStream();
928
963
  const { generator: retryGen, controller: retryCtrl } = createMessageChannel();
929
- retryCtrl.push(firstMsg);
964
+ retryCtrl.push(retry1.msg);
930
965
  // Retry with resume (safe even if JSONL doesn't exist yet — SDK handles gracefully)
931
966
  const retryOpts = { ...queryOptions, sessionId: undefined, resume: sessionId };
932
967
  const rq = query({
933
968
  prompt: retryGen,
934
969
  options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
935
970
  });
936
- this.streamingSessions.set(sessionId, { meta, query: rq, controller: retryCtrl });
971
+ this.streamingSessions.set(sessionId, { meta, query: rq, controller: retryCtrl, lastUserContent: retry1.lastUserContent, lastUserImages: retry1.lastUserImages });
937
972
  this.activeQueries.set(sessionId, rq);
938
973
  eventSource = rq;
939
974
  continue retryLoop;
@@ -994,17 +1029,18 @@ export class ClaudeAgentSdkProvider implements AIProvider {
994
1029
  authRetryCount = recovered.newRetryCount;
995
1030
  account = recovered.account;
996
1031
  const retryEnv = this.buildQueryEnv(meta.projectPath, account);
1032
+ const retry2 = buildRetryMsg();
997
1033
  closeCurrentStream();
998
1034
  const { generator: earlyAuthGen, controller: earlyAuthCtrl } = createMessageChannel();
999
- // Always re-push firstMsg — SDK needs a user message from the generator
1035
+ // Re-push current turn's message — SDK needs a user message from the generator
1000
1036
  // even with resume (resume loads JSONL history, generator provides current turn)
1001
- earlyAuthCtrl.push(firstMsg);
1037
+ earlyAuthCtrl.push(retry2.msg);
1002
1038
  const retryOpts = { ...queryOptions, sessionId: undefined, resume: sessionId, env: retryEnv };
1003
1039
  const rq = query({
1004
1040
  prompt: earlyAuthGen,
1005
1041
  options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
1006
1042
  });
1007
- this.streamingSessions.set(sessionId, { meta, query: rq, controller: earlyAuthCtrl });
1043
+ this.streamingSessions.set(sessionId, { meta, query: rq, controller: earlyAuthCtrl, lastUserContent: retry2.lastUserContent, lastUserImages: retry2.lastUserImages });
1008
1044
  this.activeQueries.set(sessionId, rq);
1009
1045
  eventSource = rq;
1010
1046
  continue retryLoop;
@@ -1151,15 +1187,16 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1151
1187
  authRetryCount = recovered.newRetryCount;
1152
1188
  account = recovered.account;
1153
1189
  const retryEnv = this.buildQueryEnv(meta.projectPath, account);
1190
+ const retry3 = buildRetryMsg();
1154
1191
  closeCurrentStream();
1155
1192
  const { generator: authRetryGen, controller: authRetryCtrl } = createMessageChannel();
1156
- authRetryCtrl.push(firstMsg);
1193
+ authRetryCtrl.push(retry3.msg);
1157
1194
  const retryOpts = { ...queryOptions, sessionId: undefined, resume: sessionId, env: retryEnv };
1158
1195
  const rq = query({
1159
1196
  prompt: authRetryGen,
1160
1197
  options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
1161
1198
  });
1162
- this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl });
1199
+ this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl, lastUserContent: retry3.lastUserContent, lastUserImages: retry3.lastUserImages });
1163
1200
  this.activeQueries.set(sessionId, rq);
1164
1201
  eventSource = rq;
1165
1202
  continue retryLoop;
@@ -1189,16 +1226,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1189
1226
  yield { type: "error", message: `Rate limited. Auto-retrying in ${backoff / 1000}s... (${rateLimitRetryCount}/${MAX_RATE_LIMIT_RETRIES})` };
1190
1227
  await new Promise((r) => setTimeout(r, backoff));
1191
1228
  // Close current streaming session and recreate with (potentially new) account env.
1229
+ const retry4 = buildRetryMsg();
1192
1230
  closeCurrentStream();
1193
1231
  const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
1194
1232
  const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
1195
- rlRetryCtrl.push(firstMsg);
1233
+ rlRetryCtrl.push(retry4.msg);
1196
1234
  const retryOpts = { ...queryOptions, sessionId: undefined, resume: sessionId, env: rlRetryEnv };
1197
1235
  const rq = query({
1198
1236
  prompt: rlRetryGen,
1199
1237
  options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
1200
1238
  });
1201
- this.streamingSessions.set(sessionId, { meta, query: rq, controller: rlRetryCtrl });
1239
+ this.streamingSessions.set(sessionId, { meta, query: rq, controller: rlRetryCtrl, lastUserContent: retry4.lastUserContent, lastUserImages: retry4.lastUserImages });
1202
1240
  this.activeQueries.set(sessionId, rq);
1203
1241
  eventSource = rq;
1204
1242
  continue retryLoop;
@@ -1284,16 +1322,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1284
1322
  }
1285
1323
  yield { type: "error", message: `Rate limited. Auto-retrying in ${backoff / 1000}s... (${rateLimitRetryCount}/${MAX_RATE_LIMIT_RETRIES})` };
1286
1324
  await new Promise((r) => setTimeout(r, backoff));
1325
+ const retry5 = buildRetryMsg();
1287
1326
  closeCurrentStream();
1288
1327
  const rlRetryEnv = this.buildQueryEnv(meta.projectPath, account);
1289
1328
  const { generator: rlRetryGen, controller: rlRetryCtrl } = createMessageChannel();
1290
- rlRetryCtrl.push(firstMsg);
1329
+ rlRetryCtrl.push(retry5.msg);
1291
1330
  const retryOpts = { ...queryOptions, sessionId: undefined, resume: sessionId, env: rlRetryEnv };
1292
1331
  const rq = query({
1293
1332
  prompt: rlRetryGen,
1294
1333
  options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
1295
1334
  });
1296
- this.streamingSessions.set(sessionId, { meta, query: rq, controller: rlRetryCtrl });
1335
+ this.streamingSessions.set(sessionId, { meta, query: rq, controller: rlRetryCtrl, lastUserContent: retry5.lastUserContent, lastUserImages: retry5.lastUserImages });
1297
1336
  this.activeQueries.set(sessionId, rq);
1298
1337
  eventSource = rq;
1299
1338
  continue retryLoop;
@@ -1312,16 +1351,17 @@ export class ClaudeAgentSdkProvider implements AIProvider {
1312
1351
  if (recovered) {
1313
1352
  authRetryCount = recovered.newRetryCount;
1314
1353
  account = recovered.account;
1354
+ const retry6 = buildRetryMsg();
1315
1355
  closeCurrentStream();
1316
1356
  const retryEnv = this.buildQueryEnv(meta.projectPath, account);
1317
1357
  const { generator: authRetryGen2, controller: authRetryCtrl2 } = createMessageChannel();
1318
- authRetryCtrl2.push(firstMsg);
1358
+ authRetryCtrl2.push(retry6.msg);
1319
1359
  const retryOpts = { ...queryOptions, sessionId: undefined, resume: sessionId, env: retryEnv };
1320
1360
  const rq = query({
1321
1361
  prompt: authRetryGen2,
1322
1362
  options: { ...retryOpts, ...(permissionHooks && { hooks: permissionHooks }), canUseTool } as any,
1323
1363
  });
1324
- this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl2 });
1364
+ this.streamingSessions.set(sessionId, { meta, query: rq, controller: authRetryCtrl2, lastUserContent: retry6.lastUserContent, lastUserImages: retry6.lastUserImages });
1325
1365
  this.activeQueries.set(sessionId, rq);
1326
1366
  eventSource = rq;
1327
1367
  continue retryLoop;
@@ -25,12 +25,15 @@ export async function authMiddleware(c: Context, next: Next) {
25
25
  }
26
26
  }
27
27
 
28
- // Fallback: ?token= query param for SSE/EventSource (can't set custom headers)
29
- // Scoped to /stream paths only to avoid leaking token in logs/referer on all GET routes
30
- if (c.req.method === "GET" && c.req.path.endsWith("/stream")) {
31
- const queryToken = c.req.query("token");
32
- if (queryToken && queryToken === authConfig.token) {
33
- return next();
28
+ // Fallback: ?token= query param for SSE/EventSource & iframe embeds (can't set custom headers)
29
+ // Scoped to /stream and /files/raw paths to avoid leaking token on all GET routes
30
+ if (c.req.method === "GET") {
31
+ const p = c.req.path;
32
+ if (p.endsWith("/stream") || p.endsWith("/files/raw") || p === "/api/fs/raw") {
33
+ const queryToken = c.req.query("token");
34
+ if (queryToken && queryToken === authConfig.token) {
35
+ return next();
36
+ }
34
37
  }
35
38
  }
36
39
 
@@ -1,35 +1,36 @@
1
- import { useCallback, useEffect, useRef, useState } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Loader2, FileWarning, ExternalLink } from "lucide-react";
3
- import { useBlobUrl } from "./use-blob-url";
3
+ import { projectUrl, getAuthToken } from "@/lib/api-client";
4
4
 
5
5
  export function PdfPreview({ filePath, projectName }: { filePath: string; projectName: string }) {
6
- const [refreshKey, setRefreshKey] = useState(0);
7
6
  const iframeRef = useRef<HTMLIFrameElement>(null);
8
- const pageHashRef = useRef("");
7
+ const [loaded, setLoaded] = useState(false);
8
+ const [error, setError] = useState(false);
9
9
 
10
- const { blobUrl, error } = useBlobUrl(filePath, projectName, "application/pdf", refreshKey);
10
+ // Build stable direct URL (no blob) so reload() preserves scroll
11
+ const iframeSrc = useMemo(() => {
12
+ const isExternal = /^(\/|[A-Za-z]:[/\\])/.test(filePath);
13
+ const base = isExternal
14
+ ? `/api/fs/raw?path=${encodeURIComponent(filePath)}`
15
+ : `${projectUrl(projectName)}/files/raw?path=${encodeURIComponent(filePath)}`;
16
+ const token = getAuthToken();
17
+ return token ? `${base}&token=${encodeURIComponent(token)}` : base;
18
+ }, [filePath, projectName]);
11
19
 
12
- // Auto-reload: listen for file:changed WS events
20
+ // Auto-reload on file change — reload() preserves browser PDF viewer scroll
13
21
  useEffect(() => {
14
22
  const handler = (e: Event) => {
15
23
  const detail = (e as CustomEvent).detail;
16
24
  if (detail.projectName !== projectName || detail.path !== filePath) return;
17
- // Save current page hash before re-fetch (Chrome PDF viewer uses #page=N&zoom=...)
18
25
  try {
19
- pageHashRef.current = iframeRef.current?.contentWindow?.location.hash || "";
20
- } catch { /* cross-origin */ }
21
- setRefreshKey((k) => k + 1);
26
+ iframeRef.current?.contentWindow?.location.reload();
27
+ } catch { /* cross-origin fallback — shouldn't happen for same-origin */ }
22
28
  };
23
29
  window.addEventListener("file:changed", handler);
24
30
  return () => window.removeEventListener("file:changed", handler);
25
31
  }, [filePath, projectName]);
26
32
 
27
- const openInNewTab = useCallback(() => { if (blobUrl) window.open(blobUrl, "_blank"); }, [blobUrl]);
28
-
29
- // Append saved page hash to blob URL so PDF viewer restores position
30
- const iframeSrc = blobUrl
31
- ? `${blobUrl}${pageHashRef.current}`
32
- : undefined;
33
+ const openInNewTab = useCallback(() => { window.open(iframeSrc, "_blank"); }, [iframeSrc]);
33
34
 
34
35
  if (error) {
35
36
  return (
@@ -39,18 +40,27 @@ export function PdfPreview({ filePath, projectName }: { filePath: string; projec
39
40
  </div>
40
41
  );
41
42
  }
42
- if (!blobUrl) {
43
- return <div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>;
44
- }
45
43
  return (
46
44
  <div className="flex flex-col h-full">
47
- <div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-background shrink-0">
48
- <span className="text-xs text-text-secondary truncate">{filePath}</span>
49
- <button onClick={openInNewTab} className="flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary transition-colors">
50
- <ExternalLink className="size-3" /> Open in new tab
51
- </button>
45
+ {!loaded && (
46
+ <div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>
47
+ )}
48
+ <div className={`flex flex-col h-full ${loaded ? "" : "hidden"}`}>
49
+ <div className="flex items-center justify-between px-3 py-1.5 border-b border-border bg-background shrink-0">
50
+ <span className="text-xs text-text-secondary truncate">{filePath}</span>
51
+ <button onClick={openInNewTab} className="flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary transition-colors">
52
+ <ExternalLink className="size-3" /> Open in new tab
53
+ </button>
54
+ </div>
55
+ <iframe
56
+ ref={iframeRef}
57
+ src={iframeSrc}
58
+ title={filePath}
59
+ className="flex-1 w-full border-none"
60
+ onLoad={() => setLoaded(true)}
61
+ onError={() => setError(true)}
62
+ />
52
63
  </div>
53
- <iframe ref={iframeRef} src={iframeSrc} title={filePath} className="flex-1 w-full border-none" />
54
64
  </div>
55
65
  );
56
66
  }