@hienlh/ppm 0.10.3 → 0.10.5

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 (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/web/assets/{ai-settings-section-LMO_cfIW.js → ai-settings-section-D2rONDPd.js} +1 -1
  3. package/dist/web/assets/{api-settings-CoKe_BdR.js → api-settings-C__hxGX2.js} +1 -1
  4. package/dist/web/assets/architecture-PBZL5I3N-DmL1WyG-.js +1 -0
  5. package/dist/web/assets/chat-tab-Dki1pz84.js +10 -0
  6. package/dist/web/assets/code-editor-D3AAT8nI.js +8 -0
  7. package/dist/web/assets/{conflict-editor-C28xnWqp.js → conflict-editor-Bxq4QiW1.js} +3 -3
  8. package/dist/web/assets/{database-viewer-DDGq5efK.js → database-viewer-CvQc1PZH.js} +2 -2
  9. package/dist/web/assets/{diff-viewer-DBELqMy0.js → diff-viewer-x7kjfVYW.js} +1 -1
  10. package/dist/web/assets/extension-webview-BFd0USXC.js +3 -0
  11. package/dist/web/assets/gitGraph-HDMCJU4V-D8vKfkjC.js +1 -0
  12. package/dist/web/assets/index-DPnjO2FY.css +2 -0
  13. package/dist/web/assets/index-DuEUN2Eg.js +26 -0
  14. package/dist/web/assets/info-3K5VOQVL-VG29MIoT.js +1 -0
  15. package/dist/web/assets/{input-CHRMley8.js → input-ClhO__YM.js} +1 -1
  16. package/dist/web/assets/keybindings-store-BkZjvU9J.js +1 -0
  17. package/dist/web/assets/{keybindings-store-D2N-Tq4N.js → keybindings-store-C9KsBH7z.js} +1 -1
  18. package/dist/web/assets/{markdown-renderer-CtsslbMO.js → markdown-renderer-CKmmrUuy.js} +3 -3
  19. package/dist/web/assets/packet-RMMSAZCW-Bl_WpvPc.js +1 -0
  20. package/dist/web/assets/pie-UPGHQEXC-BVpLpAIy.js +1 -0
  21. package/dist/web/assets/{port-forwarding-tab-BszAda9U.js → port-forwarding-tab-BUH9aImG.js} +1 -1
  22. package/dist/web/assets/{postgres-viewer-X2li3HfX.js → postgres-viewer-YkljtDWX.js} +2 -2
  23. package/dist/web/assets/{project-store-Ciq-cK1O.js → project-store-BYmQ0fDC.js} +1 -1
  24. package/dist/web/assets/radar-KQ55EAFF-CJGco43I.js +1 -0
  25. package/dist/web/assets/{scroll-area-DwWF9FpN.js → scroll-area-DW7L4Gnc.js} +1 -1
  26. package/dist/web/assets/settings-store-D9CflsKU.js +2 -0
  27. package/dist/web/assets/settings-tab-DfPjX9uY.js +1 -0
  28. package/dist/web/assets/{sql-query-editor-DZ9xskL8.js → sql-query-editor-CM_qEhaX.js} +1 -1
  29. package/dist/web/assets/{sqlite-viewer-BV0p6qnR.js → sqlite-viewer-f6ZJHIzh.js} +1 -1
  30. package/dist/web/assets/{tab-store-DZbiYk7y.js → tab-store-B3M9hjho.js} +1 -1
  31. package/dist/web/assets/terminal-tab-CVdfvDSK.js +1 -0
  32. package/dist/web/assets/treemap-KZPCXAKY-BsOrObtE.js +1 -0
  33. package/dist/web/assets/{use-monaco-theme-OY18iXNi.js → use-monaco-theme-CvV5vy_F.js} +1 -1
  34. package/dist/web/assets/{vendor-mermaid-B2SLgECS.js → vendor-mermaid-CwOSbfhN.js} +1 -1
  35. package/dist/web/index.html +13 -13
  36. package/dist/web/sw.js +1 -1
  37. package/package.json +1 -1
  38. package/packages/ext-git-graph/src/extension.ts +11 -4
  39. package/packages/ext-git-graph/src/webview-html.ts +22 -9
  40. package/src/server/routes/chat.ts +28 -3
  41. package/src/services/db.service.ts +36 -0
  42. package/src/services/extension-host-worker.ts +3 -2
  43. package/src/services/extension.service.ts +4 -2
  44. package/src/services/slash-discovery/cache.ts +38 -0
  45. package/src/services/slash-discovery/fuzzy-search.ts +12 -3
  46. package/src/services/slash-discovery/index.ts +9 -2
  47. package/src/services/slash-items.service.ts +1 -1
  48. package/src/services/supervisor-state.ts +13 -1
  49. package/src/services/supervisor.ts +4 -3
  50. package/src/web/components/chat/chat-tab.tsx +12 -2
  51. package/src/web/components/chat/message-input.tsx +24 -14
  52. package/src/web/components/chat/message-list.tsx +7 -14
  53. package/src/web/components/chat/slash-command-picker.tsx +120 -46
  54. package/src/web/components/extensions/extension-webview.tsx +7 -3
  55. package/src/web/lib/report-bug.ts +3 -2
  56. package/src/web/lib/ws-client.ts +4 -3
  57. package/src/web/stores/settings-store.ts +7 -3
  58. package/dist/web/assets/architecture-PBZL5I3N-CUZIB1Vq.js +0 -1
  59. package/dist/web/assets/chat-tab-lo46P4ZN.js +0 -10
  60. package/dist/web/assets/code-editor-DN0UiBvk.js +0 -8
  61. package/dist/web/assets/extension-webview--HUG0c_R.js +0 -3
  62. package/dist/web/assets/gitGraph-HDMCJU4V-CtOMUphQ.js +0 -1
  63. package/dist/web/assets/index-BUxaCPPv.js +0 -26
  64. package/dist/web/assets/index-Dq7PPmAk.css +0 -2
  65. package/dist/web/assets/info-3K5VOQVL-BCrPCWGY.js +0 -1
  66. package/dist/web/assets/keybindings-store-C7No6mtl.js +0 -1
  67. package/dist/web/assets/packet-RMMSAZCW-D_OqB-zi.js +0 -1
  68. package/dist/web/assets/pie-UPGHQEXC-WUHpLNJz.js +0 -1
  69. package/dist/web/assets/radar-KQ55EAFF-HQIIecVM.js +0 -1
  70. package/dist/web/assets/settings-store-B470PCWf.js +0 -2
  71. package/dist/web/assets/settings-tab-DR1HhS4C.js +0 -1
  72. package/dist/web/assets/terminal-tab-BiRx6kvn.js +0 -1
  73. package/dist/web/assets/treemap-KZPCXAKY-0wLgUUTz.js +0 -1
  74. /package/dist/web/assets/{api-client-o_6TmLGC.js → api-client-Bn-Pi9k5.js} +0 -0
  75. /package/dist/web/assets/{utils-ChWX7pZv.js → utils-CTg5uAYR.js} +0 -0
@@ -55,7 +55,19 @@ export function updateStatus(patch: Record<string, unknown>) {
55
55
  try {
56
56
  const data = { ...readStatus(), ...patch };
57
57
  atomicWriteJson(STATUS_FILE(), data);
58
- } catch {}
58
+ } catch (e) {
59
+ // Log to stderr so failures are visible in ppm.log
60
+ try { process.stderr.write(`[updateStatus] Failed to write status.json: ${e}\n`); } catch {}
61
+ }
62
+ }
63
+
64
+ /** Full write — replaces entire status.json (use at supervisor startup to clear stale data) */
65
+ export function writeStatus(data: Record<string, unknown>) {
66
+ try {
67
+ atomicWriteJson(STATUS_FILE(), data);
68
+ } catch (e) {
69
+ try { process.stderr.write(`[writeStatus] Failed to write status.json: ${e}\n`); } catch {}
70
+ }
59
71
  }
