@blackbelt-technology/pi-agent-dashboard 0.5.2 → 0.5.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/AGENTS.md +19 -30
- package/README.md +69 -1
- package/docs/architecture.md +89 -165
- package/package.json +11 -7
- package/packages/extension/package.json +2 -2
- package/packages/extension/src/__tests__/bridge-default-model-gate.test.ts +47 -0
- package/packages/extension/src/__tests__/bridge-followup-chat-order.test.ts +215 -0
- package/packages/extension/src/__tests__/bridge-followup-multi-entry.test.ts +202 -0
- package/packages/extension/src/__tests__/bridge-queue-update-forward.test.ts +77 -0
- package/packages/extension/src/__tests__/bridge-retry-ordering.test.ts +148 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-drain.test.ts +221 -0
- package/packages/extension/src/__tests__/bridge-shadow-queue-gate.test.ts +299 -0
- package/packages/extension/src/__tests__/bridge-shutdown-reset.test.ts +238 -0
- package/packages/extension/src/__tests__/bridge-slash-command-routing.test.ts +127 -31
- package/packages/extension/src/__tests__/command-handler.test.ts +105 -3
- package/packages/extension/src/__tests__/fixtures/usage-limit-error-strings.ts +127 -0
- package/packages/extension/src/__tests__/source-detector.test.ts +15 -0
- package/packages/extension/src/__tests__/usage-limit-orderer.test.ts +12 -0
- package/packages/extension/src/bridge-default-model-gate.ts +32 -0
- package/packages/extension/src/bridge.ts +299 -20
- package/packages/extension/src/command-handler.ts +53 -7
- package/packages/extension/src/dashboard-default-adapter.ts +5 -0
- package/packages/extension/src/prompt-bus.ts +15 -0
- package/packages/extension/src/slash-dispatch.ts +30 -15
- package/packages/extension/src/source-detector.ts +13 -5
- package/packages/extension/src/usage-limit-orderer.ts +18 -1
- package/packages/server/bin/pi-dashboard.mjs +62 -14
- package/packages/server/package.json +9 -5
- package/packages/server/src/__tests__/browser-gateway-register-handler.test.ts +69 -0
- package/packages/server/src/__tests__/cli-env-no-clobber.test.ts +46 -0
- package/packages/server/src/__tests__/cli-no-bootstrap-references.test.ts +69 -0
- package/packages/server/src/__tests__/cli-parse.test.ts +9 -10
- package/packages/server/src/__tests__/cli-version.test.ts +151 -0
- package/packages/server/src/__tests__/directory-service-openspec-enabled.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-refresh-force.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-specs-mtime.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service-toctou.test.ts +9 -0
- package/packages/server/src/__tests__/directory-service.test.ts +9 -0
- package/packages/server/src/__tests__/doctor-route.test.ts +53 -0
- package/packages/server/src/__tests__/event-wiring-queue-state.test.ts +156 -0
- package/packages/server/src/__tests__/event-wiring-resume-clear.test.ts +105 -0
- package/packages/server/src/__tests__/health-shape.test.ts +35 -12
- package/packages/server/src/__tests__/installed-package-enricher.test.ts +12 -12
- package/packages/server/src/__tests__/is-activity-event.test.ts +4 -7
- package/packages/server/src/__tests__/package-routes.test.ts +6 -2
- package/packages/server/src/__tests__/pi-changelog-routes.test.ts +10 -13
- package/packages/server/src/__tests__/pi-core-checker.test.ts +2 -2
- package/packages/server/src/__tests__/pi-version-skew.test.ts +3 -2
- package/packages/server/src/__tests__/plugin-activation-routes.test.ts +267 -0
- package/packages/server/src/__tests__/plugin-intent-cache.test.ts +75 -0
- package/packages/server/src/__tests__/preferences-store.test.ts +196 -0
- package/packages/server/src/__tests__/reattach-placement.test.ts +9 -0
- package/packages/server/src/__tests__/recommended-routes.test.ts +2 -2
- package/packages/server/src/__tests__/recovery-server.test.ts +203 -0
- package/packages/server/src/__tests__/session-action-handler-clear-queue.test.ts +153 -0
- package/packages/server/src/__tests__/session-action-handler-headless-reload.test.ts +43 -0
- package/packages/server/src/__tests__/session-order-manager.test.ts +9 -0
- package/packages/server/src/__tests__/session-order-reboot.test.ts +9 -0
- package/packages/server/src/__tests__/session-ordering-integration.test.ts +9 -0
- package/packages/server/src/browser-gateway.ts +83 -5
- package/packages/server/src/browser-handlers/directory-handler.ts +69 -0
- package/packages/server/src/browser-handlers/session-action-handler.ts +89 -0
- package/packages/server/src/browser-handlers/subscription-handler.ts +23 -0
- package/packages/server/src/changelog-parser.ts +1 -1
- package/packages/server/src/cli.ts +68 -250
- package/packages/server/src/event-status-extraction.ts +14 -62
- package/packages/server/src/event-wiring.ts +23 -10
- package/packages/server/src/memory-session-manager.ts +4 -0
- package/packages/server/src/pi-core-checker.ts +1 -1
- package/packages/server/src/pi-dev-version-check.ts +1 -1
- package/packages/server/src/pi-version-skew.ts +24 -46
- package/packages/server/src/plugin-intent-cache.ts +67 -0
- package/packages/server/src/preferences-store.ts +199 -13
- package/packages/server/src/recovery-server.ts +366 -0
- package/packages/server/src/routes/__tests__/manifest-route.test.ts +138 -0
- package/packages/server/src/routes/doctor-routes.ts +26 -21
- package/packages/server/src/routes/manifest-route.ts +162 -0
- package/packages/server/src/routes/openspec-routes.ts +4 -25
- package/packages/server/src/routes/pi-changelog-routes.ts +5 -24
- package/packages/server/src/routes/pi-core-routes.ts +3 -23
- package/packages/server/src/routes/plugin-activation-routes.ts +193 -0
- package/packages/server/src/routes/recommended-routes.ts +21 -0
- package/packages/server/src/routes/system-routes.ts +73 -11
- package/packages/server/src/server.ts +105 -307
- package/packages/server/src/session-api.ts +5 -63
- package/packages/shared/package.json +1 -1
- package/packages/shared/src/__tests__/binary-lookup-resolveJiti.test.ts +28 -0
- package/packages/shared/src/__tests__/binary-lookup-spawn-env.test.ts +61 -0
- package/packages/shared/src/__tests__/binary-lookup.test.ts +16 -0
- package/packages/shared/src/__tests__/bridge-register.test.ts +67 -0
- package/packages/shared/src/__tests__/ci-electron-no-side-effects.test.ts +129 -0
- package/packages/shared/src/__tests__/config.test.ts +40 -0
- package/packages/shared/src/__tests__/dashboard-paths.test.ts +81 -0
- package/packages/shared/src/__tests__/ensure-windows-path.test.ts +112 -0
- package/packages/shared/src/__tests__/intent-types.test.ts +120 -0
- package/packages/shared/src/__tests__/jiti-packages-parity.test.ts +85 -0
- package/packages/shared/src/__tests__/legacy-managed-dir.test.ts +59 -0
- package/packages/shared/src/__tests__/no-direct-child-process.test.ts +12 -0
- package/packages/shared/src/__tests__/no-electron-execpath-spawn.test.ts +149 -0
- package/packages/shared/src/__tests__/no-flow-command-route-claims.test.ts +71 -0
- package/packages/shared/src/__tests__/no-flow-references-in-shell.test.ts +221 -0
- package/packages/shared/src/__tests__/no-managed-dir-reference.test.ts +134 -0
- package/packages/shared/src/__tests__/no-pi-dashboard-version-jiti-gate.test.ts +41 -0
- package/packages/shared/src/__tests__/no-primitive-direct-import.test.ts +235 -0
- package/packages/shared/src/__tests__/no-server-imports-in-resolver.test.ts +53 -0
- package/packages/shared/src/__tests__/node-spawn-jiti-contract.test.ts +54 -101
- package/packages/shared/src/__tests__/node-spawn.test.ts +29 -13
- package/packages/shared/src/__tests__/pi-package-resolver.test.ts +300 -0
- package/packages/shared/src/__tests__/plugin-activation-contracts.test.ts +74 -0
- package/packages/shared/src/__tests__/plugin-bridge-classify-source.test.ts +73 -0
- package/packages/shared/src/__tests__/plugin-bridge-register-extended.test.ts +17 -5
- package/packages/shared/src/__tests__/plugin-bridge-register-packages.test.ts +233 -0
- package/packages/shared/src/__tests__/plugin-bridge-register.test.ts +19 -9
- package/packages/shared/src/__tests__/publish-workflow-contract.test.ts +154 -15
- package/packages/shared/src/__tests__/recommended-extensions.test.ts +28 -10
- package/packages/shared/src/__tests__/resolver-parity-with-scanner.test.ts +76 -0
- package/packages/shared/src/__tests__/server-identity.test.ts +127 -0
- package/packages/shared/src/__tests__/server-launcher.test.ts +35 -0
- package/packages/shared/src/__tests__/source-matching.test.ts +5 -5
- package/packages/shared/src/__tests__/sync-versions-spec.test.ts +76 -0
- package/packages/shared/src/__tests__/tool-registry-definitions.test.ts +50 -2
- package/packages/shared/src/bridge-register.ts +35 -2
- package/packages/shared/src/browser-protocol.ts +176 -2
- package/packages/shared/src/config.ts +12 -0
- package/packages/shared/src/dashboard-paths.ts +69 -0
- package/packages/shared/src/dashboard-plugin/index.ts +2 -0
- package/packages/shared/src/dashboard-plugin/intent-types.ts +93 -0
- package/packages/shared/src/dashboard-plugin/manifest-types.ts +55 -1
- package/packages/shared/src/dashboard-plugin/plugin-status.ts +82 -0
- package/packages/shared/src/dashboard-plugin/slot-props.ts +11 -0
- package/packages/shared/src/dashboard-plugin/slot-types.ts +16 -2
- package/packages/shared/src/dashboard-plugin/ui-primitives.ts +287 -0
- package/packages/shared/src/dashboard-starter.ts +22 -0
- package/packages/shared/src/doctor-core.ts +49 -27
- package/packages/shared/src/launch-source-types.ts +9 -9
- package/packages/shared/src/legacy-managed-dir.ts +97 -0
- package/packages/shared/src/mdns-discovery.ts +4 -1
- package/packages/shared/src/pi-package-resolver.ts +388 -0
- package/packages/shared/src/platform/binary-lookup.ts +27 -3
- package/packages/shared/src/platform/ensure-windows-path.ts +95 -0
- package/packages/shared/src/platform/exec.ts +22 -0
- package/packages/shared/src/platform/node-spawn.ts +42 -41
- package/packages/shared/src/plugin-bridge-register.ts +275 -18
- package/packages/shared/src/protocol.ts +94 -2
- package/packages/shared/src/recommended-extensions.ts +34 -10
- package/packages/shared/src/server-identity.ts +74 -5
- package/packages/shared/src/server-launcher.ts +20 -0
- package/packages/shared/src/source-matching.ts +1 -1
- package/packages/shared/src/tool-registry/__tests__/node-script-toargv-fallback.test.ts +84 -0
- package/packages/shared/src/tool-registry/definitions.ts +91 -7
- package/packages/shared/src/types.ts +12 -8
- package/scripts/maybe-patch-package.cjs +44 -0
- package/packages/server/src/__tests__/bootstrap-install-from-list.test.ts +0 -263
- package/packages/server/src/__tests__/bootstrap-queue.test.ts +0 -120
- package/packages/server/src/__tests__/bootstrap-routes.test.ts +0 -125
- package/packages/server/src/__tests__/bootstrap-state.test.ts +0 -119
- package/packages/server/src/__tests__/cli-bootstrap.test.ts +0 -36
- package/packages/server/src/__tests__/event-status-extraction-flow.test.ts +0 -55
- package/packages/server/src/__tests__/legacy-pi-cleanup.test.ts +0 -149
- package/packages/server/src/__tests__/post-install-openspec-refresh.test.ts +0 -180
- package/packages/server/src/__tests__/post-install-rescan.test.ts +0 -134
- package/packages/server/src/__tests__/system-routes-reextract.test.ts +0 -91
- package/packages/server/src/bootstrap-install-from-list.ts +0 -232
- package/packages/server/src/bootstrap-queue.ts +0 -130
- package/packages/server/src/bootstrap-state.ts +0 -159
- package/packages/server/src/legacy-pi-cleanup.ts +0 -151
- package/packages/server/src/routes/bootstrap-routes.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/README.md +0 -133
- package/packages/shared/src/__tests__/bootstrap/__snapshots__/cube.test.ts.snap +0 -378
- package/packages/shared/src/__tests__/bootstrap/assertions.ts +0 -136
- package/packages/shared/src/__tests__/bootstrap/cube.test.ts +0 -47
- package/packages/shared/src/__tests__/bootstrap/cube.ts +0 -66
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/a-electron.test.ts.snap +0 -84
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/b-npm-global.test.ts.snap +0 -90
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/c-dev-monorepo.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/d-overrides.test.ts.snap +0 -20
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/e-stale-partial.test.ts.snap +0 -62
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/f-cwd-variants.test.ts.snap +0 -34
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/g-windows-specifics.test.ts.snap +0 -49
- package/packages/shared/src/__tests__/bootstrap/families/__snapshots__/j-path-gui-minimal.test.ts.snap +0 -12
- package/packages/shared/src/__tests__/bootstrap/families/a-electron.test.ts +0 -156
- package/packages/shared/src/__tests__/bootstrap/families/b-npm-global.test.ts +0 -157
- package/packages/shared/src/__tests__/bootstrap/families/c-dev-monorepo.test.ts +0 -102
- package/packages/shared/src/__tests__/bootstrap/families/d-overrides.test.ts +0 -76
- package/packages/shared/src/__tests__/bootstrap/families/e-stale-partial.test.ts +0 -94
- package/packages/shared/src/__tests__/bootstrap/families/f-cwd-variants.test.ts +0 -87
- package/packages/shared/src/__tests__/bootstrap/families/g-windows-specifics.test.ts +0 -143
- package/packages/shared/src/__tests__/bootstrap/families/h-home-drift.test.ts +0 -64
- package/packages/shared/src/__tests__/bootstrap/families/i-malformed-settings.test.ts +0 -77
- package/packages/shared/src/__tests__/bootstrap/families/index.ts +0 -19
- package/packages/shared/src/__tests__/bootstrap/families/j-path-gui-minimal.test.ts +0 -61
- package/packages/shared/src/__tests__/bootstrap/families/k-dashboard-absent.test.ts +0 -50
- package/packages/shared/src/__tests__/bootstrap/families/l-instance-coordination.test.ts +0 -272
- package/packages/shared/src/__tests__/bootstrap/fixtures/dev-monorepo.ts +0 -58
- package/packages/shared/src/__tests__/bootstrap/fixtures/electron-layout.ts +0 -84
- package/packages/shared/src/__tests__/bootstrap/fixtures/index.ts +0 -9
- package/packages/shared/src/__tests__/bootstrap/fixtures/managed-install.ts +0 -85
- package/packages/shared/src/__tests__/bootstrap/fixtures/npm-global-layout.ts +0 -122
- package/packages/shared/src/__tests__/bootstrap/fixtures/pi-versions.ts +0 -36
- package/packages/shared/src/__tests__/bootstrap/fixtures/settings-json.ts +0 -39
- package/packages/shared/src/__tests__/bootstrap/harness.smoke.test.ts +0 -220
- package/packages/shared/src/__tests__/bootstrap/harness.ts +0 -413
- package/packages/shared/src/__tests__/bootstrap/scenarios-skipped.ts +0 -125
- package/packages/shared/src/__tests__/bootstrap/scenarios.ts +0 -132
- package/packages/shared/src/__tests__/bootstrap-install-resolve-npm.test.ts +0 -72
- package/packages/shared/src/__tests__/install-managed-node-bootstrap-order.test.ts +0 -68
- package/packages/shared/src/__tests__/install-managed-node.test.ts +0 -192
- package/packages/shared/src/__tests__/installable-list.test.ts +0 -130
- package/packages/shared/src/__tests__/no-installable-list-in-bridge.test.ts +0 -52
- package/packages/shared/src/bootstrap-install.ts +0 -406
- package/packages/shared/src/installable-list.ts +0 -152
- package/packages/shared/src/launch-source-flag.ts +0 -14
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side cache of the most recent plugin intent per
|
|
3
|
+
* (pluginId, sessionId, slot). Used to replay current state to
|
|
4
|
+
* reconnecting clients on subscribe.
|
|
5
|
+
*
|
|
6
|
+
* See change: adopt-server-driven-intent-rendering.
|
|
7
|
+
*/
|
|
8
|
+
import type { IntentNode } from "@blackbelt-technology/pi-dashboard-shared/dashboard-plugin/intent-types.js";
|
|
9
|
+
import type { SlotId } from "@blackbelt-technology/pi-dashboard-shared/dashboard-plugin/slot-types.js";
|
|
10
|
+
|
|
11
|
+
export interface CachedIntentEntry {
|
|
12
|
+
pluginId: string;
|
|
13
|
+
sessionId: string | null;
|
|
14
|
+
slot: SlotId;
|
|
15
|
+
intent: IntentNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function key(pluginId: string, sessionId: string | null, slot: SlotId): string {
|
|
19
|
+
return `${pluginId}|${sessionId ?? ""}|${slot}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class PluginIntentCache {
|
|
23
|
+
private map = new Map<string, CachedIntentEntry>();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Store the intent for a (pluginId, sessionId, slot) tuple.
|
|
27
|
+
* If `intent` is null, remove the entry (the plugin is clearing its
|
|
28
|
+
* contribution to that slot).
|
|
29
|
+
*/
|
|
30
|
+
set(pluginId: string, sessionId: string | null, slot: SlotId, intent: IntentNode | null): void {
|
|
31
|
+
const k = key(pluginId, sessionId, slot);
|
|
32
|
+
if (intent === null) {
|
|
33
|
+
this.map.delete(k);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.map.set(k, { pluginId, sessionId, slot, intent });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Return every intent currently cached for a given session. */
|
|
40
|
+
getForSession(sessionId: string | null): CachedIntentEntry[] {
|
|
41
|
+
const out: CachedIntentEntry[] = [];
|
|
42
|
+
for (const entry of this.map.values()) {
|
|
43
|
+
if (entry.sessionId === sessionId) out.push(entry);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Return EVERY cached intent (e.g. for global slots with sessionId=null). */
|
|
49
|
+
getAll(): CachedIntentEntry[] {
|
|
50
|
+
return Array.from(this.map.values());
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Remove every entry for a given session (called on session removal). */
|
|
54
|
+
clearForSession(sessionId: string | null): void {
|
|
55
|
+
for (const [k, entry] of this.map) {
|
|
56
|
+
if (entry.sessionId === sessionId) this.map.delete(k);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Test-only: clear the entire cache. */
|
|
61
|
+
reset(): void {
|
|
62
|
+
this.map.clear();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Module singleton — every call site shares the same cache. */
|
|
67
|
+
export const pluginIntentCache = new PluginIntentCache();
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global UI preferences store — JSON-backed with debounced writes.
|
|
3
|
-
* Stores cross-session state: pinned directories
|
|
3
|
+
* Stores cross-session state: pinned directories, session ordering, and
|
|
4
|
+
* folder-workspaces (named, collapsible containers grouping folders).
|
|
5
|
+
*
|
|
6
|
+
* Workspace membership is authoritative and orthogonal to pinning — see
|
|
7
|
+
* change: folder-workspaces. A folder may live in `pinnedDirectories`
|
|
8
|
+
* AND a workspace's `folders[]` independently; the two lists do not
|
|
9
|
+
* deduplicate against each other.
|
|
10
|
+
*
|
|
4
11
|
* Replaces `state-store.ts` (hidden state moved to per-session `.meta.json`).
|
|
5
12
|
*/
|
|
6
13
|
import path from "node:path";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
7
15
|
import { CONFIG_DIR } from "@blackbelt-technology/pi-dashboard-shared/config.js";
|
|
16
|
+
import type { Workspace } from "@blackbelt-technology/pi-dashboard-shared/browser-protocol.js";
|
|
8
17
|
import { readJsonFile, writeJsonFile } from "./json-store.js";
|
|
9
18
|
import { safeRealpathSync } from "./resolve-path.js";
|
|
10
19
|
import { normalizePath } from "@blackbelt-technology/pi-dashboard-shared/platform/paths.js";
|
|
11
20
|
|
|
12
21
|
export const PREFERENCES_FILE = path.join(CONFIG_DIR, "preferences.json");
|
|
13
22
|
|
|
23
|
+
const NAME_MAX = 80;
|
|
24
|
+
|
|
14
25
|
interface PreferencesData {
|
|
15
26
|
sessionOrder: Record<string, string[]>;
|
|
16
27
|
pinnedDirectories: string[];
|
|
28
|
+
workspaces?: Workspace[];
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
export interface PreferencesStore {
|
|
@@ -24,14 +36,79 @@ export interface PreferencesStore {
|
|
|
24
36
|
pinDirectory(dirPath: string): void;
|
|
25
37
|
unpinDirectory(dirPath: string): void;
|
|
26
38
|
reorderPinnedDirs(dirs: string[]): void;
|
|
39
|
+
// ── folder-workspaces ────────────────────────────────────────────
|
|
40
|
+
getWorkspaces(): Workspace[];
|
|
41
|
+
/** Returns the created workspace, or null on invalid name. */
|
|
42
|
+
createWorkspace(name: string): Workspace | null;
|
|
43
|
+
/** Returns true on mutation, false on unknown id / invalid name. */
|
|
44
|
+
renameWorkspace(id: string, name: string): boolean;
|
|
45
|
+
/** Returns true on mutation, false on unknown id. */
|
|
46
|
+
deleteWorkspace(id: string): boolean;
|
|
47
|
+
/** Returns true on mutation, false on unknown id or no-op (same value). */
|
|
48
|
+
setWorkspaceCollapsed(id: string, collapsed: boolean): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Adds `path` to workspace `id`. Single-membership invariant: removes
|
|
51
|
+
* the canonicalized path from every other workspace first. Returns
|
|
52
|
+
* true on mutation, false on unknown id or already-member (no-op).
|
|
53
|
+
*/
|
|
54
|
+
addFolderToWorkspace(id: string, dirPath: string): boolean;
|
|
55
|
+
/** Returns true on mutation, false on unknown id or not-member. */
|
|
56
|
+
removeFolderFromWorkspace(id: string, dirPath: string): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Replaces a workspace's folder order. Rejected if `paths` does not
|
|
59
|
+
* equal the current member set (after canonicalization). Returns true
|
|
60
|
+
* on mutation, false otherwise.
|
|
61
|
+
*/
|
|
62
|
+
reorderWorkspaceFolders(id: string, paths: string[]): boolean;
|
|
63
|
+
/** Reorders workspaces. Rejected if `ids` doesn't equal current id set. */
|
|
64
|
+
reorderWorkspaces(ids: string[]): boolean;
|
|
27
65
|
flush(): void;
|
|
28
66
|
dispose(): void;
|
|
29
67
|
}
|
|
30
68
|
|
|
31
69
|
const DEBOUNCE_MS = 1000;
|
|
32
70
|
|
|
71
|
+
function canonicalize(p: string): string {
|
|
72
|
+
// IMPORTANT: wrap normalizePath in an arrow so Array.prototype.map's
|
|
73
|
+
// (element, index, array) signature does not leak `index: number` into
|
|
74
|
+
// its 2nd `platform` parameter. See preferences-store git blame.
|
|
75
|
+
return safeRealpathSync(normalizePath(p));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function dedupePreserveOrder(arr: string[]): string[] {
|
|
79
|
+
return [...new Set(arr)];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setEquals(a: string[], b: string[]): boolean {
|
|
83
|
+
if (a.length !== b.length) return false;
|
|
84
|
+
const sa = new Set(a);
|
|
85
|
+
for (const x of b) if (!sa.has(x)) return false;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sanitizeName(input: unknown): string | null {
|
|
90
|
+
if (typeof input !== "string") return null;
|
|
91
|
+
const trimmed = input.trim();
|
|
92
|
+
if (trimmed.length === 0 || trimmed.length > NAME_MAX) return null;
|
|
93
|
+
return trimmed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeWorkspaceOnLoad(ws: Workspace): Workspace {
|
|
97
|
+
const folders = dedupePreserveOrder((ws.folders ?? []).map((p) => canonicalize(p)));
|
|
98
|
+
return {
|
|
99
|
+
id: typeof ws.id === "string" && ws.id.length > 0 ? ws.id : `ws_${randomUUID()}`,
|
|
100
|
+
name: typeof ws.name === "string" ? ws.name : "",
|
|
101
|
+
collapsed: Boolean(ws.collapsed),
|
|
102
|
+
folders,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
33
106
|
export function createPreferencesStore(filePath: string = PREFERENCES_FILE): PreferencesStore {
|
|
34
|
-
const data: PreferencesData = readJsonFile<PreferencesData>(filePath, {
|
|
107
|
+
const data: PreferencesData = readJsonFile<PreferencesData>(filePath, {
|
|
108
|
+
sessionOrder: {},
|
|
109
|
+
pinnedDirectories: [],
|
|
110
|
+
workspaces: [],
|
|
111
|
+
});
|
|
35
112
|
let sessionOrder: Record<string, string[]> = data.sessionOrder ?? {};
|
|
36
113
|
// Normalize + resolve symlinks in stored pinned paths on load. Normalize
|
|
37
114
|
// FIRST so cosmetic drift (trailing separator, mixed separators,
|
|
@@ -40,19 +117,24 @@ export function createPreferencesStore(filePath: string = PREFERENCES_FILE): Pre
|
|
|
40
117
|
// not-yet-existing paths, so we keep its best-effort fallback.
|
|
41
118
|
// See change: platform-path-normalization.
|
|
42
119
|
const rawPinned = data.pinnedDirectories ?? [];
|
|
43
|
-
// IMPORTANT: wrap in arrow fn — `Array.prototype.map` passes `(element,
|
|
44
|
-
// index, array)`, and `normalizePath`'s 2nd param is a `platform:
|
|
45
|
-
// NodeJS.Platform`. Passing the index (a number) silently disables the
|
|
46
|
-
// Windows branch at runtime.
|
|
47
120
|
let pinnedDirectories: string[] = rawPinned
|
|
48
121
|
.map((p) => normalizePath(p))
|
|
49
122
|
.map((p) => safeRealpathSync(p));
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
123
|
+
pinnedDirectories = dedupePreserveOrder(pinnedDirectories);
|
|
124
|
+
|
|
125
|
+
const rawWorkspaces = Array.isArray(data.workspaces) ? data.workspaces : [];
|
|
126
|
+
let workspaces: Workspace[] = rawWorkspaces.map(normalizeWorkspaceOnLoad);
|
|
54
127
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
55
|
-
let dirty =
|
|
128
|
+
let dirty =
|
|
129
|
+
pinnedDirectories.length !== rawPinned.length ||
|
|
130
|
+
pinnedDirectories.some((p, i) => p !== rawPinned[i]) ||
|
|
131
|
+
workspaces.length !== rawWorkspaces.length ||
|
|
132
|
+
workspaces.some((ws, i) => {
|
|
133
|
+
const raw = rawWorkspaces[i];
|
|
134
|
+
if (!raw) return true;
|
|
135
|
+
const rf = (raw.folders ?? []) as string[];
|
|
136
|
+
return ws.folders.length !== rf.length || ws.folders.some((f, j) => f !== rf[j]);
|
|
137
|
+
});
|
|
56
138
|
|
|
57
139
|
function scheduleSave(): void {
|
|
58
140
|
dirty = true;
|
|
@@ -61,7 +143,7 @@ export function createPreferencesStore(filePath: string = PREFERENCES_FILE): Pre
|
|
|
61
143
|
debounceTimer = null;
|
|
62
144
|
if (dirty) {
|
|
63
145
|
dirty = false;
|
|
64
|
-
writeJsonFile(filePath, { sessionOrder, pinnedDirectories } satisfies PreferencesData);
|
|
146
|
+
writeJsonFile(filePath, { sessionOrder, pinnedDirectories, workspaces } satisfies PreferencesData);
|
|
65
147
|
}
|
|
66
148
|
}, DEBOUNCE_MS);
|
|
67
149
|
}
|
|
@@ -73,12 +155,16 @@ export function createPreferencesStore(filePath: string = PREFERENCES_FILE): Pre
|
|
|
73
155
|
}
|
|
74
156
|
if (dirty) {
|
|
75
157
|
dirty = false;
|
|
76
|
-
writeJsonFile(filePath, { sessionOrder, pinnedDirectories } satisfies PreferencesData);
|
|
158
|
+
writeJsonFile(filePath, { sessionOrder, pinnedDirectories, workspaces } satisfies PreferencesData);
|
|
77
159
|
}
|
|
78
160
|
}
|
|
79
161
|
|
|
80
162
|
if (dirty) scheduleSave();
|
|
81
163
|
|
|
164
|
+
function findWs(id: string): Workspace | undefined {
|
|
165
|
+
return workspaces.find((w) => w.id === id);
|
|
166
|
+
}
|
|
167
|
+
|
|
82
168
|
return {
|
|
83
169
|
getSessionOrder(): Record<string, string[]> {
|
|
84
170
|
return sessionOrder;
|
|
@@ -116,6 +202,106 @@ export function createPreferencesStore(filePath: string = PREFERENCES_FILE): Pre
|
|
|
116
202
|
scheduleSave();
|
|
117
203
|
},
|
|
118
204
|
|
|
205
|
+
// ── folder-workspaces ───────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
getWorkspaces(): Workspace[] {
|
|
208
|
+
// Deep-ish clone so callers can't mutate internal state.
|
|
209
|
+
return workspaces.map((w) => ({ ...w, folders: [...w.folders] }));
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
createWorkspace(name: string): Workspace | null {
|
|
213
|
+
const clean = sanitizeName(name);
|
|
214
|
+
if (clean === null) return null;
|
|
215
|
+
const ws: Workspace = {
|
|
216
|
+
id: `ws_${randomUUID()}`,
|
|
217
|
+
name: clean,
|
|
218
|
+
collapsed: false,
|
|
219
|
+
folders: [],
|
|
220
|
+
};
|
|
221
|
+
workspaces.push(ws);
|
|
222
|
+
scheduleSave();
|
|
223
|
+
return { ...ws, folders: [...ws.folders] };
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
renameWorkspace(id: string, name: string): boolean {
|
|
227
|
+
const clean = sanitizeName(name);
|
|
228
|
+
if (clean === null) return false;
|
|
229
|
+
const ws = findWs(id);
|
|
230
|
+
if (!ws) return false;
|
|
231
|
+
if (ws.name === clean) return false;
|
|
232
|
+
ws.name = clean;
|
|
233
|
+
scheduleSave();
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
deleteWorkspace(id: string): boolean {
|
|
238
|
+
const idx = workspaces.findIndex((w) => w.id === id);
|
|
239
|
+
if (idx === -1) return false;
|
|
240
|
+
workspaces.splice(idx, 1);
|
|
241
|
+
scheduleSave();
|
|
242
|
+
return true;
|
|
243
|
+
},
|
|
244
|
+
|
|
245
|
+
setWorkspaceCollapsed(id: string, collapsed: boolean): boolean {
|
|
246
|
+
const ws = findWs(id);
|
|
247
|
+
if (!ws) return false;
|
|
248
|
+
if (ws.collapsed === collapsed) return false;
|
|
249
|
+
ws.collapsed = collapsed;
|
|
250
|
+
scheduleSave();
|
|
251
|
+
return true;
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
addFolderToWorkspace(id: string, dirPath: string): boolean {
|
|
255
|
+
const ws = findWs(id);
|
|
256
|
+
if (!ws) return false;
|
|
257
|
+
const canon = canonicalize(dirPath);
|
|
258
|
+
if (ws.folders.includes(canon)) return false;
|
|
259
|
+
// Single-membership invariant: detach from every OTHER workspace
|
|
260
|
+
// first. Idempotent — no-op if not currently in any other workspace.
|
|
261
|
+
for (const other of workspaces) {
|
|
262
|
+
if (other.id === id) continue;
|
|
263
|
+
const i = other.folders.indexOf(canon);
|
|
264
|
+
if (i !== -1) other.folders.splice(i, 1);
|
|
265
|
+
}
|
|
266
|
+
ws.folders.push(canon);
|
|
267
|
+
scheduleSave();
|
|
268
|
+
return true;
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
removeFolderFromWorkspace(id: string, dirPath: string): boolean {
|
|
272
|
+
const ws = findWs(id);
|
|
273
|
+
if (!ws) return false;
|
|
274
|
+
const canon = canonicalize(dirPath);
|
|
275
|
+
const i = ws.folders.indexOf(canon);
|
|
276
|
+
if (i === -1) return false;
|
|
277
|
+
ws.folders.splice(i, 1);
|
|
278
|
+
scheduleSave();
|
|
279
|
+
return true;
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
reorderWorkspaceFolders(id: string, paths: string[]): boolean {
|
|
283
|
+
const ws = findWs(id);
|
|
284
|
+
if (!ws) return false;
|
|
285
|
+
const canon = paths.map((p) => canonicalize(p));
|
|
286
|
+
// Reject if the supplied set != current set.
|
|
287
|
+
if (!setEquals(canon, ws.folders)) return false;
|
|
288
|
+
// Reject duplicates within the new order.
|
|
289
|
+
if (new Set(canon).size !== canon.length) return false;
|
|
290
|
+
ws.folders = canon;
|
|
291
|
+
scheduleSave();
|
|
292
|
+
return true;
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
reorderWorkspaces(ids: string[]): boolean {
|
|
296
|
+
const currentIds = workspaces.map((w) => w.id);
|
|
297
|
+
if (!setEquals(ids, currentIds)) return false;
|
|
298
|
+
if (new Set(ids).size !== ids.length) return false;
|
|
299
|
+
const byId = new Map(workspaces.map((w) => [w.id, w] as const));
|
|
300
|
+
workspaces = ids.map((id) => byId.get(id)!).filter(Boolean) as Workspace[];
|
|
301
|
+
scheduleSave();
|
|
302
|
+
return true;
|
|
303
|
+
},
|
|
304
|
+
|
|
119
305
|
flush(): void {
|
|
120
306
|
flushNow();
|
|
121
307
|
},
|