@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.
- package/CHANGELOG.md +15 -0
- package/dist/web/assets/{ai-settings-section-LMO_cfIW.js → ai-settings-section-D2rONDPd.js} +1 -1
- package/dist/web/assets/{api-settings-CoKe_BdR.js → api-settings-C__hxGX2.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DmL1WyG-.js +1 -0
- package/dist/web/assets/chat-tab-Dki1pz84.js +10 -0
- package/dist/web/assets/code-editor-D3AAT8nI.js +8 -0
- package/dist/web/assets/{conflict-editor-C28xnWqp.js → conflict-editor-Bxq4QiW1.js} +3 -3
- package/dist/web/assets/{database-viewer-DDGq5efK.js → database-viewer-CvQc1PZH.js} +2 -2
- package/dist/web/assets/{diff-viewer-DBELqMy0.js → diff-viewer-x7kjfVYW.js} +1 -1
- package/dist/web/assets/extension-webview-BFd0USXC.js +3 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-D8vKfkjC.js +1 -0
- package/dist/web/assets/index-DPnjO2FY.css +2 -0
- package/dist/web/assets/index-DuEUN2Eg.js +26 -0
- package/dist/web/assets/info-3K5VOQVL-VG29MIoT.js +1 -0
- package/dist/web/assets/{input-CHRMley8.js → input-ClhO__YM.js} +1 -1
- package/dist/web/assets/keybindings-store-BkZjvU9J.js +1 -0
- package/dist/web/assets/{keybindings-store-D2N-Tq4N.js → keybindings-store-C9KsBH7z.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CtsslbMO.js → markdown-renderer-CKmmrUuy.js} +3 -3
- package/dist/web/assets/packet-RMMSAZCW-Bl_WpvPc.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BVpLpAIy.js +1 -0
- package/dist/web/assets/{port-forwarding-tab-BszAda9U.js → port-forwarding-tab-BUH9aImG.js} +1 -1
- package/dist/web/assets/{postgres-viewer-X2li3HfX.js → postgres-viewer-YkljtDWX.js} +2 -2
- package/dist/web/assets/{project-store-Ciq-cK1O.js → project-store-BYmQ0fDC.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-CJGco43I.js +1 -0
- package/dist/web/assets/{scroll-area-DwWF9FpN.js → scroll-area-DW7L4Gnc.js} +1 -1
- package/dist/web/assets/settings-store-D9CflsKU.js +2 -0
- package/dist/web/assets/settings-tab-DfPjX9uY.js +1 -0
- package/dist/web/assets/{sql-query-editor-DZ9xskL8.js → sql-query-editor-CM_qEhaX.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-BV0p6qnR.js → sqlite-viewer-f6ZJHIzh.js} +1 -1
- package/dist/web/assets/{tab-store-DZbiYk7y.js → tab-store-B3M9hjho.js} +1 -1
- package/dist/web/assets/terminal-tab-CVdfvDSK.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-BsOrObtE.js +1 -0
- package/dist/web/assets/{use-monaco-theme-OY18iXNi.js → use-monaco-theme-CvV5vy_F.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-B2SLgECS.js → vendor-mermaid-CwOSbfhN.js} +1 -1
- package/dist/web/index.html +13 -13
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/packages/ext-git-graph/src/extension.ts +11 -4
- package/packages/ext-git-graph/src/webview-html.ts +22 -9
- package/src/server/routes/chat.ts +28 -3
- package/src/services/db.service.ts +36 -0
- package/src/services/extension-host-worker.ts +3 -2
- package/src/services/extension.service.ts +4 -2
- package/src/services/slash-discovery/cache.ts +38 -0
- package/src/services/slash-discovery/fuzzy-search.ts +12 -3
- package/src/services/slash-discovery/index.ts +9 -2
- package/src/services/slash-items.service.ts +1 -1
- package/src/services/supervisor-state.ts +13 -1
- package/src/services/supervisor.ts +4 -3
- package/src/web/components/chat/chat-tab.tsx +12 -2
- package/src/web/components/chat/message-input.tsx +24 -14
- package/src/web/components/chat/message-list.tsx +7 -14
- package/src/web/components/chat/slash-command-picker.tsx +120 -46
- package/src/web/components/extensions/extension-webview.tsx +7 -3
- package/src/web/lib/report-bug.ts +3 -2
- package/src/web/lib/ws-client.ts +4 -3
- package/src/web/stores/settings-store.ts +7 -3
- package/dist/web/assets/architecture-PBZL5I3N-CUZIB1Vq.js +0 -1
- package/dist/web/assets/chat-tab-lo46P4ZN.js +0 -10
- package/dist/web/assets/code-editor-DN0UiBvk.js +0 -8
- package/dist/web/assets/extension-webview--HUG0c_R.js +0 -3
- package/dist/web/assets/gitGraph-HDMCJU4V-CtOMUphQ.js +0 -1
- package/dist/web/assets/index-BUxaCPPv.js +0 -26
- package/dist/web/assets/index-Dq7PPmAk.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BCrPCWGY.js +0 -1
- package/dist/web/assets/keybindings-store-C7No6mtl.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-D_OqB-zi.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-WUHpLNJz.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-HQIIecVM.js +0 -1
- package/dist/web/assets/settings-store-B470PCWf.js +0 -2
- package/dist/web/assets/settings-tab-DR1HhS4C.js +0 -1
- package/dist/web/assets/terminal-tab-BiRx6kvn.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-0wLgUUTz.js +0 -1
- /package/dist/web/assets/{api-client-o_6TmLGC.js → api-client-Bn-Pi9k5.js} +0 -0
- /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
|
-
//
|
|
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
|
-
|
|
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
|
|
173
|
-
|
|
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((
|
|
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]);
|
|
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((
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {}
|
package/src/web/lib/ws-client.ts
CHANGED
|
@@ -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.
|
|
94
|
-
|
|
93
|
+
} else if (!this.intentionalClose) {
|
|
94
|
+
// Queue message — will 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 —
|
|
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};
|