60
72
 
61
73
  // ─── Command file protocol ─────────────────────────────────────────────
@@ -15,7 +15,7 @@ import { isCompiledBinary } from "./autostart-generator.ts";
15
15
  import {
16
16
  type SupervisorState,
17
17
  getState, setState, waitForResume, triggerResume,
18
- readAndDeleteCmd, readStatus, updateStatus,
18
+ readAndDeleteCmd, readStatus, updateStatus, writeStatus,
19
19
  STATUS_FILE, PID_FILE,
20
20
  } from "./supervisor-state.ts";
21
21
  import { startStoppedPage, stopStoppedPage } from "./supervisor-stopped-page.ts";
@@ -767,11 +767,12 @@ export async function runSupervisor(opts: {
767
767
  log("ERROR", `Unhandled rejection: ${reason}`);
768
768
  });
769
769
 
770
- // Write supervisor PID + clear stale availableVersion from previous run
770
+ // Full write to clear any stale data from previous runs (different port, dead PIDs, etc.)
771
771
  writeFileSync(PID_FILE(), String(process.pid));
772
- updateStatus({
772
+ writeStatus({
773
773
  supervisorPid: process.pid, port: opts.port, host: opts.host, availableVersion: null,
774
774
  state: "running", pausedAt: null, pauseReason: null, lastCrashError: null,
775
+ pid: null, tunnelPid: null, shareUrl: null,
775
776
  });
776
777
 
777
778
  // Build __serve__ args
@@ -38,6 +38,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
38
38
  const [slashFilter, setSlashFilter] = useState("");
39
39
  const [slashRanked, setSlashRanked] = useState(false);
40
40
  const [slashSelected, setSlashSelected] = useState<SlashItem | null>(null);
41
+ const [slashRecentNames, setSlashRecentNames] = useState<string[]>([]);
41
42
 
42
43
  // File picker state
43
44
  const [fileItems, setFileItems] = useState<FileNode[]>([]);
@@ -251,9 +252,10 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
251
252
 
252
253
  /** Stable callback for slash items loaded — prevents MessageInput memo break */
253
254
  const handleSlashItemsLoaded = useCallback(
254
- (items: SlashItem[], ranked?: boolean) => {
255
+ (items: SlashItem[], ranked?: boolean, recentNames?: string[]) => {
255
256
  setSlashItems(items);
256
257
  if (ranked !== undefined) setSlashRanked(ranked);
258
+ if (recentNames) setSlashRecentNames(recentNames);
257
259
  },
258
260
  [],
259
261
  );
@@ -270,7 +272,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
270
272
  setShowSlashPicker(false);
271
273
  setSlashFilter("");
272
274
  setTimeout(() => setSlashSelected(null), 50);
273
- }, []);
275
+ // Record usage for recents (fire-and-forget)
276
+ if (projectName) {
277
+ api.post(`${projectUrl(projectName)}/chat/slash-recents`, { name: item.name, type: item.type }).catch(() => {});
278
+ // Optimistic update: add to front of recents
279
+ setSlashRecentNames((prev) => [item.name, ...prev.filter((n) => n !== item.name)].slice(0, 5));
280
+ }
281
+ }, [projectName]);
274
282
 
