@baique/pi-web-sky 0.1.35 → 0.1.36
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +16 -16
- package/.next/build-manifest.json +3 -3
- package/.next/react-loadable-manifest.json +1 -1
- package/.next/required-server-files.js +1 -1
- package/.next/required-server-files.json +1 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +3 -3
- package/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +3 -3
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/agent/[id]/bash-output/route.js +1 -1
- package/.next/server/app/api/agent/[id]/events/route.js +1 -1
- package/.next/server/app/api/agent/[id]/route.js +1 -1
- package/.next/server/app/api/app-update/route.js +1 -1
- package/.next/server/app/api/auth/login/[provider]/route.js +1 -1
- package/.next/server/app/api/cwd/validate/route.js +1 -1
- package/.next/server/app/api/default-cwd/route.js +1 -1
- package/.next/server/app/api/file-index/route.js +1 -1
- package/.next/server/app/api/files/[...path]/route.js +1 -1
- package/.next/server/app/api/mcp/route.js +1 -1
- package/.next/server/app/api/models/route.js +1 -1
- package/.next/server/app/api/plugins/route.js +1 -1
- package/.next/server/app/api/project-trust/route.js +1 -1
- package/.next/server/app/api/search/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/auto-name/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/context/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/entries/[entryId]/thinking/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/state/route.js +1 -1
- package/.next/server/app/api/sessions/[id]/todos/route.js +1 -1
- package/.next/server/app/api/sessions/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js +1 -1
- package/.next/server/app/api/skills/check/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/install/route.js +1 -1
- package/.next/server/app/api/skills/route.js +2 -31
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/update/route.js +1 -1
- package/.next/server/app/api/skills/update/route.js.nft.json +1 -1
- package/.next/server/app/api/worktrees/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +5 -5
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +5 -5
- package/.next/server/app/index.segments/_full.segment.rsc +5 -5
- package/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +16 -16
- package/.next/server/chunks/2404.js +1 -1
- package/.next/server/chunks/3500.js +30 -0
- package/.next/server/chunks/4226.js +2 -2
- package/.next/server/chunks/542.js +10 -5
- package/.next/server/chunks/5794.js +1 -1
- package/.next/server/chunks/6429.js +1 -1
- package/.next/server/chunks/898.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/middleware-react-loadable-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/static/chunks/5315.7e72d26a3ad3bd9a.js +4 -0
- package/.next/static/chunks/app/{layout-1d9a7a44fbc92d2e.js → layout-ebbc0d287267b07b.js} +1 -1
- package/.next/static/chunks/app/{page-6d6779a81a0a7639.js → page-e568623a44c67606.js} +2 -2
- package/.next/static/chunks/{webpack-fa4f40085b1ec1ac.js → webpack-2e4e348867a555cd.js} +1 -1
- package/.next/static/css/422c7dc0faa04d89.css +1 -0
- package/.next/trace +8 -9
- package/.next/trace-build +1 -1
- package/bin/pi-web-sky.js +15 -20
- package/lib/agent-client.test.mjs +64 -0
- package/lib/agent-client.ts +53 -0
- package/lib/agent-event-connection.test.mjs +193 -0
- package/lib/agent-event-connection.ts +202 -0
- package/lib/agent-event-stream.test.mjs +155 -0
- package/lib/agent-event-stream.ts +132 -0
- package/lib/agent-event-wire.test.mjs +278 -0
- package/lib/agent-event-wire.ts +107 -0
- package/lib/agent-phase.ts +24 -0
- package/lib/allowed-roots.ts +31 -0
- package/lib/ansi.test.mjs +52 -0
- package/lib/ansi.ts +211 -0
- package/lib/api-types.ts +139 -0
- package/lib/app-update.test.mjs +27 -0
- package/lib/app-update.ts +28 -0
- package/lib/atomic-file.test.mjs +37 -0
- package/lib/atomic-file.ts +34 -0
- package/lib/audit-session.ts +235 -0
- package/lib/bash-output.test.mjs +63 -0
- package/lib/bash-output.ts +53 -0
- package/lib/bg-image.ts +129 -0
- package/lib/board-events.ts +83 -0
- package/lib/board-purge.test.mjs +66 -0
- package/lib/board-purge.ts +65 -0
- package/lib/board-search.test.mjs +110 -0
- package/lib/board-search.ts +63 -0
- package/lib/board-store.test.mjs +542 -0
- package/lib/board-store.ts +794 -0
- package/lib/board-types.ts +96 -0
- package/lib/board-utils.ts +17 -0
- package/lib/bounded-form-data.test.mjs +54 -0
- package/lib/bounded-form-data.ts +49 -0
- package/lib/browser-notifications.test.mjs +167 -0
- package/lib/browser-notifications.ts +104 -0
- package/lib/chat-lazy-load.test.mjs +140 -0
- package/lib/chat-lazy-load.ts +64 -0
- package/lib/clipboard-paths.test.mjs +140 -0
- package/lib/clipboard-paths.ts +192 -0
- package/lib/clipboard.ts +18 -0
- package/lib/compaction-summary.test.mjs +44 -0
- package/lib/compaction-summary.ts +34 -0
- package/lib/custom-ui-terminal.test.mjs +32 -0
- package/lib/custom-ui-terminal.ts +27 -0
- package/lib/deepseek-pricing.test.mjs +120 -0
- package/lib/deepseek-pricing.ts +71 -0
- package/lib/directory-browser.test.mjs +60 -0
- package/lib/directory-browser.ts +83 -0
- package/lib/draft-stash.test.mjs +91 -0
- package/lib/draft-stash.ts +102 -0
- package/lib/draft-store.ts +105 -0
- package/lib/dropdown-direction.ts +27 -0
- package/lib/extension-command.test.mjs +63 -0
- package/lib/extension-command.ts +81 -0
- package/lib/file-access.test.mjs +31 -0
- package/lib/file-access.ts +59 -0
- package/lib/file-dirent.test.mjs +56 -0
- package/lib/file-dirent.ts +17 -0
- package/lib/file-explorer-state.test.mjs +63 -0
- package/lib/file-explorer-state.ts +36 -0
- package/lib/file-fuzzy.test.mjs +29 -0
- package/lib/file-fuzzy.ts +189 -0
- package/lib/file-links.test.mjs +92 -0
- package/lib/file-links.ts +148 -0
- package/lib/file-paths.ts +43 -0
- package/lib/file-types.test.mjs +34 -0
- package/lib/file-types.ts +73 -0
- package/lib/file-upload.test.mjs +70 -0
- package/lib/file-upload.ts +59 -0
- package/lib/file-viewer-state.test.mjs +20 -0
- package/lib/file-viewer-state.ts +15 -0
- package/lib/frontmatter.test.mjs +55 -0
- package/lib/frontmatter.ts +87 -0
- package/lib/git-changes.test.mjs +52 -0
- package/lib/git-changes.ts +232 -0
- package/lib/git-status.ts +47 -0
- package/lib/git-types.ts +29 -0
- package/lib/http-dispatcher.test.mjs +83 -0
- package/lib/http-dispatcher.ts +86 -0
- package/lib/i18n/format.test.mjs +19 -0
- package/lib/i18n/format.ts +60 -0
- package/lib/i18n/messages/en.ts +662 -0
- package/lib/i18n/messages/zh-CN.ts +661 -0
- package/lib/i18n/registry.test.mjs +32 -0
- package/lib/i18n/registry.ts +43 -0
- package/lib/i18n/types.ts +15 -0
- package/lib/image-attachments.test.mjs +27 -0
- package/lib/image-attachments.ts +56 -0
- package/lib/initial-navigation.test.mjs +65 -0
- package/lib/initial-navigation.ts +15 -0
- package/lib/markdown.test.mjs +104 -0
- package/lib/markdown.ts +347 -0
- package/lib/message-display.test.mjs +130 -0
- package/lib/message-display.ts +47 -0
- package/lib/model-catalog.test.mjs +327 -0
- package/lib/model-catalog.ts +414 -0
- package/lib/model-discovery-auth.ts +58 -0
- package/lib/model-discovery.test.mjs +50 -0
- package/lib/model-discovery.ts +75 -0
- package/lib/model-id-preference.ts +46 -0
- package/lib/model-scope.test.mjs +193 -0
- package/lib/model-scope.ts +198 -0
- package/lib/models-cache.test.mjs +122 -0
- package/lib/models-cache.ts +90 -0
- package/lib/models-config-store.test.mjs +200 -0
- package/lib/models-config-store.ts +84 -0
- package/lib/next-config-esm.test.mjs +14 -0
- package/lib/next-config.test.mjs +13 -0
- package/lib/node-version.test.mjs +33 -0
- package/lib/normalize.test.mjs +54 -0
- package/lib/normalize.ts +63 -0
- package/lib/npx.ts +62 -0
- package/lib/panel-layout.test.mjs +56 -0
- package/lib/panel-layout.ts +52 -0
- package/lib/patch.test.mjs +110 -0
- package/lib/patch.ts +134 -0
- package/lib/path-security.ts +42 -0
- package/lib/paths.test.mjs +58 -0
- package/lib/paths.ts +71 -0
- package/lib/pi-types.ts +183 -0
- package/lib/pi-web-options.test.mjs +63 -0
- package/lib/process-lifecycle.test.mjs +83 -0
- package/lib/project-command-env.test.mjs +298 -0
- package/lib/project-command-env.ts +129 -0
- package/lib/project-groups.test.mjs +60 -0
- package/lib/project-groups.ts +53 -0
- package/lib/project-identity.test.mjs +37 -0
- package/lib/project-identity.ts +26 -0
- package/lib/project-tree.test.mjs +115 -0
- package/lib/project-tree.ts +176 -0
- package/lib/project-trust.test.mjs +117 -0
- package/lib/project-trust.ts +48 -0
- package/lib/prompt-recovery.test.mjs +33 -0
- package/lib/prompt-recovery.ts +47 -0
- package/lib/provider-api-key-route.test.mjs +12 -0
- package/lib/provider-credential-store.test.mjs +126 -0
- package/lib/provider-credential-store.ts +114 -0
- package/lib/provider-listing-runtime.ts +37 -0
- package/lib/provider-listing.test.mjs +114 -0
- package/lib/provider-listing.ts +118 -0
- package/lib/provider-services.ts +64 -0
- package/lib/request-security.test.mjs +256 -0
- package/lib/request-security.ts +136 -0
- package/lib/rpc-manager-shutdown.test.mjs +372 -0
- package/lib/rpc-manager-widgets.test.mjs +342 -0
- package/lib/rpc-manager.test.mjs +139 -0
- package/lib/rpc-manager.ts +1703 -0
- package/lib/rpc-session-info.test.mjs +93 -0
- package/lib/session-delete.test.mjs +53 -0
- package/lib/session-delete.ts +86 -0
- package/lib/session-file-references-core.ts +82 -0
- package/lib/session-file-references.test.mjs +91 -0
- package/lib/session-file-references.ts +29 -0
- package/lib/session-path.test.mjs +24 -0
- package/lib/session-path.ts +11 -0
- package/lib/session-reader.pagination.test.mjs +115 -0
- package/lib/session-reader.test.mjs +446 -0
- package/lib/session-reader.ts +548 -0
- package/lib/session-resolve.test.mjs +65 -0
- package/lib/session-row-context-menu.test.mjs +46 -0
- package/lib/session-row-context-menu.ts +29 -0
- package/lib/session-scanner.test.mjs +151 -0
- package/lib/session-scanner.ts +238 -0
- package/lib/session-search.test.mjs +173 -0
- package/lib/session-search.ts +284 -0
- package/lib/session-stats.ts +26 -0
- package/lib/session-timing.test.mjs +107 -0
- package/lib/session-timing.ts +45 -0
- package/lib/session-title.test.mjs +284 -0
- package/lib/session-title.ts +255 -0
- package/lib/skill-frontmatter.test.mjs +75 -0
- package/lib/skill-frontmatter.ts +40 -0
- package/lib/skill-lock.test.mjs +182 -0
- package/lib/skill-lock.ts +146 -0
- package/lib/skill-updates.test.mjs +182 -0
- package/lib/skill-updates.ts +263 -0
- package/lib/skills-service.ts +16 -0
- package/lib/slash-display.test.mjs +122 -0
- package/lib/slash-display.ts +30 -0
- package/lib/sqlite-db.test.mjs +140 -0
- package/lib/sqlite-db.ts +205 -0
- package/lib/startup-preferences.test.mjs +130 -0
- package/lib/startup-preferences.ts +65 -0
- package/lib/streaming-message.test.mjs +212 -0
- package/lib/streaming-message.ts +151 -0
- package/lib/subagent-widget.test.mjs +87 -0
- package/lib/subagent-widget.ts +109 -0
- package/lib/sync-room-server.mjs +137 -0
- package/lib/task-card-store.test.mjs +309 -0
- package/lib/task-card-store.ts +559 -0
- package/lib/task-scheduler.ts +498 -0
- package/lib/task-store.test.mjs +216 -0
- package/lib/task-store.ts +271 -0
- package/lib/terminal-input.test.mjs +46 -0
- package/lib/terminal-input.ts +63 -0
- package/lib/terminal-manager.test.mjs +71 -0
- package/lib/terminal-manager.ts +245 -0
- package/lib/tool-execution-progress.test.mjs +32 -0
- package/lib/tool-execution-progress.ts +29 -0
- package/lib/tool-names.ts +25 -0
- package/lib/tool-preset-preference.test.mjs +72 -0
- package/lib/tool-preset-preference.ts +41 -0
- package/lib/tool-presets.test.mjs +56 -0
- package/lib/tool-presets.ts +42 -0
- package/lib/turn-written-files.test.mjs +137 -0
- package/lib/turn-written-files.ts +60 -0
- package/lib/types.ts +349 -0
- package/lib/wallpaper-settings.ts +191 -0
- package/lib/web-auth.test.mjs +50 -0
- package/lib/web-auth.ts +45 -0
- package/lib/workspace-memory.test.mjs +129 -0
- package/lib/workspace-memory.ts +98 -0
- package/lib/worktree.test.mjs +50 -0
- package/lib/worktree.ts +250 -0
- package/package.json +3 -1
- package/server.mjs +34 -0
- package/.next/server/chunks/7513.js +0 -30
- package/.next/static/chunks/5315.03de3c6abd8b283e.js +0 -4
- package/.next/static/css/cf25164cb7c04d95.css +0 -1
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_buildManifest.js +0 -0
- /package/.next/static/{Ud6Y2ONkGuPVFbo1xZQvk → EoBw3VYlygpr1EqlVOa5b}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createJiti } from "jiti";
|
|
4
|
+
|
|
5
|
+
const jiti = createJiti(import.meta.url);
|
|
6
|
+
const {
|
|
7
|
+
getLastOpenSession,
|
|
8
|
+
setLastOpenSession,
|
|
9
|
+
clearLastOpen,
|
|
10
|
+
workspaceKeyOf,
|
|
11
|
+
} = await jiti.import("./workspace-memory.ts");
|
|
12
|
+
|
|
13
|
+
function createStorage(initial = {}) {
|
|
14
|
+
const values = new Map(Object.entries(initial));
|
|
15
|
+
return {
|
|
16
|
+
values,
|
|
17
|
+
getItem(key) {
|
|
18
|
+
return values.get(key) ?? null;
|
|
19
|
+
},
|
|
20
|
+
setItem(key, value) {
|
|
21
|
+
values.set(key, value);
|
|
22
|
+
},
|
|
23
|
+
removeItem(key) {
|
|
24
|
+
values.delete(key);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("returns null for an unknown workspace", () => {
|
|
30
|
+
assert.equal(getLastOpenSession("root-a", createStorage()), null);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("set then get round-trips the remembered session", () => {
|
|
34
|
+
const storage = createStorage();
|
|
35
|
+
setLastOpenSession("root-a", "session-1", storage);
|
|
36
|
+
assert.equal(getLastOpenSession("root-a", storage), "session-1");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("workspaces are remembered independently", () => {
|
|
40
|
+
const storage = createStorage();
|
|
41
|
+
setLastOpenSession("root-a", "session-a", storage);
|
|
42
|
+
setLastOpenSession("root-b", "session-b", storage);
|
|
43
|
+
assert.equal(getLastOpenSession("root-a", storage), "session-a");
|
|
44
|
+
assert.equal(getLastOpenSession("root-b", storage), "session-b");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("clearLastOpen removes only the named workspace", () => {
|
|
48
|
+
const storage = createStorage();
|
|
49
|
+
setLastOpenSession("root-a", "session-a", storage);
|
|
50
|
+
setLastOpenSession("root-b", "session-b", storage);
|
|
51
|
+
clearLastOpen("root-a", storage);
|
|
52
|
+
assert.equal(getLastOpenSession("root-a", storage), null);
|
|
53
|
+
assert.equal(getLastOpenSession("root-b", storage), "session-b");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("clearing the last entry removes the storage key entirely", () => {
|
|
57
|
+
const storage = createStorage();
|
|
58
|
+
setLastOpenSession("root-a", "session-a", storage);
|
|
59
|
+
clearLastOpen("root-a", storage);
|
|
60
|
+
assert.equal(getLastOpenSession("root-a", storage), null);
|
|
61
|
+
assert.equal(storage.values.has("pi-web:last-open-by-workspace"), false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("ignores a corrupt stored map", () => {
|
|
65
|
+
const storage = createStorage({ "pi-web:last-open-by-workspace": "{not-json" });
|
|
66
|
+
assert.equal(getLastOpenSession("root-a", storage), null);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("ignores a stored map of the wrong shape", () => {
|
|
70
|
+
const storage = createStorage({ "pi-web:last-open-by-workspace": `"just a string"` });
|
|
71
|
+
assert.equal(getLastOpenSession("root-a", storage), null);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("recovers from an array-shaped stored map", () => {
|
|
75
|
+
const storage = createStorage({ "pi-web:last-open-by-workspace": "[]" });
|
|
76
|
+
setLastOpenSession("root-a", "session-a", storage);
|
|
77
|
+
assert.equal(getLastOpenSession("root-a", storage), "session-a");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("ignores an empty or non-string session id", () => {
|
|
81
|
+
const storage = createStorage({
|
|
82
|
+
"pi-web:last-open-by-workspace": `{"root-a": "", "root-b": 42, "root-c": "ok"}`,
|
|
83
|
+
});
|
|
84
|
+
assert.equal(getLastOpenSession("root-a", storage), null);
|
|
85
|
+
assert.equal(getLastOpenSession("root-b", storage), null);
|
|
86
|
+
assert.equal(getLastOpenSession("root-c", storage), "ok");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("falls back to null / no-ops when browser storage is unavailable", () => {
|
|
90
|
+
const unavailable = {
|
|
91
|
+
getItem() { throw new Error("blocked"); },
|
|
92
|
+
setItem() { throw new Error("blocked"); },
|
|
93
|
+
removeItem() { throw new Error("blocked"); },
|
|
94
|
+
};
|
|
95
|
+
assert.equal(getLastOpenSession("root-a", unavailable), null);
|
|
96
|
+
assert.doesNotThrow(() => setLastOpenSession("root-a", "session-a", unavailable));
|
|
97
|
+
assert.doesNotThrow(() => clearLastOpen("root-a", unavailable));
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("falls back when browser storage access throws", () => {
|
|
101
|
+
const previousWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
102
|
+
const blockedWindow = {};
|
|
103
|
+
Object.defineProperty(blockedWindow, "localStorage", {
|
|
104
|
+
get() { throw new DOMException("blocked", "SecurityError"); },
|
|
105
|
+
});
|
|
106
|
+
Object.defineProperty(globalThis, "window", {
|
|
107
|
+
configurable: true,
|
|
108
|
+
value: blockedWindow,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
assert.equal(getLastOpenSession("root-a"), null);
|
|
113
|
+
assert.doesNotThrow(() => setLastOpenSession("root-a", "session-a"));
|
|
114
|
+
assert.doesNotThrow(() => clearLastOpen("root-a"));
|
|
115
|
+
} finally {
|
|
116
|
+
if (previousWindow) Object.defineProperty(globalThis, "window", previousWindow);
|
|
117
|
+
else delete globalThis.window;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("workspaceKeyOf prefers projectKey, then projectRoot, then cwd", () => {
|
|
122
|
+
assert.equal(
|
|
123
|
+
workspaceKeyOf({ cwd: "/repos/a/worktrees/b", projectRoot: "/repos/a", projectKey: "project:a" }),
|
|
124
|
+
"project:a",
|
|
125
|
+
);
|
|
126
|
+
assert.equal(workspaceKeyOf({ cwd: "/repos/a/worktrees/b", projectRoot: "/repos/a" }), "/repos/a");
|
|
127
|
+
assert.equal(workspaceKeyOf({ cwd: "/plain/dir", projectRoot: null }), "/plain/dir");
|
|
128
|
+
assert.equal(workspaceKeyOf({ cwd: "/plain/dir" }), "/plain/dir");
|
|
129
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-workspace "last open session" memory.
|
|
3
|
+
*
|
|
4
|
+
* Switching to a workspace (project root or cwd) restores the session the user
|
|
5
|
+
* had open there last, instead of landing on a blank new-session page. Without
|
|
6
|
+
* this, every workspace switch required re-picking the session by hand.
|
|
7
|
+
*
|
|
8
|
+
* The workspace key is the server-provided project identity when known, so
|
|
9
|
+
* Windows path variants and all worktrees of one repo share one memory slot.
|
|
10
|
+
* Transient and legacy session objects fall back to projectRoot/cwd.
|
|
11
|
+
*
|
|
12
|
+
* Stored in localStorage; best-effort (silently ignored when unavailable).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const STORAGE_KEY = "pi-web:last-open-by-workspace";
|
|
16
|
+
|
|
17
|
+
interface StorageLike {
|
|
18
|
+
getItem(key: string): string | null;
|
|
19
|
+
setItem(key: string, value: string): void;
|
|
20
|
+
removeItem(key: string): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getBrowserStorage(): StorageLike | null {
|
|
24
|
+
if (typeof window === "undefined") return null;
|
|
25
|
+
try {
|
|
26
|
+
return window.localStorage;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readMap(storage: StorageLike): Record<string, string | undefined> {
|
|
33
|
+
const raw = storage.getItem(STORAGE_KEY);
|
|
34
|
+
if (!raw) return {};
|
|
35
|
+
try {
|
|
36
|
+
const parsed: unknown = JSON.parse(raw);
|
|
37
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
38
|
+
? parsed as Record<string, string | undefined>
|
|
39
|
+
: {};
|
|
40
|
+
} catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The remembered session id for a workspace, or null when none/stale. */
|
|
46
|
+
export function getLastOpenSession(
|
|
47
|
+
workspaceKey: string,
|
|
48
|
+
storage: StorageLike | null = getBrowserStorage(),
|
|
49
|
+
): string | null {
|
|
50
|
+
if (!storage) return null;
|
|
51
|
+
try {
|
|
52
|
+
const id = readMap(storage)[workspaceKey];
|
|
53
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function setLastOpenSession(
|
|
60
|
+
workspaceKey: string,
|
|
61
|
+
sessionId: string,
|
|
62
|
+
storage: StorageLike | null = getBrowserStorage(),
|
|
63
|
+
): void {
|
|
64
|
+
if (!storage) return;
|
|
65
|
+
try {
|
|
66
|
+
const map = readMap(storage);
|
|
67
|
+
map[workspaceKey] = sessionId;
|
|
68
|
+
storage.setItem(STORAGE_KEY, JSON.stringify(map));
|
|
69
|
+
} catch {
|
|
70
|
+
// storage unavailable — memory is best-effort
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function clearLastOpen(
|
|
75
|
+
workspaceKey: string,
|
|
76
|
+
storage: StorageLike | null = getBrowserStorage(),
|
|
77
|
+
): void {
|
|
78
|
+
if (!storage) return;
|
|
79
|
+
try {
|
|
80
|
+
const map = readMap(storage);
|
|
81
|
+
if (!(workspaceKey in map)) return;
|
|
82
|
+
delete map[workspaceKey];
|
|
83
|
+
// Keep the store clean: drop the key entirely when nothing is remembered.
|
|
84
|
+
if (Object.keys(map).length === 0) storage.removeItem(STORAGE_KEY);
|
|
85
|
+
else storage.setItem(STORAGE_KEY, JSON.stringify(map));
|
|
86
|
+
} catch {
|
|
87
|
+
// ignore
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Workspace identity for a session: resolved project root when known, else cwd. */
|
|
92
|
+
export function workspaceKeyOf(session: {
|
|
93
|
+
cwd: string;
|
|
94
|
+
projectRoot?: string | null;
|
|
95
|
+
projectKey?: string | null;
|
|
96
|
+
}): string {
|
|
97
|
+
return session.projectKey ?? session.projectRoot ?? session.cwd;
|
|
98
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import test from "node:test";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
async function loadSubject() {
|
|
12
|
+
const { createJiti } = await import("jiti");
|
|
13
|
+
return createJiti(import.meta.url).import("./worktree.ts");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function git(cwd, args) {
|
|
17
|
+
await execFileAsync("git", ["-C", cwd, ...args]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("main and linked worktrees share one canonical project root", async (t) => {
|
|
21
|
+
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "pi-web-worktree-"));
|
|
22
|
+
t.after(() => rm(tempRoot, { recursive: true, force: true }));
|
|
23
|
+
|
|
24
|
+
const repo = path.join(tempRoot, "repo");
|
|
25
|
+
const linked = path.join(tempRoot, "linked");
|
|
26
|
+
await execFileAsync("git", ["init", repo]);
|
|
27
|
+
await git(repo, ["config", "user.name", "Pi Web Test"]);
|
|
28
|
+
await git(repo, ["config", "user.email", "pi-web-test@example.invalid"]);
|
|
29
|
+
await git(repo, ["config", "commit.gpgsign", "false"]);
|
|
30
|
+
await writeFile(path.join(repo, "README.md"), "# test\n");
|
|
31
|
+
await git(repo, ["add", "README.md"]);
|
|
32
|
+
await git(repo, ["commit", "-m", "initial"]);
|
|
33
|
+
await git(repo, ["worktree", "add", "-b", "feature/test", linked]);
|
|
34
|
+
|
|
35
|
+
const { findCurrentWorktreePath, listWorktrees, resolveProject } = await loadSubject();
|
|
36
|
+
const mainProject = await resolveProject(`${repo}${path.sep}`);
|
|
37
|
+
const linkedProject = await resolveProject(linked);
|
|
38
|
+
|
|
39
|
+
assert.equal(mainProject.isTopLevel, true);
|
|
40
|
+
assert.equal(mainProject.isWorktree, false);
|
|
41
|
+
assert.equal(linkedProject.isTopLevel, true);
|
|
42
|
+
assert.equal(linkedProject.isWorktree, true);
|
|
43
|
+
assert.equal(linkedProject.branch, "feature/test");
|
|
44
|
+
assert.equal(mainProject.projectRoot, linkedProject.projectRoot);
|
|
45
|
+
|
|
46
|
+
const worktrees = await listWorktrees(linked);
|
|
47
|
+
const listedLinked = worktrees.find((worktree) => worktree.branch === "feature/test");
|
|
48
|
+
assert.ok(listedLinked);
|
|
49
|
+
assert.equal(findCurrentWorktreePath(worktrees, `${linked}${path.sep}`), listedLinked.path);
|
|
50
|
+
});
|
package/lib/worktree.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { existsSync, mkdirSync, realpathSync } from "fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "path";
|
|
4
|
+
import { promisify } from "util";
|
|
5
|
+
import { allowFileRoot } from "./allowed-roots";
|
|
6
|
+
import { samePath, toNativePath } from "./paths";
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Project resolution: cwd → { projectRoot, branch }
|
|
12
|
+
//
|
|
13
|
+
// A worktree's `git rev-parse --git-common-dir` points at the *main* repo's
|
|
14
|
+
// .git directory, so its parent is the project root shared by all worktrees.
|
|
15
|
+
// Non-git directories resolve to themselves. Results are cached on globalThis
|
|
16
|
+
// (hot-reload safe) with a short TTL; add/remove worktree invalidates eagerly.
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface ProjectInfo {
|
|
20
|
+
projectRoot: string;
|
|
21
|
+
/** Current branch of the cwd, null for non-git dirs or detached HEAD */
|
|
22
|
+
branch: string | null;
|
|
23
|
+
/** True when cwd is a linked worktree (not the main checkout) */
|
|
24
|
+
isWorktree: boolean;
|
|
25
|
+
/** True when cwd is the top-level directory of a checkout (main or linked).
|
|
26
|
+
* False for repo subdirectories and non-git dirs — the worktree switcher
|
|
27
|
+
* is only meaningful at the top level. */
|
|
28
|
+
isTopLevel: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorktreeInfo {
|
|
32
|
+
path: string;
|
|
33
|
+
branch: string | null;
|
|
34
|
+
isMain: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
declare global {
|
|
38
|
+
var __piProjectCache: Map<string, { info: ProjectInfo; expiresAt: number }> | undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const PROJECT_CACHE_TTL_MS = 60_000;
|
|
42
|
+
|
|
43
|
+
function getProjectCache(): Map<string, { info: ProjectInfo; expiresAt: number }> {
|
|
44
|
+
if (!globalThis.__piProjectCache) globalThis.__piProjectCache = new Map();
|
|
45
|
+
return globalThis.__piProjectCache;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function invalidateProjectCache(): void {
|
|
49
|
+
globalThis.__piProjectCache?.clear();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function git(cwd: string, args: string[]): Promise<string> {
|
|
53
|
+
const { stdout } = await execFileAsync("git", ["-C", cwd, ...args], {
|
|
54
|
+
timeout: 10_000,
|
|
55
|
+
maxBuffer: 1024 * 1024,
|
|
56
|
+
// Pin the message locale so error-text matching (e.g. the dirty-worktree
|
|
57
|
+
// detection in the DELETE route) works regardless of system language.
|
|
58
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
59
|
+
});
|
|
60
|
+
return stdout.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function realPathOrSelf(filePath: string): string {
|
|
64
|
+
try {
|
|
65
|
+
return realpathSync(filePath);
|
|
66
|
+
} catch {
|
|
67
|
+
return filePath;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* addWorktree() places worktrees in `<repoRoot>-worktrees/<dir>`. When such a
|
|
73
|
+
* directory no longer exists (worktree removed), group its sessions back
|
|
74
|
+
* under the main repo instead of letting them dangle as a phantom project.
|
|
75
|
+
* The dir name is the sanitized branch name — close enough for display.
|
|
76
|
+
*/
|
|
77
|
+
function inferRemovedWorktree(cwd: string): ProjectInfo | null {
|
|
78
|
+
const parent = dirname(cwd);
|
|
79
|
+
if (!parent.endsWith("-worktrees")) return null;
|
|
80
|
+
const repoRoot = parent.slice(0, -"-worktrees".length);
|
|
81
|
+
if (!repoRoot || !existsSync(join(repoRoot, ".git"))) return null;
|
|
82
|
+
return { projectRoot: realPathOrSelf(repoRoot), branch: basename(cwd), isWorktree: true, isTopLevel: true };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function resolveProject(cwd: string): Promise<ProjectInfo> {
|
|
86
|
+
const cache = getProjectCache();
|
|
87
|
+
const cached = cache.get(cwd);
|
|
88
|
+
if (cached && cached.expiresAt > Date.now()) return cached.info;
|
|
89
|
+
|
|
90
|
+
let info: ProjectInfo;
|
|
91
|
+
try {
|
|
92
|
+
if (!existsSync(cwd)) {
|
|
93
|
+
info = inferRemovedWorktree(cwd) ?? { projectRoot: cwd, branch: null, isWorktree: false, isTopLevel: false };
|
|
94
|
+
cache.set(cwd, { info, expiresAt: Date.now() + PROJECT_CACHE_TTL_MS });
|
|
95
|
+
return info;
|
|
96
|
+
}
|
|
97
|
+
const out = await git(cwd, [
|
|
98
|
+
"rev-parse", "--path-format=absolute",
|
|
99
|
+
"--git-common-dir", "--git-dir", "--show-toplevel",
|
|
100
|
+
"--abbrev-ref", "HEAD",
|
|
101
|
+
]);
|
|
102
|
+
const [commonDirRaw, gitDirRaw, toplevelRaw, ref] = out.split("\n").map((l) => l.trim());
|
|
103
|
+
// Only the first three lines are paths — `ref` is a branch name and must
|
|
104
|
+
// keep its forward slashes (`feature/foo`).
|
|
105
|
+
const [commonDir, gitDir, toplevel] = [commonDirRaw, gitDirRaw, toplevelRaw].map(toNativePath);
|
|
106
|
+
// git prints resolved (symlink-free) paths; normalize cwd the same way
|
|
107
|
+
const realCwd = realPathOrSelf(cwd);
|
|
108
|
+
// For a linked worktree, --git-dir differs from --git-common-dir.
|
|
109
|
+
// Only collapse *worktree toplevels* into the main repo. A session whose
|
|
110
|
+
// cwd is a subdirectory of a repo keeps its own project identity —
|
|
111
|
+
// grouping subdirs under the repo root would change where new sessions
|
|
112
|
+
// are created for existing users.
|
|
113
|
+
const isTopLevel = samePath(toplevel, realCwd);
|
|
114
|
+
const isWorktreeTopLevel = !samePath(gitDir, commonDir) && isTopLevel;
|
|
115
|
+
const topLevelProjectRoot = isWorktreeTopLevel ? dirname(commonDir) : toplevel;
|
|
116
|
+
info = {
|
|
117
|
+
projectRoot: isTopLevel ? realPathOrSelf(topLevelProjectRoot) : cwd,
|
|
118
|
+
branch: ref && ref !== "HEAD" ? ref : null,
|
|
119
|
+
isWorktree: isWorktreeTopLevel,
|
|
120
|
+
isTopLevel,
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
info = { projectRoot: cwd, branch: null, isWorktree: false, isTopLevel: false };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
cache.set(cwd, { info, expiresAt: Date.now() + PROJECT_CACHE_TTL_MS });
|
|
127
|
+
return info;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Worktree operations
|
|
132
|
+
//
|
|
133
|
+
// These take any directory inside the repo (a worktree, the main checkout, or
|
|
134
|
+
// a subdirectory) and resolve the main repo root themselves via the git
|
|
135
|
+
// common dir, so callers can pass session cwds directly.
|
|
136
|
+
// ============================================================================
|
|
137
|
+
|
|
138
|
+
/** Main repo root (parent of the shared .git dir), or throws for non-git dirs */
|
|
139
|
+
async function getRepoRoot(cwd: string): Promise<string> {
|
|
140
|
+
const commonDir = await git(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
141
|
+
return realPathOrSelf(dirname(toNativePath(commonDir)));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function listWorktrees(cwd: string): Promise<WorktreeInfo[]> {
|
|
145
|
+
const out = await git(cwd, ["worktree", "list", "--porcelain"]);
|
|
146
|
+
const worktrees: WorktreeInfo[] = [];
|
|
147
|
+
let current: (Partial<WorktreeInfo> & { prunable?: boolean }) | null = null;
|
|
148
|
+
|
|
149
|
+
const flush = () => {
|
|
150
|
+
if (current?.path) {
|
|
151
|
+
// Prunable worktrees point at missing/broken gitdirs and cannot be
|
|
152
|
+
// browsed or selected usefully. Also skip vanished paths even if git has
|
|
153
|
+
// not marked them prunable yet.
|
|
154
|
+
if (!current.prunable && existsSync(current.path)) {
|
|
155
|
+
worktrees.push({
|
|
156
|
+
path: current.path,
|
|
157
|
+
branch: current.branch ?? null,
|
|
158
|
+
isMain: worktrees.length === 0,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
current = null;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const line of out.split("\n")) {
|
|
166
|
+
if (line.startsWith("worktree ")) {
|
|
167
|
+
flush();
|
|
168
|
+
current = { path: toNativePath(line.slice("worktree ".length).trim()) };
|
|
169
|
+
} else if (line.startsWith("branch ") && current) {
|
|
170
|
+
current.branch = line.slice("branch ".length).trim().replace(/^refs\/heads\//, "");
|
|
171
|
+
} else if (line.startsWith("prunable") && current) {
|
|
172
|
+
current.prunable = true;
|
|
173
|
+
} else if (line.trim() === "") {
|
|
174
|
+
flush();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
flush();
|
|
178
|
+
return worktrees;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findWorktreeByPath(worktrees: readonly WorktreeInfo[], candidate: string): WorktreeInfo | undefined {
|
|
182
|
+
return worktrees.find((worktree) => samePath(worktree.path, candidate));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function findCurrentWorktreePath(worktrees: readonly WorktreeInfo[], cwd: string): string | null {
|
|
186
|
+
return findWorktreeByPath(worktrees, realPathOrSelf(cwd))?.path ?? null;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function sanitizeBranchForDir(branch: string): string {
|
|
190
|
+
return branch.replace(/[\/\\:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export async function addWorktree(cwd: string, branch: string): Promise<{ path: string; branch: string }> {
|
|
194
|
+
const trimmed = branch.trim();
|
|
195
|
+
if (!trimmed) throw new Error("Branch name is required");
|
|
196
|
+
|
|
197
|
+
const dirName = sanitizeBranchForDir(trimmed);
|
|
198
|
+
if (!dirName) throw new Error(`Invalid branch name: ${branch}`);
|
|
199
|
+
|
|
200
|
+
const repoRoot = await getRepoRoot(cwd);
|
|
201
|
+
const baseDir = `${resolve(repoRoot)}-worktrees`;
|
|
202
|
+
const worktreePath = join(baseDir, dirName);
|
|
203
|
+
if (existsSync(worktreePath)) {
|
|
204
|
+
throw new Error(`Directory already exists: ${worktreePath}`);
|
|
205
|
+
}
|
|
206
|
+
mkdirSync(baseDir, { recursive: true });
|
|
207
|
+
|
|
208
|
+
// Reuse the branch if it already exists, otherwise create it at HEAD.
|
|
209
|
+
let branchExists = false;
|
|
210
|
+
try {
|
|
211
|
+
await git(repoRoot, ["rev-parse", "--verify", "--quiet", `refs/heads/${trimmed}`]);
|
|
212
|
+
branchExists = true;
|
|
213
|
+
} catch {
|
|
214
|
+
branchExists = false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
if (branchExists) {
|
|
219
|
+
await git(repoRoot, ["worktree", "add", "--", worktreePath, trimmed]);
|
|
220
|
+
} else {
|
|
221
|
+
await git(repoRoot, ["worktree", "add", "-b", trimmed, "--", worktreePath]);
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw new Error(extractGitError(error));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
allowFileRoot(worktreePath);
|
|
228
|
+
invalidateProjectCache();
|
|
229
|
+
return { path: worktreePath, branch: trimmed };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export async function removeWorktree(cwd: string, worktreePath: string, force = false): Promise<void> {
|
|
233
|
+
const worktrees = await listWorktrees(cwd);
|
|
234
|
+
const target = findWorktreeByPath(worktrees, worktreePath);
|
|
235
|
+
if (!target) throw new Error(`Not a worktree of this repository: ${worktreePath}`);
|
|
236
|
+
if (target.isMain) throw new Error("Cannot remove the main worktree");
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
await git(cwd, ["worktree", "remove", ...(force ? ["--force"] : []), target.path]);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
throw new Error(extractGitError(error));
|
|
242
|
+
}
|
|
243
|
+
invalidateProjectCache();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function extractGitError(error: unknown): string {
|
|
247
|
+
const stderr = (error as { stderr?: string }).stderr;
|
|
248
|
+
if (typeof stderr === "string" && stderr.trim()) return stderr.trim();
|
|
249
|
+
return error instanceof Error ? error.message : String(error);
|
|
250
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baique/pi-web-sky",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "Web UI for the pi coding agent — Sky skin edition (fork of @agegr/pi-web / earendil-labs pi-web, MIT)",
|
|
5
5
|
"homepage": "https://github.com/baique/pi-web-sky#readme",
|
|
6
6
|
"repository": {
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
"!.next/**/*.js.map",
|
|
26
26
|
"public",
|
|
27
27
|
"next.config.ts",
|
|
28
|
+
"server.mjs",
|
|
29
|
+
"lib",
|
|
28
30
|
"package.json"
|
|
29
31
|
],
|
|
30
32
|
"scripts": {
|
package/server.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 自定义 Next.js server:HTTP 交 Next,WebSocket 升级分流
|
|
3
|
+
// - HTTP/HTTPS:next.getRequestHandler()(页面/API/HMR 全走 Next)
|
|
4
|
+
// - WS upgrade:/connect/:boardId → 内嵌 tldraw sync 房间(TLSocketRoom)
|
|
5
|
+
// 其余(/_next/hmr 等)→ next.getUpgradeHandler()
|
|
6
|
+
// - 启动:npm run dev / npm start(均走本文件,单进程同端口,无需另起 sync-server)
|
|
7
|
+
// ============================================================================
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import next from "next";
|
|
10
|
+
import { handleSyncUpgrade } from "./lib/sync-room-server.mjs";
|
|
11
|
+
|
|
12
|
+
const dev = process.env.NODE_ENV !== "production";
|
|
13
|
+
const hostname = process.env.HOSTNAME ?? "127.0.0.1";
|
|
14
|
+
const port = Number(process.env.PORT ?? 30143);
|
|
15
|
+
|
|
16
|
+
const app = next({ dev, hostname, port });
|
|
17
|
+
const handle = app.getRequestHandler();
|
|
18
|
+
|
|
19
|
+
const server = createServer((req, res) => handle(req, res));
|
|
20
|
+
|
|
21
|
+
// WebSocket 升级分流:sync 房间优先,其余(HMR)交给 Next
|
|
22
|
+
server.on("upgrade", (req, socket, head) => {
|
|
23
|
+
if (handleSyncUpgrade(req, socket, head)) return;
|
|
24
|
+
app.getUpgradeHandler()(req, socket, head).catch((err) => {
|
|
25
|
+
console.error("[server] upgrade error:", err);
|
|
26
|
+
socket.destroy();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await app.prepare();
|
|
31
|
+
server.listen(port, hostname, () => {
|
|
32
|
+
console.log(`> Pi Web ready on http://${hostname}:${port} (dev=${dev})`);
|
|
33
|
+
console.log(`> tldraw sync rooms embedded on ws://${hostname}:${port}/connect/:boardId`);
|
|
34
|
+
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
exports.id=7513,exports.ids=[7513],exports.modules={19038:(a,b,c)=>{"use strict";c.d(b,{CU:()=>l,S7:()=>u});var d=c(79646),e=c(79748),f=c(21820),g=c(33873),h=c(28354);let i=process.env.SKILLS_API_URL||"https://skills.sh",j=(0,h.promisify)(d.execFile);class k extends Error{constructor(a){super(`HTTP ${a}`),this.status=a}}function l(a){let b=n(a.skillPath??""),c=b?`${a.source}/${b}`:a.source,d=a.ref?`#${encodeURIComponent(a.ref)}`:"",e=["skills","add",`${c}${d}`,"--skill",m(a.package),"-y","--agent","pi"];return"global"===a.scope&&e.push("-g"),e}function m(a){let b=a.lastIndexOf("@");return b>=0?a.slice(b+1):a}function n(a){let b=a.replace(/\\/g,"/");return b.toLowerCase().endsWith("/skill.md")?b=b.slice(0,-9):b.toLowerCase().endsWith("skill.md")&&(b=b.slice(0,-8)),b.replace(/\/$/,"")}function o(a,b,c,d){return{package:a.package,scope:a.scope,state:b,currentVersion:a.versionHash,latestVersion:c,message:d}}async function p(a,b,c){let d=await b(a,{cache:"no-store",headers:c,signal:AbortSignal.timeout(15e3)});if(!d.ok)throw new k(d.status);return d.json()}async function q(a){let b=`https://github.com/${a.source}.git`,c=a.ref||"HEAD",d=n(a.skillPath),h=await (0,e.mkdtemp)((0,g.join)((0,f.tmpdir)(),"pi-web-skill-check-"));try{await j("git",["init","--bare",h],{timeout:3e4}),await j("git",[`--git-dir=${h}`,"fetch","--depth=1","--filter=blob:none","--no-tags",b,c],{timeout:3e4});let a=d?`FETCH_HEAD:${d}`:"FETCH_HEAD^{tree}",{stdout:e}=await j("git",[`--git-dir=${h}`,"rev-parse",a],{timeout:3e4}),f=e.trim();if(!/^[0-9a-f]{40}$/i.test(f))throw Error("Invalid Git tree hash");return f}finally{await (0,e.rm)(h,{recursive:!0,force:!0})}}async function r(a,b){let c,d=a.ref||"HEAD",e=`https://api.github.com/repos/${a.source}/git/trees/${encodeURIComponent(d)}?recursive=1`,f={Accept:"application/vnd.github.v3+json","User-Agent":"pi-web"};b.githubToken&&(f.Authorization=`Bearer ${b.githubToken}`);let g=n(a.skillPath);try{let a=await p(e,b.fetcher,f);if(c="string"!=typeof a.sha||g?void 0:a.sha,g&&Array.isArray(a.tree)){let b=a.tree.find(a=>"tree"===a.type&&a.path===g);b&&"string"==typeof b.sha&&(c=b.sha)}}catch(d){if(!(d instanceof k)||![401,403,429].includes(d.status))throw d;c=await b.resolveGitTreeHash(a)}return c?o(a,c===a.versionHash?"up-to-date":"update-available",c):o(a,"error",void 0,"Remote skill path was not found.")}async function s(a,b){let[c,d]=a.source.split("/"),e=m(a.package).toLowerCase().replace(/[\s_]+/g,"-").replace(/[^a-z0-9-]/g,"").replace(/-+/g,"-").replace(/^-|-$/g,""),f=`${b.skillsApiBase}/api/download/${encodeURIComponent(c)}/${encodeURIComponent(d)}/${encodeURIComponent(e)}`,g=await p(f,b.fetcher),h="string"==typeof g.hash?g.hash:void 0;return h?o(a,h===a.versionHash?"up-to-date":"update-available",h):o(a,"error",void 0,"skills.sh did not return a version hash.")}async function t(a,b={}){if(!a.canCheckForUpdates||!a.versionHash||!a.skillPath)return o(a,"unsupported",void 0,"This lock entry cannot be checked automatically.");let c={...b,fetcher:b.fetcher??fetch,skillsApiBase:b.skillsApiBase??i,resolveGitTreeHash:b.resolveGitTreeHash??q};try{return"global"===a.scope?await r(a,c):await s(a,c)}catch(b){return o(a,"error",void 0,b instanceof Error?b.message:String(b))}}async function u(a,b={}){let c=b.fetcher??fetch,d=new Map,e=async(a,b)=>{let e=d.get(a);return e||(e=c(a,b),d.set(a,e)),(await e).clone()};return Promise.all(a.map(a=>t(a,{...b,fetcher:e})))}},21278:(a,b,c)=>{"use strict";c.a(a,async(a,d)=>{try{c.d(b,{y:()=>i});var e=c(75430),f=c(75805),g=c(72498),h=a([e,g]);async function i(a){let b=(0,e.getAgentDir)(),c=new e.DefaultResourceLoader({cwd:a,agentDir:b});await c.reload((0,g.Fk)(a,b));let{skills:d,diagnostics:h}=c.getSkills();return{skills:(0,f._)(d,{cwd:a,agentDir:b}),diagnostics:h,projectResourcesLoaded:(0,g.Y1)(a,b).trusted}}[e,g]=h.then?(await h)():h,d()}catch(a){d(a)}})},66737:(a,b,c)=>{"use strict";c.d(b,{Lf:()=>g});var d=c(80099),e=c(21820),f=c(33873);function g(){var a;if(globalThis.__piWebDb)return globalThis.__piWebDb;let b=new d.DatabaseSync((0,f.join)(function(){let a=process.env.PI_CODING_AGENT_DIR;if(a){let b=a.startsWith("~")?(0,f.join)((0,e.homedir)(),a.slice(1)):a;return(0,f.normalize)(b)}return(0,f.join)((0,e.homedir)(),".pi","agent")}(),"pi-web.db"));return b.exec("PRAGMA journal_mode = WAL;"),b.exec("PRAGMA foreign_keys = ON;"),(a=b).exec(`
|
|
2
|
-
CREATE TABLE IF NOT EXISTS tasks (
|
|
3
|
-
id TEXT PRIMARY KEY,
|
|
4
|
-
project_key TEXT NOT NULL,
|
|
5
|
-
name TEXT NOT NULL,
|
|
6
|
-
created INTEGER NOT NULL,
|
|
7
|
-
updated INTEGER NOT NULL
|
|
8
|
-
);
|
|
9
|
-
CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_key);
|
|
10
|
-
|
|
11
|
-
CREATE TABLE IF NOT EXISTS session_meta (
|
|
12
|
-
session_id TEXT PRIMARY KEY,
|
|
13
|
-
task_id TEXT,
|
|
14
|
-
updated INTEGER NOT NULL
|
|
15
|
-
);
|
|
16
|
-
CREATE INDEX IF NOT EXISTS idx_meta_task ON session_meta(task_id);
|
|
17
|
-
|
|
18
|
-
CREATE TABLE IF NOT EXISTS search_state (
|
|
19
|
-
session_id TEXT PRIMARY KEY,
|
|
20
|
-
mtime TEXT NOT NULL,
|
|
21
|
-
title TEXT NOT NULL
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS session_search USING fts5(
|
|
25
|
-
session_id UNINDEXED,
|
|
26
|
-
title,
|
|
27
|
-
body,
|
|
28
|
-
tokenize = 'trigram'
|
|
29
|
-
);
|
|
30
|
-
`),function(a){let b=a.prepare("PRAGMA user_version").get().user_version;for(let c of h)if(!(c.version<=b)){a.exec("BEGIN");try{for(let b of c.statements)try{a.exec(b)}catch(b){let a=b instanceof Error?b.message:String(b);if(!/duplicate column name/i.test(a))throw b}a.exec(`PRAGMA user_version = ${c.version}`),a.exec("COMMIT"),b=c.version}catch(b){throw a.exec("ROLLBACK"),b}}}(a),globalThis.__piWebDb=b,b}let h=[{version:1,name:"pinned columns",statements:["ALTER TABLE tasks ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;","ALTER TABLE session_meta ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;"]},{version:2,name:"tasks.sort_order",statements:["ALTER TABLE tasks ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;"]},{version:3,name:"session boards",statements:["CREATE TABLE IF NOT EXISTS boards (\n id TEXT PRIMARY KEY,\n project_key TEXT NOT NULL,\n name TEXT NOT NULL,\n is_system INTEGER NOT NULL DEFAULT 0,\n created INTEGER NOT NULL,\n updated INTEGER NOT NULL\n);","CREATE INDEX IF NOT EXISTS idx_boards_project ON boards(project_key);","CREATE TABLE IF NOT EXISTS board_nodes (\n id TEXT PRIMARY KEY,\n board_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n ref_id TEXT,\n x REAL NOT NULL,\n y REAL NOT NULL,\n w REAL NOT NULL DEFAULT 0,\n h REAL NOT NULL DEFAULT 0,\n expanded INTEGER NOT NULL DEFAULT 0,\n props TEXT NOT NULL DEFAULT '{}',\n created INTEGER NOT NULL,\n updated INTEGER NOT NULL\n);","CREATE INDEX IF NOT EXISTS idx_board_nodes_board ON board_nodes(board_id);","CREATE TABLE IF NOT EXISTS board_edges (\n id TEXT PRIMARY KEY,\n board_id TEXT NOT NULL,\n from_id TEXT NOT NULL,\n to_id TEXT NOT NULL,\n label TEXT,\n color TEXT,\n dashed INTEGER NOT NULL DEFAULT 0,\n created INTEGER NOT NULL,\n updated INTEGER NOT NULL\n);","CREATE INDEX IF NOT EXISTS idx_board_edges_board ON board_edges(board_id);","CREATE TABLE IF NOT EXISTS board_view (\n board_id TEXT PRIMARY KEY,\n camera_x REAL NOT NULL DEFAULT 0,\n camera_y REAL NOT NULL DEFAULT 0,\n camera_z REAL NOT NULL DEFAULT 1,\n updated INTEGER NOT NULL\n);"]},{version:4,name:"boards.sort_order",statements:["ALTER TABLE boards ADD COLUMN sort_order INTEGER NOT NULL DEFAULT 0;"]},{version:5,name:"boards.task_id",statements:["ALTER TABLE boards ADD COLUMN task_id TEXT NULL;","CREATE INDEX IF NOT EXISTS idx_boards_task ON boards(task_id);"]},{version:6,name:"boards.task_id unique (防并发重复创建任务看板)",statements:["CREATE UNIQUE INDEX IF NOT EXISTS idx_boards_task_unique ON boards(task_id);"]},{version:7,name:"task cards",statements:["CREATE TABLE IF NOT EXISTS task_cards (\n id TEXT PRIMARY KEY,\n board_id TEXT NOT NULL,\n project_key TEXT NOT NULL,\n number INTEGER NOT NULL,\n name TEXT NOT NULL,\n description TEXT NOT NULL DEFAULT '',\n ready_status TEXT NOT NULL DEFAULT 'draft',\n exec_status TEXT NOT NULL DEFAULT 'not_started',\n priority INTEGER NOT NULL DEFAULT 0,\n due INTEGER,\n attachments TEXT NOT NULL DEFAULT '[]',\n cwd TEXT,\n use_worktree INTEGER NOT NULL DEFAULT 0,\n max_retries INTEGER NOT NULL DEFAULT 3,\n retry_count INTEGER NOT NULL DEFAULT 0,\n session_id TEXT,\n created INTEGER NOT NULL,\n updated INTEGER NOT NULL\n);","CREATE INDEX IF NOT EXISTS idx_task_cards_board ON task_cards(board_id);","CREATE INDEX IF NOT EXISTS idx_task_cards_project ON task_cards(project_key);","CREATE UNIQUE INDEX IF NOT EXISTS idx_task_cards_number ON task_cards(project_key, number);","CREATE INDEX IF NOT EXISTS idx_task_cards_status ON task_cards(ready_status, exec_status);","CREATE TABLE IF NOT EXISTS task_card_links (\n id TEXT PRIMARY KEY,\n card_id TEXT NOT NULL,\n target_card_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n created INTEGER NOT NULL,\n UNIQUE(card_id, target_card_id, kind)\n);","CREATE INDEX IF NOT EXISTS idx_task_links_card ON task_card_links(card_id);","CREATE TABLE IF NOT EXISTS task_card_questions (\n id TEXT PRIMARY KEY,\n card_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n question TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'pending',\n answer TEXT,\n created INTEGER NOT NULL,\n answered INTEGER\n);","CREATE INDEX IF NOT EXISTS idx_task_questions_status ON task_card_questions(status);"]}]},72498:(a,b,c)=>{"use strict";c.a(a,async(a,d)=>{try{c.d(b,{Fk:()=>i,Y1:()=>g,er:()=>h});var e=c(75430),f=a([e]);function g(a,b){if(!(a&&(0,e.hasTrustRequiringProjectResources)(a)))return{requiresTrust:!1,trusted:!0};let c=new e.ProjectTrustStore(b);return{requiresTrust:!0,trusted:!0===c.get(a)}}function h(a,b){let c=g(a,b);return c.requiresTrust?(new e.ProjectTrustStore(b).set(a,!0),{requiresTrust:!0,trusted:!0}):c}function i(a,b){if(!g(a,b).requiresTrust)return;let c=new e.ProjectTrustStore(b);return{resolveProjectTrust:async()=>!0===c.get(a)}}e=(f.then?(await f)():f)[0],d()}catch(a){d(a)}})},75805:(a,b,c)=>{"use strict";c.d(b,{_:()=>j});var d=c(29021),e=c(21820),f=c(33873);function g(a){try{let b=JSON.parse((0,d.readFileSync)(a,"utf8"));return b.skills&&"object"==typeof b.skills?b.skills:{}}catch{return{}}}function h(a,b){let c=(0,f.relative)((0,f.resolve)(b),(0,f.resolve)(a));return""!==c&&".."!==c&&!c.startsWith(`..${f.sep}`)&&!(0,f.isAbsolute)(c)}function i(a,b,c){var d;let e=function(a,b){if(a[b])return a[b];let c=b.toLowerCase(),d=Object.keys(a).find(a=>a.toLowerCase()===c);return d?a[d]:void 0}(a,b);if(!e||"string"!=typeof e.source||!e.source.trim())return;let f="string"==typeof e.sourceType?e.sourceType:void 0,g=(d=e.source.trim(),"github"!==f?d.replace(/\/$/,""):d.replace(/^git\+/,"").replace(/^https?:\/\/github\.com\//,"").replace(/^git@github\.com:/,"").replace(/\.git$/,"").replace(/\/$/,""));if(!g)return;let h="string"==typeof e.skillPath?e.skillPath:void 0,i="string"==typeof e.ref?e.ref:void 0,j="global"===c?e.skillFolderHash:e.computedHash,k="string"==typeof j&&j?j:void 0,l="github"===f&&/^[\w.-]+\/[\w.-]+$/.test(g);return{package:`${g}@${b}`,scope:c,source:g,sourceType:f,skillsShUrl:"local"===f?void 0:function(a,b){if(!a||a.includes("://")||a.startsWith("git@"))return;let c=a.split("/").filter(Boolean).map(encodeURIComponent).join("/");if(c)return`https://skills.sh/${c}/${encodeURIComponent(b)}`}(g,b),...h&&{skillPath:h},...i&&{ref:i},...k&&{versionHash:k},canCheckForUpdates:!!(l&&h&&k&&("global"===c||!i))}}function j(a,{cwd:b,agentDir:c,globalLockPath:k=function({homeDir:a=(0,e.homedir)(),xdgStateHome:b=process.env.XDG_STATE_HOME}={}){return b?(0,f.join)(b,"skills",".skill-lock.json"):(0,f.join)(a,".agents",".skill-lock.json")}(),projectLockPath:l=(0,f.join)(b,"skills-lock.json")}){let m=g(k),n=g(l),o=(0,f.join)(c,"skills"),p=(0,f.join)(b,".pi","skills");return a.map(a=>{if(!(0,d.existsSync)(a.filePath))return a;let b=h(a.filePath,o)?i(m,a.name,"global"):h(a.filePath,p)?i(n,a.name,"project"):void 0;return b?{...a,install:b}:a})}},78335:()=>{},80547:(a,b,c)=>{"use strict";c.a(a,async(a,d)=>{try{c.d(b,{A$:()=>o,B7:()=>p,Nw:()=>i.Nw,mc:()=>n,uK:()=>l.uK,wz:()=>i.wz});var e=c(29021),f=c(21820),g=c(33873),h=c.n(g),i=c(25252),j=c(82536),k=c(40402),l=c(58864),m=a([k]);async function n(){let a=Date.now(),b=globalThis.__piAllowedRootsCache;if(b&&b.expiresAt>a)return b.roots;let c=await (0,k.listAllSessions)(),d=new Set;for(let a of c)a.cwd&&d.add((0,i.Nw)(a.cwd)),a.projectRoot&&d.add((0,i.Nw)(a.projectRoot));try{for(let a of(0,e.readdirSync)((0,f.homedir)()))/^pi-cwd-\d{8}$/.test(a)&&d.add((0,i.Nw)(h().join((0,f.homedir)(),a)))}catch{}for(let a of(0,i.HM)())d.add(a);return globalThis.__piAllowedRootsCache={roots:d,expiresAt:a+5e3},d}function o(a,b){return(0,j.o)(a,b)}function p(a,b){return(0,j.x)(a,b)}k=(m.then?(await m)():m)[0],d()}catch(a){d(a)}})},82536:(a,b,c)=>{"use strict";c.d(b,{o:()=>h,x:()=>i});var d=c(29021),e=c(33873),f=c.n(e),g=c(58864);function h(a,b){for(let c of b){let b=(0,g.uK)(a)||(0,g.uK)(c),d=b?f().win32:f(),e=b?"\\":f().sep,h=d.resolve(a),i=d.resolve(c),j=b?h.toLowerCase():h,k=b?i.toLowerCase():i,l=k.endsWith(e)?k:k+e;if(j===k||j.startsWith(l))return!0}return!1}function i(a,b){let c;try{c=(0,d.realpathSync)(a)}catch{return!1}let e=new Set;for(let a of b)try{e.add((0,d.realpathSync)(a))}catch{}return h(c,e)}},96487:()=>{}};
|