@hienlh/ppm 0.13.2 → 0.13.4
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 +31 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-settings-section-QE6nBNgN.js → ai-settings-section-DeW4WN43.js} +1 -1
- package/dist/web/assets/{api-settings-DAk7D-NP.js → api-settings-t7Leca7J.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-Dy3PgD6O.js +1 -0
- package/dist/web/assets/{audio-preview--hRMnXRZ.js → audio-preview-BSAe2WQB.js} +1 -1
- package/dist/web/assets/chat-tab-UFEFOnpl.js +12 -0
- package/dist/web/assets/code-editor-BJ1tSNWA.js +8 -0
- package/dist/web/assets/{conflict-editor-Dlo25nmt.js → conflict-editor-CrgrMZ2F.js} +1 -1
- package/dist/web/assets/{csv-preview-HMSavgBb.js → csv-preview-C9qGhDlb.js} +1 -1
- package/dist/web/assets/{database-viewer-DcBl6OkV.js → database-viewer-e_NAkIL_.js} +2 -2
- package/dist/web/assets/diff-viewer-C2eOczTs.js +4 -0
- package/dist/web/assets/{esm-K1XIK4vc.js → esm-B3je8j5P.js} +1 -1
- package/dist/web/assets/{extension-webview-D7bGVSEd.js → extension-webview-B95nOfj-.js} +2 -2
- package/dist/web/assets/{file-store-BrbCNyLm.js → file-store-BgZggznw.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bu1SIFFq.js +1 -0
- package/dist/web/assets/{image-preview-CfkqnhXJ.js → image-preview-DAuPOzYl.js} +1 -1
- package/dist/web/assets/index-DJOjXTcq.js +27 -0
- package/dist/web/assets/index-DSOP0R0s.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-DzfAxmVd.js +1 -0
- package/dist/web/assets/{input-Dk49gO8E.js → input-bGJExpJZ.js} +1 -1
- package/dist/web/assets/keybindings-store-V12kZZHO.js +1 -0
- package/dist/web/assets/{markdown-renderer-DyAm7zuA.js → markdown-renderer-DwINRWo4.js} +3 -3
- package/dist/web/assets/packet-RMMSAZCW-DpzHf4xp.js +1 -0
- package/dist/web/assets/{pdf-preview-CZPcuy5c.js → pdf-preview-CqoQE09t.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-BpzFCKJ8.js +1 -0
- package/dist/web/assets/{port-forwarding-tab-3RNozlZ5.js → port-forwarding-tab-De7qxkjp.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CXJv4TXc.js → postgres-viewer-Dd6rLb8b.js} +3 -3
- package/dist/web/assets/radar-KQ55EAFF-DAxWKxM4.js +1 -0
- package/dist/web/assets/{scroll-area-BEllam7_.js → scroll-area-D0EQpAH2.js} +1 -1
- package/dist/web/assets/{settings-store-BLLR7ed8.js → settings-store-CdcSAgEZ.js} +2 -2
- package/dist/web/assets/settings-tab-BdTEumwU.js +1 -0
- package/dist/web/assets/{sql-query-editor-CVAnRFbi.js → sql-query-editor-vpD0I0KG.js} +1 -1
- package/dist/web/assets/sqlite-viewer-Ccz2crvN.js +1 -0
- package/dist/web/assets/{tab-store-B3M9hjho.js → tab-store-Jvy1eZGM.js} +1 -1
- package/dist/web/assets/terminal-tab-D7u7wsyb.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-D6dgXbAe.js +1 -0
- package/dist/web/assets/{use-blob-url-e9uTXjv5.js → use-blob-url-BgxxT-n_.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-BkZDwoVd.js → use-monaco-theme-dtPsv6sh.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-Dx86tuVP.js → vendor-mermaid-DCxaaPi4.js} +2 -2
- package/dist/web/assets/{video-preview-Dfz71RGb.js → video-preview-BSDzqlzk.js} +1 -1
- package/dist/web/index.html +17 -19
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +2 -0
- package/docs/journals/2026-04-22-compare-files-feature-ship.md +53 -0
- package/docs/project-changelog.md +9 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +5 -2
- package/src/server/routes/chat.ts +10 -1
- package/src/server/ws/chat.ts +29 -2
- package/src/services/file-filter.service.ts +17 -4
- package/src/services/file-list-index.service.ts +7 -3
- package/src/types/chat.ts +1 -1
- package/src/types/project.ts +2 -0
- package/src/web/app.tsx +4 -0
- package/src/web/components/chat/message-list.tsx +6 -5
- package/src/web/components/editor/compare-picker.tsx +245 -0
- package/src/web/components/explorer/file-tree.tsx +42 -1
- package/src/web/components/layout/command-palette.tsx +66 -13
- package/src/web/components/layout/draggable-tab.tsx +13 -5
- package/src/web/components/layout/tab-bar.tsx +101 -27
- package/src/web/hooks/use-chat.ts +6 -0
- package/src/web/hooks/use-global-keybindings.ts +20 -0
- package/src/web/lib/open-compare-tab.ts +76 -0
- package/src/web/lib/score-file-search.ts +41 -21
- package/src/web/stores/compare-store.ts +57 -0
- package/src/web/stores/keybindings-store.ts +1 -0
- package/dist/web/assets/architecture-PBZL5I3N-DvZbltvY.js +0 -1
- package/dist/web/assets/chat-tab-4kL3DNxf.js +0 -12
- package/dist/web/assets/code-editor-Caq5_BaF.js +0 -8
- package/dist/web/assets/columns-2-4fQcE4PF.js +0 -1
- package/dist/web/assets/diff-viewer-CCzPq1o-.js +0 -4
- package/dist/web/assets/extension-store-3yZYn07W.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-BxhdxFgj.js +0 -1
- package/dist/web/assets/index-BGFG66Gh.js +0 -27
- package/dist/web/assets/index-Bce0weeW.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-BwAZ2zd8.js +0 -1
- package/dist/web/assets/keybindings-store-B-zET-0o.js +0 -1
- package/dist/web/assets/keybindings-store-DaBV6qhz.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-tx2n5Qry.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D6S2MqVT.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-BviZcL-b.js +0 -1
- package/dist/web/assets/settings-tab-Cnav4g2u.js +0 -1
- package/dist/web/assets/sqlite-viewer-C8WUEFhA.js +0 -1
- package/dist/web/assets/terminal-tab-CaEsMxp8.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-CM54VdaB.js +0 -1
- /package/dist/web/assets/{api-client-Dvzcc_EO.js → api-client-r4nyVy7H.js} +0 -0
- /package/dist/web/assets/{csv-parser--2WJNgS7.js → csv-parser-DxVplKKB.js} +0 -0
- /package/dist/web/assets/{database-D4DIhgi-.js → database-DCT0OjgQ.js} +0 -0
- /package/dist/web/assets/{dist-im4ynINo.js → dist-BqoEabX7.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-BwzaQ50n.js → file-exclamation-point-Baz81y5z.js} +0 -0
- /package/dist/web/assets/{katex-CKoArbIw.js → katex-bpagxk3Z.js} +0 -0
- /package/dist/web/assets/{lib-DQHnkzGy.js → lib-BqkcKGFq.js} +0 -0
- /package/dist/web/assets/{react-GqWghJ-L.js → react-BkWDCPD7.js} +0 -0
- /package/dist/web/assets/{refresh-cw-LlbZDJpO.js → refresh-cw-CSFrDtiu.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-C3cq9j99.js → sql-completion-provider-EzHOQLfo.js} +0 -0
- /package/dist/web/assets/{table-Dq575bPF.js → table-DbSviOmw.js} +0 -0
- /package/dist/web/assets/{text-wrap-Cn6BNQfq.js → text-wrap-DzvCTq_i.js} +0 -0
- /package/dist/web/assets/{trash-2-CJYoLw7Q.js → trash-2-BgDIBl6f.js} +0 -0
- /package/dist/web/assets/{utils-CTg5uAYR.js → utils-ChWX7pZv.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-CU2c3f0A.js → vendor-xterm-D7SePDJp.js} +0 -0
- /package/dist/web/assets/{x-DlFGzN8d.js → x-BtqbfkR7.js} +0 -0
|
@@ -505,6 +505,9 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
505
505
|
setPhase(p);
|
|
506
506
|
phaseRef.current = p;
|
|
507
507
|
setConnectingElapsed(p === "connecting" ? ((data as any).elapsed ?? 0) : 0);
|
|
508
|
+
// Safety: idle phase means no turn running — ensure compact indicator does not linger.
|
|
509
|
+
// BE should broadcast compact_status=done too, but this is a belt-and-braces clear.
|
|
510
|
+
if (p === "idle") setCompactStatus(null);
|
|
508
511
|
return;
|
|
509
512
|
}
|
|
510
513
|
|
|
@@ -523,6 +526,9 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
523
526
|
input: state.pendingApproval.input,
|
|
524
527
|
});
|
|
525
528
|
}
|
|
529
|
+
// Sync compact indicator from authoritative server state (covers reconnect).
|
|
530
|
+
// state.compactStatus is "compacting" | null — treat undefined as null for back-compat.
|
|
531
|
+
setCompactStatus(state.compactStatus === "compacting" ? "compacting" : null);
|
|
526
532
|
// If idle, refetch history (completed turns) and hide overlay
|
|
527
533
|
if (p === "idle") {
|
|
528
534
|
refetchRef.current?.();
|
|
@@ -4,6 +4,8 @@ import { useSettingsStore } from "@/stores/settings-store";
|
|
|
4
4
|
import { useProjectStore } from "@/stores/project-store";
|
|
5
5
|
import { useKeybindingsStore, parseCombo, eventMatchesCombo } from "@/stores/keybindings-store";
|
|
6
6
|
import { useExtensionStore } from "@/stores/extension-store";
|
|
7
|
+
import { useCompareStore } from "@/stores/compare-store";
|
|
8
|
+
import { basename } from "@/lib/utils";
|
|
7
9
|
|
|
8
10
|
/** Dispatch this event to open the command palette from anywhere, optionally with initial query */
|
|
9
11
|
export function openCommandPalette(initialQuery?: string) {
|
|
@@ -156,6 +158,24 @@ export function useGlobalKeybindings() {
|
|
|
156
158
|
return;
|
|
157
159
|
}
|
|
158
160
|
|
|
161
|
+
// Compare Files — seed A from active editor tab if applicable, then open picker
|
|
162
|
+
if (match(e, "compare-files")) {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
const { activeTabId, tabs } = useTabStore.getState();
|
|
165
|
+
const active = tabs.find((t) => t.id === activeTabId);
|
|
166
|
+
const meta = active?.metadata as { filePath?: string; projectName?: string; unsavedContent?: string } | undefined;
|
|
167
|
+
if (active?.type === "editor" && meta?.filePath && meta?.projectName) {
|
|
168
|
+
useCompareStore.getState().setSelection({
|
|
169
|
+
filePath: meta.filePath,
|
|
170
|
+
projectName: meta.projectName,
|
|
171
|
+
dirtyContent: meta.unsavedContent,
|
|
172
|
+
label: basename(meta.filePath),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
window.dispatchEvent(new CustomEvent("open-compare-picker"));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
// Open search (sidebar)
|
|
160
180
|
if (match(e, "open-search")) {
|
|
161
181
|
e.preventDefault();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
2
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
3
|
+
import { basename } from "@/lib/utils";
|
|
4
|
+
|
|
5
|
+
/** One side of a compare — path + optional in-memory dirty buffer. */
|
|
6
|
+
export interface CompareSide {
|
|
7
|
+
path: string;
|
|
8
|
+
dirtyContent?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Open a `git-diff` tab comparing two files.
|
|
13
|
+
*
|
|
14
|
+
* Routing:
|
|
15
|
+
* - If either side has `dirtyContent` → fetch clean side via `/files/read`
|
|
16
|
+
* and pass `original`+`modified` inline (DiffViewer's inline mode).
|
|
17
|
+
* - Else → pass `file1`+`file2` metadata (DiffViewer fetches `/files/compare`).
|
|
18
|
+
*
|
|
19
|
+
* Returns the new tab id.
|
|
20
|
+
*/
|
|
21
|
+
export async function openCompareTab(
|
|
22
|
+
a: CompareSide,
|
|
23
|
+
b: CompareSide,
|
|
24
|
+
projectName: string,
|
|
25
|
+
): Promise<string> {
|
|
26
|
+
const title = `${basename(a.path)} ↔ ${basename(b.path)}`;
|
|
27
|
+
const aDirty = a.dirtyContent !== undefined;
|
|
28
|
+
const bDirty = b.dirtyContent !== undefined;
|
|
29
|
+
|
|
30
|
+
let metadata: Record<string, unknown>;
|
|
31
|
+
|
|
32
|
+
if (aDirty || bDirty) {
|
|
33
|
+
const [original, modified] = await Promise.all([
|
|
34
|
+
resolveSideContent(a, projectName),
|
|
35
|
+
resolveSideContent(b, projectName),
|
|
36
|
+
]);
|
|
37
|
+
// Inline mode — DiffViewer uses `original`/`modified` when present
|
|
38
|
+
// (see diff-viewer.tsx:36 `isInline` check).
|
|
39
|
+
metadata = {
|
|
40
|
+
projectName,
|
|
41
|
+
original,
|
|
42
|
+
modified,
|
|
43
|
+
// Keep paths around for future needs (copy path, re-open source, etc.).
|
|
44
|
+
file1: a.path,
|
|
45
|
+
file2: b.path,
|
|
46
|
+
};
|
|
47
|
+
} else {
|
|
48
|
+
metadata = {
|
|
49
|
+
projectName,
|
|
50
|
+
file1: a.path,
|
|
51
|
+
file2: b.path,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const id = useTabStore.getState().openTab({
|
|
56
|
+
type: "git-diff",
|
|
57
|
+
title,
|
|
58
|
+
projectId: projectName,
|
|
59
|
+
metadata,
|
|
60
|
+
closable: true,
|
|
61
|
+
});
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function resolveSideContent(side: CompareSide, projectName: string): Promise<string> {
|
|
66
|
+
if (side.dirtyContent !== undefined) return side.dirtyContent;
|
|
67
|
+
try {
|
|
68
|
+
const { content } = await api.get<{ content: string }>(
|
|
69
|
+
`${projectUrl(projectName)}/files/read?path=${encodeURIComponent(side.path)}`,
|
|
70
|
+
);
|
|
71
|
+
return content;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
74
|
+
throw new Error(`Failed to read "${side.path}": ${reason}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* > path contains(3) > fuzzy filename(4) > fuzzy path(5)
|
|
7
7
|
*
|
|
8
8
|
* Tie-breakers: shorter filename, fewer path segments.
|
|
9
|
+
*
|
|
10
|
+
* Hot-path note: callers pass PRE-LOWERCASED strings to avoid repeated
|
|
11
|
+
* allocations per keystroke. Use `scoreFileSearch` (convenience wrapper)
|
|
12
|
+
* for ad-hoc calls; use `scoreFileSearchFast` for the inner loop.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
export interface FileSearchScore {
|
|
@@ -20,7 +24,7 @@ export interface FileSearchScore {
|
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/** Extract filename from a path */
|
|
23
|
-
function getFilename(path: string): string {
|
|
27
|
+
export function getFilename(path: string): string {
|
|
24
28
|
const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
25
29
|
return i >= 0 ? path.slice(i + 1) : path;
|
|
26
30
|
}
|
|
@@ -43,42 +47,58 @@ function fuzzyGap(query: string, text: string): number {
|
|
|
43
47
|
return gap;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Fast scoring — requires pre-lowercased inputs. Use for tight loops.
|
|
52
|
+
* All string params MUST already be lowercase.
|
|
53
|
+
*/
|
|
54
|
+
export function scoreFileSearchFast(
|
|
55
|
+
qLower: string,
|
|
56
|
+
filenameLower: string,
|
|
57
|
+
pathLower: string,
|
|
58
|
+
labelLen: number,
|
|
59
|
+
depth: number,
|
|
50
60
|
): FileSearchScore | null {
|
|
51
|
-
const q = query.toLowerCase();
|
|
52
|
-
const nameLower = label.toLowerCase();
|
|
53
|
-
const pathLower = path.toLowerCase();
|
|
54
|
-
const filename = getFilename(pathLower);
|
|
55
|
-
const depth = path.split("/").length;
|
|
56
|
-
|
|
57
61
|
// Tier 0: exact filename match
|
|
58
|
-
if (
|
|
62
|
+
if (filenameLower === qLower) return { tier: 0, offset: 0, nameLen: labelLen, depth };
|
|
59
63
|
|
|
60
64
|
// Tier 1: filename starts with query
|
|
61
|
-
if (
|
|
65
|
+
if (filenameLower.startsWith(qLower)) return { tier: 1, offset: 0, nameLen: labelLen, depth };
|
|
62
66
|
|
|
63
67
|
// Tier 2: filename contains query as substring
|
|
64
|
-
const fnIdx =
|
|
65
|
-
if (fnIdx >= 0) return { tier: 2, offset: fnIdx, nameLen:
|
|
68
|
+
const fnIdx = filenameLower.indexOf(qLower);
|
|
69
|
+
if (fnIdx >= 0) return { tier: 2, offset: fnIdx, nameLen: labelLen, depth };
|
|
66
70
|
|
|
67
71
|
// Tier 3: full path contains query as substring
|
|
68
|
-
const pathIdx = pathLower.indexOf(
|
|
69
|
-
if (pathIdx >= 0) return { tier: 3, offset: pathIdx, nameLen:
|
|
72
|
+
const pathIdx = pathLower.indexOf(qLower);
|
|
73
|
+
if (pathIdx >= 0) return { tier: 3, offset: pathIdx, nameLen: labelLen, depth };
|
|
70
74
|
|
|
71
75
|
// Tier 4: fuzzy match on filename
|
|
72
|
-
const fnGap = fuzzyGap(
|
|
73
|
-
if (fnGap >= 0) return { tier: 4, offset: fnGap, nameLen:
|
|
76
|
+
const fnGap = fuzzyGap(qLower, filenameLower);
|
|
77
|
+
if (fnGap >= 0) return { tier: 4, offset: fnGap, nameLen: labelLen, depth };
|
|
74
78
|
|
|
75
79
|
// Tier 5: fuzzy match on full path
|
|
76
|
-
const pathGap = fuzzyGap(
|
|
77
|
-
if (pathGap >= 0) return { tier: 5, offset: pathGap, nameLen:
|
|
80
|
+
const pathGap = fuzzyGap(qLower, pathLower);
|
|
81
|
+
if (pathGap >= 0) return { tier: 5, offset: pathGap, nameLen: labelLen, depth };
|
|
78
82
|
|
|
79
83
|
return null;
|
|
80
84
|
}
|
|
81
85
|
|
|
86
|
+
/** Convenience wrapper — lowers inputs on the fly. Use for ad-hoc calls. */
|
|
87
|
+
export function scoreFileSearch(
|
|
88
|
+
query: string,
|
|
89
|
+
label: string,
|
|
90
|
+
path: string,
|
|
91
|
+
): FileSearchScore | null {
|
|
92
|
+
const pathLower = path.toLowerCase();
|
|
93
|
+
return scoreFileSearchFast(
|
|
94
|
+
query.toLowerCase(),
|
|
95
|
+
getFilename(pathLower),
|
|
96
|
+
pathLower,
|
|
97
|
+
label.length,
|
|
98
|
+
path.split("/").length,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
/** Compare two scores — for Array.sort (ascending = best first) */
|
|
83
103
|
export function compareScores(a: FileSearchScore, b: FileSearchScore): number {
|
|
84
104
|
return (
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import { persist } from "zustand/middleware";
|
|
3
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
4
|
+
|
|
5
|
+
/** Selection captured when user picks a file "for compare". */
|
|
6
|
+
export interface CompareSelection {
|
|
7
|
+
filePath: string;
|
|
8
|
+
projectName: string;
|
|
9
|
+
/** Captured snapshot of dirty editor buffer — undefined if file was clean. */
|
|
10
|
+
dirtyContent?: string;
|
|
11
|
+
/** Display name (basename) for menu/dialog UI. */
|
|
12
|
+
label: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CompareStore {
|
|
16
|
+
selection: CompareSelection | null;
|
|
17
|
+
setSelection: (sel: CompareSelection) => void;
|
|
18
|
+
clearSelection: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Avoid persisting huge dirty buffers (>500KB) to keep localStorage fast. */
|
|
22
|
+
const MAX_DIRTY_PERSIST_BYTES = 500_000;
|
|
23
|
+
|
|
24
|
+
export const useCompareStore = create<CompareStore>()(
|
|
25
|
+
persist(
|
|
26
|
+
(set) => ({
|
|
27
|
+
selection: null,
|
|
28
|
+
setSelection: (sel) => set({ selection: sel }),
|
|
29
|
+
clearSelection: () => set({ selection: null }),
|
|
30
|
+
}),
|
|
31
|
+
{
|
|
32
|
+
name: "ppm:compare-selection",
|
|
33
|
+
// Strip oversized dirtyContent before persisting — keep the path so user
|
|
34
|
+
// can still compare (content will be re-fetched from disk).
|
|
35
|
+
partialize: (s) => {
|
|
36
|
+
if (!s.selection) return { selection: null };
|
|
37
|
+
const sel = s.selection;
|
|
38
|
+
if (sel.dirtyContent && sel.dirtyContent.length > MAX_DIRTY_PERSIST_BYTES) {
|
|
39
|
+
const { dirtyContent: _, ...rest } = sel;
|
|
40
|
+
return { selection: rest };
|
|
41
|
+
}
|
|
42
|
+
return { selection: sel };
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Auto-clear selection when the user switches active project.
|
|
49
|
+
// Tracked in module scope to avoid clearing on the initial load hydration.
|
|
50
|
+
let lastActiveProject: string | null = null;
|
|
51
|
+
useProjectStore.subscribe((state) => {
|
|
52
|
+
const now = state.activeProject?.name ?? null;
|
|
53
|
+
if (lastActiveProject !== null && lastActiveProject !== now) {
|
|
54
|
+
useCompareStore.getState().clearSelection();
|
|
55
|
+
}
|
|
56
|
+
lastActiveProject = now;
|
|
57
|
+
});
|
|
@@ -37,6 +37,7 @@ export const KEY_ACTIONS: KeyAction[] = [
|
|
|
37
37
|
{ id: "open-git-status", label: "Git Status (sidebar)", category: "tabs", defaultKey: "Mod+Shift+E" },
|
|
38
38
|
{ id: "open-search", label: "Search Files (sidebar)", category: "tabs", defaultKey: "Mod+Shift+F" },
|
|
39
39
|
{ id: "voice-input", label: "Voice Input", category: "general", defaultKey: "Mod+Shift+V", note: "Toggle speech-to-text in chat" },
|
|
40
|
+
{ id: "compare-files", label: "Compare Files...", category: "general", defaultKey: "Mod+Alt+D", note: "Open file-compare picker (seeds active file as A)" },
|
|
40
41
|
// Projects — Mod+1..9
|
|
41
42
|
...Array.from({ length: 9 }, (_, i) => ({
|
|
42
43
|
id: `switch-project-${i + 1}`,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{W as e}from"./vendor-mermaid-Dx86tuVP.js";export{e as createArchitectureServices};
|