275
283
  const handleSlashClose = useCallback(() => {
276
284
  setShowSlashPicker(false);
@@ -404,6 +412,8 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
404
412
  onClose={handleSlashClose}
405
413
  visible={showSlashPicker}
406
414
  ranked={slashRanked}
415
+ recentNames={slashRecentNames}
416
+ projectName={projectName}
407
417
  />
408
418
  <FilePicker
409
419
  items={fileItems}
@@ -32,7 +32,7 @@ interface MessageInputProps {
32
32
  projectName?: string;
33
33
  /** Slash picker state change */
34
34
  onSlashStateChange?: (visible: boolean, filter: string) => void;
35
- onSlashItemsLoaded?: (items: SlashItem[], ranked?: boolean) => void;
35
+ onSlashItemsLoaded?: (items: SlashItem[], ranked?: boolean, recentNames?: string[]) => void;
36
36
  slashSelected?: SlashItem | null;
37
37
  /** File picker state change */
38
38
  onFileStateChange?: (visible: boolean, filter: string) => void;
@@ -169,25 +169,35 @@ export const MessageInput = memo(function MessageInput({
169
169
  setTimeout(() => { getVisibleTextarea()?.focus(); }, 100);
170
170
  }, []); // eslint-disable-line react-hooks/exhaustive-deps
171
171
 
172
- // Fetch slash items when projectName changes
173
- useEffect(() => {
172
+ // Fetch slash items from server
173
+ const fetchSlashItems = useCallback(() => {
174
174
  if (!projectName) {
175
175
  slashItemsRef.current = [];
176
- onSlashItemsLoaded?.([], false);
176
+ onSlashItemsLoaded?.([], false, []);
177
177
  return;
178
178
  }
179
179
  api
180
- .get<SlashItem[]>(`${projectUrl(projectName)}/chat/slash-items`)
181
- .then((items) => {
182
- slashItemsRef.current = items;
180
+ .get<{ items: SlashItem[]; recentNames: string[] }>(`${projectUrl(projectName)}/chat/slash-items`)
181
+ .then((data) => {
182
+ slashItemsRef.current = data.items;
183
183
  slashRankedRef.current = false;
184
- onSlashItemsLoaded?.(items, false);
184
+ onSlashItemsLoaded?.(data.items, false, data.recentNames);
185
185
  })
186
186
  .catch(() => {
187
187
  slashItemsRef.current = [];
188
- onSlashItemsLoaded?.([], false);
188
+ onSlashItemsLoaded?.([], false, []);
189
189
  });
190
- }, [projectName]); // eslint-disable-line react-hooks/exhaustive-deps
190
+ }, [projectName, onSlashItemsLoaded]);
191
+
192
+ // Fetch slash items when projectName changes
193
+ useEffect(() => { fetchSlashItems(); }, [fetchSlashItems]);
194
+
195
+ // Re-fetch when cache is invalidated via refresh button
196
+ useEffect(() => {
197
+ const handler = () => fetchSlashItems();
198
+ window.addEventListener("ppm:slash-items-refresh", handler);
199
+ return () => window.removeEventListener("ppm:slash-items-refresh", handler);
200
+ }, [fetchSlashItems]);
191
201
 
192
202
  // Cleanup debounce timer on unmount
193
203
  useEffect(() => {
@@ -437,12 +447,12 @@ export const MessageInput = memo(function MessageInput({
437
447
  const requestId = ++slashSearchIdRef.current;
438
448
  slashDebounceRef.current = setTimeout(() => {
439
449
  api
440
- .get<SlashItem[]>(`${projectUrl(projectName)}/chat/slash-items?q=${encodeURIComponent(query)}`)
441
- .then((items) => {
450
+ .get<{ items: SlashItem[]; recentNames: string[] }>(`${projectUrl(projectName)}/chat/slash-items?q=${encodeURIComponent(query)}`)
451
+ .then((data) => {
442
452
  if (requestId !== slashSearchIdRef.current) return; // stale response
443
- slashItemsRef.current = items;
453
+ slashItemsRef.current = data.items;
444
454
  slashRankedRef.current = true;
445
- onSlashItemsLoaded?.(items, true);
455
+ onSlashItemsLoaded?.(data.items, true, data.recentNames);
446
456
  })
447
457
  .catch(() => {
448
458
  if (requestId !== slashSearchIdRef.current) return;
@@ -807,21 +807,14 @@ function ThinkingBlock({ content, isStreaming }: { content: string; isStreaming:
807
807
  * When `isStreaming=true`, shows a blinking cursor at the end.
808
808
  */
809
809
  function StreamingText({ content, animate: isStreaming, projectName }: { content: string; animate: boolean; projectName?: string }) {
810
- // During active streaming, render plain text to avoid running 6 heavy markdown
811
- // plugins (remarkGfm, remarkMath, remarkBreaks, rehypeRaw, rehypeKatex, rehypeHighlight)
812
- // on every rAF frame. Full markdown renders once streaming ends.
813
- if (isStreaming) {
814
- const cleaned = stripTeammateMessages(content);
815
- return (
816
- <>
817
- {cleaned && (
818
- <div className="prose prose-invert max-w-none whitespace-pre-wrap break-words select-text">{cleaned}</div>
819
- )}
810
+ return (
811
+ <>
812
+ <MarkdownContent content={content} projectName={projectName} isStreaming={isStreaming} />
813
+ {isStreaming && (
820
814
  <span className="text-text-subtle text-sm animate-pulse">Thinking...</span>
821
- </>
822
- );
823
- }
824
- return <MarkdownContent content={content} projectName={projectName} />;
815
+ )}
816
+ </>
817
+ );
825
818
  }
826
819
 
827
820
  /**
@@ -1,5 +1,6 @@
1
- import { useState, useEffect, useRef, useCallback, type KeyboardEvent } from "react";
2
- import { Sparkles, Terminal, Zap } from "lucide-react";
1
+ import { useState, useEffect, useRef, useCallback, useMemo, type KeyboardEvent } from "react";
2
+ import { Sparkles, Terminal, Zap, RefreshCw, Clock } from "lucide-react";
3
+ import { api, projectUrl } from "@/lib/api-client";
3
4
 
4
5
  export interface SlashItem {
5
6
  type: "skill" | "command" | "builtin";
@@ -19,6 +20,10 @@ interface SlashCommandPickerProps {
19
20
  visible: boolean;
20
21
  /** When true, items are pre-ranked by server — skip client-side filtering */
21
22
  ranked?: boolean;
23
+ /** Recently used item names (most recent first) */
24
+ recentNames?: string[];
25
+ /** Project name for cache invalidation */
26
+ projectName?: string;
22
27
  }
23
28
 
24
29
  export function SlashCommandPicker({
@@ -28,19 +33,46 @@ export function SlashCommandPicker({
28
33
  onClose,
29
34
  visible,
30
35
  ranked,
36
+ recentNames = [],
37
+ projectName,
31
38
  }: SlashCommandPickerProps) {
32
39
  const [selectedIndex, setSelectedIndex] = useState(0);
40
+ const [refreshing, setRefreshing] = useState(false);
33
41
  const listRef = useRef<HTMLDivElement>(null);
34
42
 
35
- const filtered = ranked
36
- ? items
37
- : items.filter((item) => {
38
- const q = filter.toLowerCase();
39
- return (
40
- item.name.toLowerCase().includes(q) ||
41
- item.description.toLowerCase().includes(q)
42
- );
43
- });
43
+ const recentSet = useMemo(() => new Set(recentNames), [recentNames]);
44
+
45
+ // Build display list: when no filter + not ranked, put recents first
46
+ const displayItems = useMemo(() => {
47
+ let base: SlashItem[];
48
+ if (ranked) {
49
+ base = items;
50
+ } else if (filter) {
51
+ const q = filter.toLowerCase();
52
+ base = items.filter(
53
+ (item) => item.name.toLowerCase().includes(q) || item.description.toLowerCase().includes(q),
54
+ );
55
+ } else {
56
+ base = items;
57
+ }
58
+
59
+ // Reorder: recents first when no filter and not server-ranked
60
+ if (!filter && !ranked && recentNames.length > 0) {
61
+ const recents: SlashItem[] = [];
62
+ const rest: SlashItem[] = [];
63
+ for (const item of base) {
64
+ if (recentSet.has(item.name)) recents.push(item);
65
+ else rest.push(item);
66
+ }
67
+ // Sort recents by their order in recentNames (most recent first)
68
+ recents.sort((a, b) => recentNames.indexOf(a.name) - recentNames.indexOf(b.name));
69
+ return { items: [...recents, ...rest], recentCount: recents.length };
70
+ }
71
+ return { items: base, recentCount: 0 };
72
+ }, [items, filter, ranked, recentNames, recentSet]);
73
+
74
+ const filtered = displayItems.items;
75
+ const recentCount = displayItems.recentCount;
44
76
 
45
77
  // Reset selection when filter changes
46
78
  useEffect(() => {
@@ -95,49 +127,91 @@ export function SlashCommandPicker({
95
127
  return () => document.removeEventListener("keydown", handler, true);
96
128
  }, [visible, handleKeyDown]);
97
129
 
130
+ const handleRefresh = useCallback(() => {
131
+ if (!projectName || refreshing) return;
132
+ setRefreshing(true);
133
+ api.del(`${projectUrl(projectName)}/chat/slash-items/cache`)
134
+ .then(() => {
135
+ // Trigger re-fetch by dispatching custom event (MessageInput listens on projectName)
136
+ window.dispatchEvent(new CustomEvent("ppm:slash-items-refresh"));
137
+ })
138
+ .finally(() => setRefreshing(false));
139
+ }, [projectName, refreshing]);
140
+
98
141
  if (!visible || filtered.length === 0) return null;
99
142
 
100
143
  return (
101
144
  <div className="max-h-52 overflow-y-auto border-b border-border bg-surface">
102
145
  <div ref={listRef} className="py-1">
103
- {filtered.map((item, i) => (
104
- <button
105
- key={`${item.type}-${item.name}`}
106
- className={`flex items-start gap-3 w-full px-3 py-2 text-left transition-colors ${
107
- i === selectedIndex
108
- ? "bg-primary/10 text-primary"
109
- : "hover:bg-surface-hover text-text-primary"
110
- }`}
111
- onMouseEnter={() => setSelectedIndex(i)}
112
- onClick={() => onSelect(item)}
113
- >
114
- <span className="shrink-0 mt-0.5">
115
- {item.type === "builtin" ? (
116
- <Zap className="size-4 text-emerald-500" />
117
- ) : item.type === "skill" ? (
118
- <Sparkles className="size-4 text-amber-500" />
119
- ) : (
120
- <Terminal className="size-4 text-blue-500" />
146
+ {filtered.map((item, i) => {
147
+ // Show "Recent" separator before first item, "All" before first non-recent
148
+ const showRecentLabel = recentCount > 0 && i === 0;
149
+ const showAllLabel = recentCount > 0 && i === recentCount;
150
+
151
+ return (
152
+ <div key={`${item.type}-${item.name}`}>
153
+ {showRecentLabel && (
154
+ <div className="flex items-center justify-between px-3 pt-1 pb-0.5">
155
+ <span className="text-[10px] font-medium text-text-subtle uppercase tracking-wider flex items-center gap-1">
156
+ <Clock className="size-3" />
157
+ Recent
158
+ </span>
159
+ {projectName && (
160
+ <button
161
+ type="button"
162
+ onClick={(e) => { e.stopPropagation(); handleRefresh(); }}
163
+ className="text-text-subtle hover:text-text-primary transition-colors p-0.5 rounded"
164
+ title="Refresh skill list"
165
+ aria-label="Refresh skill list"
166
+ >
167
+ <RefreshCw className={`size-3 ${refreshing ? "animate-spin" : ""}`} />
168
+ </button>
169
+ )}
170
+ </div>
121
171
  )}
122
- </span>
123
- <div className="min-w-0 flex-1">
124
- <div className="flex items-baseline gap-2">
125
- <span className="font-medium text-sm">/{item.name}</span>
126
- {item.argumentHint && (
127
- <span className="text-xs text-text-subtle">{item.argumentHint}</span>
128
- )}
129
- <span className="text-xs text-text-subtle capitalize ml-auto">
130
- {item.scope === "bundled" ? "PPM" : item.scope === "user" ? "global" : item.type}
131
- </span>
132
- </div>
133
- {item.description && (
134
- <p className="text-xs text-text-subtle mt-0.5 line-clamp-2">
135
- {item.description}
136
- </p>
172
+ {showAllLabel && (
173
+ <div className="px-3 pt-1.5 pb-0.5">
174
+ <span className="text-[10px] font-medium text-text-subtle uppercase tracking-wider">All</span>
175
+ </div>
137
176
  )}
177
+ <button
178
+ className={`flex items-start gap-3 w-full px-3 py-2 text-left transition-colors ${
179
+ i === selectedIndex
180
+ ? "bg-primary/10 text-primary"
181
+ : "hover:bg-surface-hover text-text-primary"
182
+ }`}
183
+ onMouseEnter={() => setSelectedIndex(i)}
184
+ onClick={() => onSelect(item)}
185
+ >
186
+ <span className="shrink-0 mt-0.5">
187
+ {item.type === "builtin" ? (
188
+ <Zap className="size-4 text-emerald-500" />
189
+ ) : item.type === "skill" ? (
190
+ <Sparkles className="size-4 text-amber-500" />
191
+ ) : (
192
+ <Terminal className="size-4 text-blue-500" />
193
+ )}
194
+ </span>
195
+ <div className="min-w-0 flex-1">
196
+ <div className="flex items-baseline gap-2">
197
+ <span className="font-medium text-sm">/{item.name}</span>
198
+ {item.argumentHint && (
199
+ <span className="text-xs text-text-subtle">{item.argumentHint}</span>
200
+ )}
201
+ <span className="text-xs text-text-subtle capitalize ml-auto">
202
+ {item.scope === "bundled" ? "PPM" : item.scope === "user" ? "global" : item.type}
203
+ </span>
204
+ </div>
205
+ {item.description && (
206
+ <p className="text-xs text-text-subtle mt-0.5 line-clamp-2">
207
+ {item.description}
208
+ </p>
209
+ )}
210
+ </div>
211
+ </button>
138
212
  </div>
139
- </button>
140
- ))}
213
+ );
214
+ })}
141
215
  </div>
142
216
  </div>
143
217
  );
@@ -1,6 +1,7 @@
1
1
  import { useRef, useEffect, useState, useCallback } from "react";
2
2
  import { useExtensionStore } from "@/stores/extension-store";
3
3
  import { useTabStore } from "@/stores/tab-store";
4
+ import { getAuthToken } from "@/lib/api-client";
4
5
  import { Loader2 } from "lucide-react";
5
6
 
6
7
  /** Inject acquireVsCodeApi() shim so extension webviews can postMessage to parent */
@@ -74,7 +75,8 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
74
75
  let args: unknown[] = [];
75
76
  if (projectName) {
76
77
  try {
77
- const res = await fetch("/api/projects");
78
+ const token = getAuthToken();
79
+ const res = await fetch("/api/projects", token ? { headers: { Authorization: `Bearer ${token}` } } : {});
78
80
  const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
79
81
  const match = json.data?.find((p) => p.name === projectName);
80
82
  if (match) args = [match.path];
@@ -103,7 +105,8 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
103
105
  const command = viewType.includes(".") ? viewType : `${viewType}.view`;
104
106
  (async () => {
105
107
  try {
106
- const res = await fetch("/api/projects");
108
+ const token = getAuthToken();
109
+ const res = await fetch("/api/projects", token ? { headers: { Authorization: `Bearer ${token}` } } : {});
107
110
  const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
108
111
  const match = json.data?.find((p) => p.name === projectName);
109
112
  if (match) {
@@ -135,7 +138,8 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
135
138
  const command = viewType.includes(".") ? viewType : `${viewType}.view`;
136
139
  (async () => {
137
140
  try {
138
- const res = await fetch("/api/projects");
141
+ const token = getAuthToken();
142
+ const res = await fetch("/api/projects", token ? { headers: { Authorization: `Bearer ${token}` } } : {});
139
143
  const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
140
144
  const match = json.data?.find((p) => p.name === projectName);
141
145
  const args = match ? [match.path] : [];
@@ -1,4 +1,4 @@
1
- import { api, projectUrl } from "./api-client";
1
+ import { api, projectUrl, getAuthToken } from "./api-client";
2
2
 
3
3
  const REPO = "hienlh/ppm";
4
4
 
@@ -9,7 +9,8 @@ export async function buildBugReport(
9
9
  ): Promise<string> {
10
10
  let serverLogs = "(could not fetch)";
11
11
  try {
12
- const res = await fetch("/api/logs/recent");
12
+ const token = getAuthToken();
13
+ const res = await fetch("/api/logs/recent", token ? { headers: { Authorization: `Bearer ${token}` } } : {});
13
14
  const json = await res.json();
14
15
  if (json.ok) serverLogs = json.data.logs || "(empty)";
15
16
  } catch {}
@@ -90,11 +90,12 @@ export class WsClient {
90
90
  send(data: string | ArrayBuffer): void {
91
91
  if (this.ws?.readyState === WebSocket.OPEN) {
92
92
  this.ws.send(data);
93
- } else if (this.ws?.readyState === WebSocket.CONNECTING) {
94
- console.warn("[ws] WS still CONNECTING queuing message");
93
+ } else if (!this.intentionalClose) {
94
+ // Queue messagewill be flushed when WS (re)connects
95
+ console.warn(`[ws] WS not open (readyState=${this.ws?.readyState ?? "no-ws"}) — queuing message`);
95
96
  this.pendingMessages.push(data);
96
97
  } else {
97
- console.warn(`[ws] message dropped — readyState=${this.ws?.readyState ?? "no-ws"}`);
98
+ console.warn(`[ws] message dropped — WS intentionally closed`);
98
99
  }
99
100
  }
100
101
 
@@ -1,4 +1,5 @@
1
1
  import { create } from "zustand";
2
+ import { getAuthToken } from "@/lib/api-client";
2
3
 
3
4
  export type Theme = "light" | "dark" | "system";
4
5
  export type GitStatusViewMode = "flat" | "tree";
@@ -93,9 +94,10 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
93
94
  applyThemeClass(theme);
94
95
  set({ theme });
95
96
  // Save to server (fire-and-forget)
97
+ const token = getAuthToken();
96
98
  fetch("/api/settings/theme", {
97
99
  method: "PUT",
98
- headers: { "Content-Type": "application/json" },
100
+ headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
99
101
  body: JSON.stringify({ theme }),
100
102
  }).catch(() => {});
101
103
  },
@@ -144,9 +146,11 @@ export const useSettingsStore = create<SettingsState>((set, get) => ({
144
146
 
145
147
  fetchServerInfo: async () => {
146
148
  try {
149
+ const token = getAuthToken();
150
+ const authInit = token ? { headers: { Authorization: `Bearer ${token}` } } : {};
147
151
  const [infoRes, themeRes] = await Promise.all([
148
- fetch("/api/info"),
149
- fetch("/api/settings/theme"),
152
+ fetch("/api/info", authInit),
153
+ fetch("/api/settings/theme", authInit),
150
154
  ]);
151
155
  const infoJson = await infoRes.json();
152
156
  if (infoJson.ok) {
@@ -1 +0,0 @@
1
- import{W as e}from"./vendor-mermaid-B2SLgECS.js";export{e as createArchitectureServices};