@hienlh/ppm 0.9.0-beta.8 → 0.9.1
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 +238 -0
- package/bun.lock +17 -0
- package/dist/web/assets/api-settings-BUvk6Saw.js +1 -0
- package/dist/web/assets/arrow-up-BYhx9ckd.js +1 -0
- package/dist/web/assets/browser-tab-CrkhFCaw.js +1 -0
- package/dist/web/assets/chat-tab-C6jpiwh7.js +8 -0
- package/dist/web/assets/chevron-right-5HgK6l7K.js +1 -0
- package/dist/web/assets/code-editor-CBIPzlP2.js +2 -0
- package/dist/web/assets/columns-2-cEVJHYd7.js +1 -0
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +1 -0
- package/dist/web/assets/{csv-preview-DLqYtXxt.js → csv-preview-ncSOnJSC.js} +2 -2
- package/dist/web/assets/database-viewer-BqOJR_zi.js +1 -0
- package/dist/web/assets/diff-viewer-CcLyp4eY.js +4 -0
- package/dist/web/assets/{dist-CALwEtco.js → dist-DIV6WgAG.js} +1 -1
- package/dist/web/assets/{dist-DGDPTxs1.js → dist-ovWkrgO-.js} +1 -1
- package/dist/web/assets/extension-webview-NiZ7Ybvv.js +3 -0
- package/dist/web/assets/git-graph-CoTvMrIo.js +1 -0
- package/dist/web/assets/index-C8byznLO.js +37 -0
- package/dist/web/assets/index-KwC2YrG4.css +2 -0
- package/dist/web/assets/jsx-runtime-kMwlnEGE.js +1 -0
- package/dist/web/assets/keybindings-store-DPYzBe_M.js +1 -0
- package/dist/web/assets/{markdown-renderer-DklUd_Gv.js → markdown-renderer-DPLdR9xc.js} +4 -4
- package/dist/web/assets/postgres-viewer-BeiK4lCa.js +1 -0
- package/dist/web/assets/settings-tab-D3AvU4lu.js +1 -0
- package/dist/web/assets/sqlite-viewer-nA2sD4Yv.js +1 -0
- package/dist/web/assets/tab-store-BOgTrqRr.js +1 -0
- package/dist/web/assets/table-DFevCOMd.js +1 -0
- package/dist/web/assets/tag-CXMT0QB6.js +1 -0
- package/dist/web/assets/{terminal-tab-CqRuiIFn.js → terminal-tab-BBi0pEji.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-Dcz3aLAE.js → use-monaco-theme-B5pG2d1w.js} +1 -1
- package/dist/web/index.html +8 -8
- package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
- package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
- package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +128 -1
- package/docs/codebase-summary.md +79 -12
- package/docs/extension-development-guide.md +532 -0
- package/docs/project-changelog.md +51 -1
- package/docs/project-roadmap.md +9 -3
- package/docs/streaming-input-guide.md +267 -0
- package/docs/system-architecture.md +432 -3
- package/package.json +6 -3
- package/packages/ext-database/package.json +41 -0
- package/packages/ext-database/src/connection-tree.ts +142 -0
- package/packages/ext-database/src/extension.ts +346 -0
- package/packages/ext-database/src/query-panel.ts +120 -0
- package/packages/ext-database/src/table-viewer-panel.ts +410 -0
- package/packages/ext-database/tsconfig.json +8 -0
- package/packages/vscode-compat/package.json +16 -0
- package/packages/vscode-compat/src/commands.ts +39 -0
- package/packages/vscode-compat/src/context.ts +65 -0
- package/packages/vscode-compat/src/disposable.ts +21 -0
- package/packages/vscode-compat/src/env.ts +20 -0
- package/packages/vscode-compat/src/event-emitter.ts +28 -0
- package/packages/vscode-compat/src/index.ts +93 -0
- package/packages/vscode-compat/src/not-supported.ts +15 -0
- package/packages/vscode-compat/src/types.ts +167 -0
- package/packages/vscode-compat/src/uri.ts +65 -0
- package/packages/vscode-compat/src/window.ts +229 -0
- package/packages/vscode-compat/src/workspace.ts +76 -0
- package/packages/vscode-compat/tsconfig.json +10 -0
- package/snapshot-state.md +1526 -0
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/ext-cmd.ts +121 -0
- package/src/cli/commands/restart.ts +9 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/index.ts +5 -3
- package/src/providers/claude-agent-sdk.ts +221 -17
- package/src/providers/cli-provider-base.ts +6 -0
- package/src/server/index.ts +55 -155
- package/src/server/routes/chat.ts +81 -11
- package/src/server/routes/extensions.ts +81 -0
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/settings.ts +27 -0
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +9 -3
- package/src/server/ws/extensions.ts +175 -0
- package/src/services/account-selector.service.ts +14 -5
- package/src/services/account.service.ts +20 -15
- package/src/services/claude-usage.service.ts +29 -24
- package/src/services/cloud-ws.service.ts +228 -0
- package/src/services/cloud.service.ts +11 -6
- package/src/services/contribution-registry.ts +110 -0
- package/src/services/db.service.ts +181 -4
- package/src/services/extension-host-worker.ts +160 -0
- package/src/services/extension-installer.ts +112 -0
- package/src/services/extension-manifest.ts +65 -0
- package/src/services/extension-rpc-handlers.ts +235 -0
- package/src/services/extension-rpc.ts +105 -0
- package/src/services/extension.service.ts +228 -0
- package/src/services/mcp-config.service.ts +15 -6
- package/src/services/supervisor.ts +271 -25
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +4 -0
- package/src/types/extension-messages.ts +64 -0
- package/src/types/extension.ts +131 -0
- package/src/web/app.tsx +69 -48
- package/src/web/components/chat/account-rotation-settings.tsx +163 -0
- package/src/web/components/chat/chat-history-bar.tsx +106 -10
- package/src/web/components/chat/chat-tab.tsx +15 -10
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-list.tsx +19 -6
- package/src/web/components/chat/session-picker.tsx +80 -32
- package/src/web/components/chat/usage-badge.tsx +68 -8
- package/src/web/components/editor/editor-breadcrumb.tsx +20 -29
- package/src/web/components/extensions/extension-inputbox.tsx +92 -0
- package/src/web/components/extensions/extension-quickpick.tsx +194 -0
- package/src/web/components/extensions/extension-tree-view.tsx +240 -0
- package/src/web/components/extensions/extension-webview.tsx +83 -0
- package/src/web/components/layout/command-palette.tsx +22 -2
- package/src/web/components/layout/editor-panel.tsx +163 -18
- package/src/web/components/layout/mobile-nav.tsx +2 -1
- package/src/web/components/layout/sidebar.tsx +21 -3
- package/src/web/components/layout/status-bar.tsx +64 -0
- package/src/web/components/layout/tab-bar.tsx +2 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/components/layout/upgrade-banner.tsx +15 -5
- package/src/web/components/settings/change-password-section.tsx +128 -0
- package/src/web/components/settings/extension-manager-section.tsx +214 -0
- package/src/web/components/settings/settings-tab.tsx +9 -2
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +28 -0
- package/src/web/hooks/use-extension-ws.ts +181 -0
- package/src/web/hooks/use-global-keybindings.ts +18 -2
- package/src/web/hooks/use-server-reload.ts +9 -0
- package/src/web/hooks/use-url-sync.ts +173 -21
- package/src/web/stores/connection-store.ts +39 -0
- package/src/web/stores/extension-store.ts +204 -0
- package/src/web/stores/panel-store.ts +63 -9
- package/src/web/stores/panel-utils.ts +145 -3
- package/src/web/stores/settings-store.ts +7 -2
- package/src/web/stores/tab-store.ts +2 -1
- package/test-session-ops.mjs +444 -0
- package/test-tokens.mjs +212 -0
- package/tsconfig.json +3 -1
- package/dist/web/assets/api-settings-D21InCnR.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-BEe89aSD.js +0 -1
- package/dist/web/assets/chat-tab-9lqvWozA.js +0 -7
- package/dist/web/assets/chevron-right-CHnjJt4E.js +0 -1
- package/dist/web/assets/code-editor-COAIZx-B.js +0 -2
- package/dist/web/assets/columns-2-DbesTfa7.js +0 -1
- package/dist/web/assets/database-viewer-aRR9n_Ui.js +0 -1
- package/dist/web/assets/diff-viewer-C4KMvpHr.js +0 -4
- package/dist/web/assets/dist-CVTST7Gc.js +0 -1
- package/dist/web/assets/git-graph-CfJjl4E3.js +0 -1
- package/dist/web/assets/index-Db8uky1a.css +0 -2
- package/dist/web/assets/index-DxZuwBDe.js +0 -37
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
- package/dist/web/assets/keybindings-store-_uWVCZMv.js +0 -1
- package/dist/web/assets/postgres-viewer-DEAvAyaX.js +0 -1
- package/dist/web/assets/settings-tab-BQedc-No.js +0 -1
- package/dist/web/assets/sqlite-viewer-BPA5idzT.js +0 -1
- package/dist/web/assets/tab-store-DhK6EpBT.js +0 -1
- package/dist/web/assets/table-CQVQM2SB.js +0 -1
- package/dist/web/assets/tag-Q2dZiSPX.js +0 -1
|
@@ -1,37 +1,181 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
import { useTabStore } from "@/stores/tab-store";
|
|
2
|
+
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// URL state types
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
export interface UrlState {
|
|
9
|
+
projectName: string | null;
|
|
10
|
+
tabType: TabType | null;
|
|
11
|
+
tabIdentifier: string | null;
|
|
12
|
+
openChat: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const VALID_TAB_TYPES: TabType[] = [
|
|
16
|
+
"terminal", "chat", "editor", "database", "sqlite",
|
|
17
|
+
"postgres", "git-graph", "git-diff", "settings", "browser",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Parse URL → state
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
3
23
|
|
|
4
24
|
/**
|
|
5
|
-
* Parse the current URL to extract project name and tab
|
|
6
|
-
*
|
|
25
|
+
* Parse the current URL to extract project name and tab info.
|
|
26
|
+
* Format: /project/{name}/{tabType}/{...identifier}
|
|
7
27
|
*/
|
|
8
|
-
export function parseUrlState():
|
|
28
|
+
export function parseUrlState(): UrlState {
|
|
9
29
|
const path = window.location.pathname;
|
|
10
|
-
const match = path.match(/^\/project\/([^/]+)(?:\/tab\/([^/]+))?/);
|
|
11
30
|
const params = new URLSearchParams(window.location.search);
|
|
12
31
|
const openChat = params.get("openChat");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
32
|
+
|
|
33
|
+
const match = path.match(/^\/project\/([^/]+)(?:\/([^/]+)(\/.*)?)?/);
|
|
34
|
+
if (!match) return { projectName: null, tabType: null, tabIdentifier: null, openChat };
|
|
35
|
+
|
|
36
|
+
const projectName = decodeURIComponent(match[1]!);
|
|
37
|
+
const rawType = match[2] ?? null;
|
|
38
|
+
const rawIdentifier = match[3] ? match[3].slice(1) : null; // strip leading /
|
|
39
|
+
|
|
40
|
+
// Legacy fallback: /project/{name}/tab/{tabId}
|
|
41
|
+
if (rawType === "tab") {
|
|
42
|
+
return { projectName, tabType: null, tabIdentifier: null, openChat };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const tabType = VALID_TAB_TYPES.includes(rawType as TabType) ? (rawType as TabType) : null;
|
|
46
|
+
|
|
47
|
+
return { projectName, tabType, tabIdentifier: rawIdentifier, openChat };
|
|
19
48
|
}
|
|
20
49
|
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Build URL from state
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
|
|
21
54
|
/**
|
|
22
|
-
* Build URL path from project name and tab ID.
|
|
55
|
+
* Build URL path from project name and deterministic tab ID.
|
|
23
56
|
*/
|
|
24
|
-
function buildUrl(projectName: string | null, tabId: string | null): string {
|
|
57
|
+
export function buildUrl(projectName: string | null, tabId: string | null): string {
|
|
25
58
|
if (!projectName || projectName === "__global__") return "/";
|
|
59
|
+
|
|
26
60
|
let url = `/project/${encodeURIComponent(projectName)}`;
|
|
27
|
-
if (tabId) url
|
|
61
|
+
if (!tabId) return url;
|
|
62
|
+
|
|
63
|
+
// Strip panel suffix (@panel-xxx) — not meaningful in URLs
|
|
64
|
+
const atIdx = tabId.indexOf("@");
|
|
65
|
+
const cleanId = atIdx !== -1 ? tabId.slice(0, atIdx) : tabId;
|
|
66
|
+
|
|
67
|
+
// tabId format: "type:identifier" or "type" (singletons)
|
|
68
|
+
const colonIdx = cleanId.indexOf(":");
|
|
69
|
+
if (colonIdx === -1) {
|
|
70
|
+
// Singleton: git-graph, settings
|
|
71
|
+
url += `/${cleanId}`;
|
|
72
|
+
} else {
|
|
73
|
+
const type = cleanId.slice(0, colonIdx);
|
|
74
|
+
const identifier = cleanId.slice(colonIdx + 1);
|
|
75
|
+
// Real slashes — no encoding for paths. Only encode special URL chars.
|
|
76
|
+
url += `/${type}/${identifier.replace(/[?#]/g, encodeURIComponent)}`;
|
|
77
|
+
}
|
|
28
78
|
return url;
|
|
29
79
|
}
|
|
30
80
|
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Tab ID reconstruction from URL
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Reconstruct deterministic tab ID from parsed URL */
|
|
86
|
+
export function tabIdFromUrl(tabType: TabType, tabIdentifier: string | null): string {
|
|
87
|
+
if (!tabIdentifier) return tabType; // singleton
|
|
88
|
+
return `${tabType}:${tabIdentifier}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Auto-open tab from URL
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
function buildMetadataFromUrl(
|
|
96
|
+
type: TabType, identifier: string | null, projectName: string,
|
|
97
|
+
): Record<string, unknown> | null {
|
|
98
|
+
switch (type) {
|
|
99
|
+
case "editor": return identifier ? { filePath: identifier, projectName } : null;
|
|
100
|
+
case "chat": {
|
|
101
|
+
if (!identifier) return null;
|
|
102
|
+
const slashIdx = identifier.indexOf("/");
|
|
103
|
+
if (slashIdx === -1) return { sessionId: identifier, projectName };
|
|
104
|
+
const providerId = identifier.slice(0, slashIdx);
|
|
105
|
+
const sessionId = identifier.slice(slashIdx + 1);
|
|
106
|
+
return sessionId ? { sessionId, providerId, projectName } : null;
|
|
107
|
+
}
|
|
108
|
+
case "terminal": return { terminalIndex: parseInt(identifier ?? "1", 10), projectName };
|
|
109
|
+
case "git-graph": return { projectName };
|
|
110
|
+
case "git-diff": return identifier ? { filePath: identifier, projectName } : null;
|
|
111
|
+
case "settings": return {};
|
|
112
|
+
case "database": {
|
|
113
|
+
const [connId, tableName] = (identifier ?? "").split(":");
|
|
114
|
+
return connId ? { connectionId: connId, tableName: tableName ?? "" } : null;
|
|
115
|
+
}
|
|
116
|
+
case "sqlite": return identifier ? { filePath: identifier, projectName } : null;
|
|
117
|
+
case "postgres": {
|
|
118
|
+
const [connId, tableName] = (identifier ?? "").split(":");
|
|
119
|
+
return connId ? { connectionId: connId, tableName: tableName ?? "" } : null;
|
|
120
|
+
}
|
|
121
|
+
case "browser": return identifier ? { url: identifier } : null;
|
|
122
|
+
default: return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function buildTitleFromUrl(type: TabType, identifier: string | null): string {
|
|
127
|
+
switch (type) {
|
|
128
|
+
case "editor": return identifier?.split("/").pop() ?? "File";
|
|
129
|
+
case "chat": return "Chat";
|
|
130
|
+
case "terminal": return `Terminal ${identifier ?? "1"}`;
|
|
131
|
+
case "git-graph": return "Git Graph";
|
|
132
|
+
case "git-diff": return identifier?.split("/").pop() ?? "Diff";
|
|
133
|
+
case "settings": return "Settings";
|
|
134
|
+
case "database": return identifier ?? "Database";
|
|
135
|
+
case "sqlite": return identifier?.split("/").pop() ?? "SQLite";
|
|
136
|
+
case "postgres": return identifier ?? "PostgreSQL";
|
|
137
|
+
case "browser": return "Browser";
|
|
138
|
+
default: return type;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Auto-open or focus a tab based on URL state */
|
|
143
|
+
export function autoOpenFromUrl(
|
|
144
|
+
tabType: TabType,
|
|
145
|
+
tabIdentifier: string | null,
|
|
146
|
+
projectName: string,
|
|
147
|
+
): void {
|
|
148
|
+
const { tabs, setActiveTab, openTab } = useTabStore.getState();
|
|
149
|
+
const expectedId = tabIdFromUrl(tabType, tabIdentifier);
|
|
150
|
+
|
|
151
|
+
// Check if tab already exists
|
|
152
|
+
const existing = tabs.find((t) => t.id === expectedId);
|
|
153
|
+
if (existing) {
|
|
154
|
+
setActiveTab(existing.id);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Auto-create tab from URL
|
|
159
|
+
const metadata = buildMetadataFromUrl(tabType, tabIdentifier, projectName);
|
|
160
|
+
if (!metadata) return;
|
|
161
|
+
|
|
162
|
+
openTab({
|
|
163
|
+
type: tabType,
|
|
164
|
+
title: buildTitleFromUrl(tabType, tabIdentifier),
|
|
165
|
+
projectId: projectName,
|
|
166
|
+
closable: true,
|
|
167
|
+
metadata,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Hook: sync URL ↔ tab state
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
|
|
31
175
|
/**
|
|
32
176
|
* Sync tab/project state with browser URL.
|
|
33
|
-
* - On tab/project change → pushState
|
|
34
|
-
* - On popstate (back/forward) → restore tab from URL
|
|
177
|
+
* - On tab/project change → pushState with type-based URL
|
|
178
|
+
* - On popstate (back/forward) → restore/create tab from URL
|
|
35
179
|
*/
|
|
36
180
|
export function useUrlSync() {
|
|
37
181
|
const activeTabId = useTabStore((s) => s.activeTabId);
|
|
@@ -40,7 +184,6 @@ export function useUrlSync() {
|
|
|
40
184
|
|
|
41
185
|
// Push URL when active tab or project changes
|
|
42
186
|
useEffect(() => {
|
|
43
|
-
// Skip push if this change was triggered by popstate (back/forward)
|
|
44
187
|
if (isPopState.current) {
|
|
45
188
|
isPopState.current = false;
|
|
46
189
|
return;
|
|
@@ -55,11 +198,20 @@ export function useUrlSync() {
|
|
|
55
198
|
// Listen for back/forward navigation
|
|
56
199
|
useEffect(() => {
|
|
57
200
|
function handlePopState() {
|
|
58
|
-
const {
|
|
201
|
+
const { tabType, tabIdentifier } = parseUrlState();
|
|
202
|
+
if (!tabType) return;
|
|
203
|
+
|
|
204
|
+
isPopState.current = true;
|
|
59
205
|
const { tabs, setActiveTab } = useTabStore.getState();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
206
|
+
const expectedId = tabIdFromUrl(tabType, tabIdentifier);
|
|
207
|
+
const existing = tabs.find((t) => t.id === expectedId);
|
|
208
|
+
|
|
209
|
+
if (existing) {
|
|
210
|
+
setActiveTab(existing.id);
|
|
211
|
+
} else {
|
|
212
|
+
// Auto-open tab on back/forward if it was closed
|
|
213
|
+
const project = useTabStore.getState().currentProject;
|
|
214
|
+
if (project) autoOpenFromUrl(tabType, tabIdentifier, project);
|
|
63
215
|
}
|
|
64
216
|
}
|
|
65
217
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
interface ConnectionState {
|
|
4
|
+
/** Whether the server is currently unreachable */
|
|
5
|
+
isDown: boolean;
|
|
6
|
+
/** Timestamp when the server first went down */
|
|
7
|
+
downSince: number | null;
|
|
8
|
+
/** Whether the overlay should be shown (down for > threshold) */
|
|
9
|
+
showOverlay: boolean;
|
|
10
|
+
|
|
11
|
+
markDown: () => void;
|
|
12
|
+
markUp: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** How long the server must be unreachable before showing the overlay */
|
|
16
|
+
const OVERLAY_THRESHOLD_MS = 15_000;
|
|
17
|
+
|
|
18
|
+
export const useConnectionStore = create<ConnectionState>((set, get) => ({
|
|
19
|
+
isDown: false,
|
|
20
|
+
downSince: null,
|
|
21
|
+
showOverlay: false,
|
|
22
|
+
|
|
23
|
+
markDown: () => {
|
|
24
|
+
const { downSince } = get();
|
|
25
|
+
const now = Date.now();
|
|
26
|
+
const since = downSince ?? now;
|
|
27
|
+
const elapsed = now - since;
|
|
28
|
+
|
|
29
|
+
set({
|
|
30
|
+
isDown: true,
|
|
31
|
+
downSince: since,
|
|
32
|
+
showOverlay: elapsed >= OVERLAY_THRESHOLD_MS,
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
markUp: () => {
|
|
37
|
+
set({ isDown: false, downSince: null, showOverlay: false });
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { ExtensionContributes, ContributedCommand } from "../../types/extension.ts";
|
|
3
|
+
|
|
4
|
+
// --- UI types for extension components ---
|
|
5
|
+
|
|
6
|
+
export interface StatusBarItemUI {
|
|
7
|
+
id: string;
|
|
8
|
+
text: string;
|
|
9
|
+
tooltip?: string;
|
|
10
|
+
command?: string;
|
|
11
|
+
alignment: "left" | "right";
|
|
12
|
+
priority: number;
|
|
13
|
+
extensionId?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TreeItemAction {
|
|
17
|
+
icon: "refresh" | "edit" | "trash" | "plus" | "search";
|
|
18
|
+
tooltip: string;
|
|
19
|
+
command: string;
|
|
20
|
+
commandArgs?: unknown[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TreeItemUI {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
tooltip?: string;
|
|
28
|
+
icon?: string;
|
|
29
|
+
color?: string;
|
|
30
|
+
badge?: string;
|
|
31
|
+
actions?: TreeItemAction[];
|
|
32
|
+
collapsibleState: "none" | "collapsed" | "expanded";
|
|
33
|
+
command?: string;
|
|
34
|
+
commandArgs?: unknown[];
|
|
35
|
+
children?: TreeItemUI[];
|
|
36
|
+
contextValue?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WebviewPanelUI {
|
|
40
|
+
id: string;
|
|
41
|
+
extensionId: string;
|
|
42
|
+
viewType: string;
|
|
43
|
+
title: string;
|
|
44
|
+
html: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface QuickPickState {
|
|
48
|
+
items: QuickPickItemUI[];
|
|
49
|
+
options: { placeholder?: string; canPickMany?: boolean };
|
|
50
|
+
resolve: (selected: QuickPickItemUI[] | undefined) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface QuickPickItemUI {
|
|
54
|
+
label: string;
|
|
55
|
+
description?: string;
|
|
56
|
+
detail?: string;
|
|
57
|
+
picked?: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface InputBoxState {
|
|
61
|
+
options: { prompt?: string; value?: string; placeholder?: string; password?: boolean };
|
|
62
|
+
resolve: (value: string | undefined) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Store ---
|
|
66
|
+
|
|
67
|
+
interface ExtensionStore {
|
|
68
|
+
// Status bar
|
|
69
|
+
statusBarItems: StatusBarItemUI[];
|
|
70
|
+
addStatusBarItem: (item: StatusBarItemUI) => void;
|
|
71
|
+
removeStatusBarItem: (id: string) => void;
|
|
72
|
+
updateStatusBarItem: (id: string, updates: Partial<StatusBarItemUI>) => void;
|
|
73
|
+
|
|
74
|
+
// Tree views
|
|
75
|
+
treeViews: Record<string, TreeItemUI[]>;
|
|
76
|
+
updateTree: (viewId: string, items: TreeItemUI[]) => void;
|
|
77
|
+
updateTreeChildren: (viewId: string, parentId: string, children: TreeItemUI[]) => void;
|
|
78
|
+
removeTree: (viewId: string) => void;
|
|
79
|
+
|
|
80
|
+
// Webview panels
|
|
81
|
+
webviewPanels: Record<string, WebviewPanelUI>;
|
|
82
|
+
addWebviewPanel: (panel: WebviewPanelUI) => void;
|
|
83
|
+
removeWebviewPanel: (id: string) => void;
|
|
84
|
+
updateWebviewPanel: (id: string, updates: Partial<WebviewPanelUI>) => void;
|
|
85
|
+
|
|
86
|
+
// Contributions (fetched from API)
|
|
87
|
+
contributions: ExtensionContributes | null;
|
|
88
|
+
setContributions: (c: ExtensionContributes) => void;
|
|
89
|
+
|
|
90
|
+
// QuickPick modal
|
|
91
|
+
quickPick: QuickPickState | null;
|
|
92
|
+
showQuickPick: (items: QuickPickItemUI[], options?: QuickPickState["options"]) => Promise<QuickPickItemUI[] | undefined>;
|
|
93
|
+
resolveQuickPick: (selected: QuickPickItemUI[] | undefined) => void;
|
|
94
|
+
|
|
95
|
+
// InputBox modal
|
|
96
|
+
inputBox: InputBoxState | null;
|
|
97
|
+
showInputBox: (options?: InputBoxState["options"]) => Promise<string | undefined>;
|
|
98
|
+
resolveInputBox: (value: string | undefined) => void;
|
|
99
|
+
|
|
100
|
+
// Cleanup
|
|
101
|
+
clearExtension: (extensionId: string) => void;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const useExtensionStore = create<ExtensionStore>((set, get) => ({
|
|
105
|
+
// --- Status bar ---
|
|
106
|
+
statusBarItems: [],
|
|
107
|
+
addStatusBarItem: (item) => set((s) => ({
|
|
108
|
+
statusBarItems: [...s.statusBarItems.filter((i) => i.id !== item.id), item],
|
|
109
|
+
})),
|
|
110
|
+
removeStatusBarItem: (id) => set((s) => ({
|
|
111
|
+
statusBarItems: s.statusBarItems.filter((i) => i.id !== id),
|
|
112
|
+
})),
|
|
113
|
+
updateStatusBarItem: (id, updates) => set((s) => ({
|
|
114
|
+
statusBarItems: s.statusBarItems.map((i) => i.id === id ? { ...i, ...updates } : i),
|
|
115
|
+
})),
|
|
116
|
+
|
|
117
|
+
// --- Tree views ---
|
|
118
|
+
treeViews: {},
|
|
119
|
+
updateTree: (viewId, items) => set((s) => ({
|
|
120
|
+
treeViews: { ...s.treeViews, [viewId]: items },
|
|
121
|
+
})),
|
|
122
|
+
updateTreeChildren: (viewId, parentId, children) => set((s) => {
|
|
123
|
+
const items = s.treeViews[viewId];
|
|
124
|
+
if (!items) return s;
|
|
125
|
+
const merge = (nodes: TreeItemUI[]): TreeItemUI[] =>
|
|
126
|
+
nodes.map((n) => {
|
|
127
|
+
if (n.id === parentId) return { ...n, children, collapsibleState: "expanded" as const };
|
|
128
|
+
if (n.children) return { ...n, children: merge(n.children) };
|
|
129
|
+
return n;
|
|
130
|
+
});
|
|
131
|
+
return { treeViews: { ...s.treeViews, [viewId]: merge(items) } };
|
|
132
|
+
}),
|
|
133
|
+
removeTree: (viewId) => set((s) => {
|
|
134
|
+
const { [viewId]: _, ...rest } = s.treeViews;
|
|
135
|
+
return { treeViews: rest };
|
|
136
|
+
}),
|
|
137
|
+
|
|
138
|
+
// --- Webview panels ---
|
|
139
|
+
webviewPanels: {},
|
|
140
|
+
addWebviewPanel: (panel) => set((s) => ({
|
|
141
|
+
webviewPanels: { ...s.webviewPanels, [panel.id]: panel },
|
|
142
|
+
})),
|
|
143
|
+
removeWebviewPanel: (id) => set((s) => {
|
|
144
|
+
const { [id]: _, ...rest } = s.webviewPanels;
|
|
145
|
+
return { webviewPanels: rest };
|
|
146
|
+
}),
|
|
147
|
+
updateWebviewPanel: (id, updates) => set((s) => {
|
|
148
|
+
const existing = s.webviewPanels[id];
|
|
149
|
+
if (!existing) return s;
|
|
150
|
+
return { webviewPanels: { ...s.webviewPanels, [id]: { ...existing, ...updates } } };
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
// --- Contributions ---
|
|
154
|
+
contributions: null,
|
|
155
|
+
setContributions: (c) => set({ contributions: c }),
|
|
156
|
+
|
|
157
|
+
// --- QuickPick ---
|
|
158
|
+
quickPick: null,
|
|
159
|
+
showQuickPick: (items, options = {}) => {
|
|
160
|
+
// Resolve any existing quickpick first (prevents promise leak)
|
|
161
|
+
const existing = get().quickPick;
|
|
162
|
+
if (existing) existing.resolve(undefined);
|
|
163
|
+
return new Promise((resolve) => {
|
|
164
|
+
set({ quickPick: { items, options, resolve } });
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
resolveQuickPick: (selected) => {
|
|
168
|
+
const qp = get().quickPick;
|
|
169
|
+
if (qp) {
|
|
170
|
+
qp.resolve(selected);
|
|
171
|
+
set({ quickPick: null });
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// --- InputBox ---
|
|
176
|
+
inputBox: null,
|
|
177
|
+
showInputBox: (options = {}) => {
|
|
178
|
+
// Resolve any existing inputbox first (prevents promise leak)
|
|
179
|
+
const existing = get().inputBox;
|
|
180
|
+
if (existing) existing.resolve(undefined);
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
set({ inputBox: { options, resolve } });
|
|
183
|
+
});
|
|
184
|
+
},
|
|
185
|
+
resolveInputBox: (value) => {
|
|
186
|
+
const ib = get().inputBox;
|
|
187
|
+
if (ib) {
|
|
188
|
+
ib.resolve(value);
|
|
189
|
+
set({ inputBox: null });
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
// --- Cleanup ---
|
|
194
|
+
clearExtension: (extensionId) => set((s) => {
|
|
195
|
+
const webviewPanels = { ...s.webviewPanels };
|
|
196
|
+
for (const [id, panel] of Object.entries(webviewPanels)) {
|
|
197
|
+
if (panel.extensionId === extensionId) delete webviewPanels[id];
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
statusBarItems: s.statusBarItems.filter((i) => i.extensionId !== extensionId),
|
|
201
|
+
webviewPanels,
|
|
202
|
+
};
|
|
203
|
+
}),
|
|
204
|
+
}));
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
-
import { randomId } from "@/lib/utils";
|
|
3
2
|
import type { Tab, TabType } from "./tab-store";
|
|
4
3
|
import {
|
|
5
4
|
type Panel,
|
|
@@ -13,18 +12,15 @@ import {
|
|
|
13
12
|
MAX_ROWS,
|
|
14
13
|
savePanelLayout,
|
|
15
14
|
loadPanelLayout,
|
|
15
|
+
deriveTabId,
|
|
16
16
|
} from "./panel-utils";
|
|
17
17
|
|
|
18
18
|
/** Tab types that can only have 1 instance per project */
|
|
19
|
-
const SINGLETON_TYPES = new Set<TabType>(["git-graph"]);
|
|
19
|
+
const SINGLETON_TYPES = new Set<TabType>(["git-graph", "settings"]);
|
|
20
20
|
|
|
21
21
|
/** Tab types removed in a prior version — filter them out when loading persisted state */
|
|
22
22
|
const OBSOLETE_TAB_TYPES = new Set(["projects", "git-status"]);
|
|
23
23
|
|
|
24
|
-
function generateTabId(): string {
|
|
25
|
-
return `tab-${randomId()}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
24
|
function pushHistory(history: string[], id: string): string[] {
|
|
29
25
|
const filtered = history.filter((h) => h !== id);
|
|
30
26
|
filtered.push(id);
|
|
@@ -47,6 +43,7 @@ export interface PanelStore {
|
|
|
47
43
|
|
|
48
44
|
// Project lifecycle
|
|
49
45
|
switchProject: (projectName: string) => void;
|
|
46
|
+
reloadProject: (projectName: string) => void;
|
|
50
47
|
|
|
51
48
|
// Panel focus
|
|
52
49
|
setFocusedPanel: (panelId: string) => void;
|
|
@@ -188,6 +185,25 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
188
185
|
}
|
|
189
186
|
},
|
|
190
187
|
|
|
188
|
+
reloadProject: (projectName) => {
|
|
189
|
+
const { projectGrids, projectFocused, panels } = get();
|
|
190
|
+
// Clear in-memory cache so switchProject re-reads from localStorage
|
|
191
|
+
const newGrids = { ...projectGrids };
|
|
192
|
+
const newFocused = { ...projectFocused };
|
|
193
|
+
delete newGrids[projectName];
|
|
194
|
+
delete newFocused[projectName];
|
|
195
|
+
|
|
196
|
+
// Remove old panels belonging to this project from flat map
|
|
197
|
+
const oldGrid = projectGrids[projectName];
|
|
198
|
+
const oldPanelIds = oldGrid ? new Set(oldGrid.flat()) : new Set<string>();
|
|
199
|
+
const cleanedPanels = { ...panels };
|
|
200
|
+
for (const id of oldPanelIds) delete cleanedPanels[id];
|
|
201
|
+
|
|
202
|
+
set({ projectGrids: newGrids, projectFocused: newFocused, panels: cleanedPanels, currentProject: null });
|
|
203
|
+
// Re-trigger full load from localStorage
|
|
204
|
+
get().switchProject(projectName);
|
|
205
|
+
},
|
|
206
|
+
|
|
191
207
|
setFocusedPanel: (panelId) => {
|
|
192
208
|
if (get().panels[panelId]) set({ focusedPanelId: panelId });
|
|
193
209
|
},
|
|
@@ -197,10 +213,25 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
197
213
|
const panel = get().panels[pid];
|
|
198
214
|
if (!panel) return "";
|
|
199
215
|
|
|
200
|
-
//
|
|
216
|
+
// Terminal: compute next available index if not provided
|
|
217
|
+
if (tabDef.type === "terminal" && !tabDef.metadata?.terminalIndex) {
|
|
218
|
+
const allTabs = Object.values(get().panels).flatMap((p) => p.tabs);
|
|
219
|
+
const terminalNums = allTabs
|
|
220
|
+
.filter((t) => t.type === "terminal")
|
|
221
|
+
.map((t) => {
|
|
222
|
+
const match = t.id.match(/^terminal:(\d+)/);
|
|
223
|
+
return match ? parseInt(match[1]!, 10) : 0;
|
|
224
|
+
});
|
|
225
|
+
const nextIndex = terminalNums.length > 0 ? Math.max(...terminalNums) + 1 : 1;
|
|
226
|
+
tabDef = { ...tabDef, metadata: { ...tabDef.metadata, terminalIndex: nextIndex } };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const baseId = deriveTabId(tabDef.type, tabDef.metadata);
|
|
230
|
+
|
|
231
|
+
// Singleton check — focus existing across ALL panels
|
|
201
232
|
if (SINGLETON_TYPES.has(tabDef.type)) {
|
|
202
233
|
for (const p of Object.values(get().panels)) {
|
|
203
|
-
const existing = p.tabs.find((t) => t.
|
|
234
|
+
const existing = p.tabs.find((t) => t.id === baseId);
|
|
204
235
|
if (existing) {
|
|
205
236
|
set((s) => ({
|
|
206
237
|
focusedPanelId: p.id,
|
|
@@ -219,7 +250,30 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
219
250
|
}
|
|
220
251
|
}
|
|
221
252
|
|
|
222
|
-
|
|
253
|
+
// Non-singleton: dedup within SAME panel only
|
|
254
|
+
const currentPanel = get().panels[pid]!;
|
|
255
|
+
const existingInPanel = currentPanel.tabs.find((t) => t.id === baseId);
|
|
256
|
+
if (existingInPanel) {
|
|
257
|
+
set((s) => ({
|
|
258
|
+
panels: {
|
|
259
|
+
...s.panels,
|
|
260
|
+
[pid]: {
|
|
261
|
+
...currentPanel,
|
|
262
|
+
activeTabId: existingInPanel.id,
|
|
263
|
+
tabHistory: pushHistory(currentPanel.tabHistory, existingInPanel.id),
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
267
|
+
persist();
|
|
268
|
+
return existingInPanel.id;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Check if same base ID exists in OTHER panels (split case)
|
|
272
|
+
const existsElsewhere = Object.values(get().panels).some(
|
|
273
|
+
(p) => p.id !== pid && p.tabs.some((t) => t.id === baseId),
|
|
274
|
+
);
|
|
275
|
+
const id = existsElsewhere ? `${baseId}@${pid}` : baseId;
|
|
276
|
+
|
|
223
277
|
const tab: Tab = { ...tabDef, id };
|
|
224
278
|
set((s) => {
|
|
225
279
|
const p = s.panels[pid]!;
|