@co0ontty/wand 4.63.0 → 4.64.0
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/dist/build-info.d.ts +18 -0
- package/dist/build-info.js +13 -0
- package/dist/build-info.json +3 -3
- package/dist/config.js +6 -11
- package/dist/env-utils.d.ts +5 -0
- package/dist/env-utils.js +4 -0
- package/dist/git-quick-commit.js +7 -11
- package/dist/ios-ota.js +8 -29
- package/dist/layout-tree.d.ts +6 -0
- package/dist/layout-tree.js +9 -0
- package/dist/missions.js +2 -2
- package/dist/models.js +2 -5
- package/dist/path-repair.js +2 -1
- package/dist/process-manager.js +10 -20
- package/dist/provider-cli-updater.js +3 -6
- package/dist/pty-shell-launch.js +3 -5
- package/dist/server-app-connect.js +11 -46
- package/dist/server-github-routes.js +12 -11
- package/dist/server-local-preview-routes.js +6 -1
- package/dist/server-request.d.ts +4 -2
- package/dist/server-request.js +25 -4
- package/dist/server-session-routes.js +14 -78
- package/dist/server-settings-routes.d.ts +2 -6
- package/dist/server-settings-routes.js +3 -8
- package/dist/server-task-routes.d.ts +9 -6
- package/dist/server-task-routes.js +50 -43
- package/dist/server-update-routes.d.ts +2 -5
- package/dist/server-update-routes.js +42 -9
- package/dist/server-workspace-routes.js +4 -12
- package/dist/server.js +33 -99
- package/dist/session-ai-context.js +6 -38
- package/dist/session-provider.d.ts +16 -0
- package/dist/session-provider.js +49 -0
- package/dist/session-registry.d.ts +10 -0
- package/dist/session-registry.js +41 -14
- package/dist/session-topic.js +3 -11
- package/dist/shell-quote.d.ts +9 -0
- package/dist/shell-quote.js +13 -0
- package/dist/signal-utils.d.ts +10 -0
- package/dist/signal-utils.js +38 -0
- package/dist/storage.js +17 -55
- package/dist/structured-client-protocol.js +4 -8
- package/dist/structured-codex-protocol.js +1 -6
- package/dist/structured-content.d.ts +2 -0
- package/dist/structured-content.js +6 -0
- package/dist/structured-exec-host.d.ts +0 -2
- package/dist/structured-exec-host.js +2 -11
- package/dist/structured-exec-pump.js +2 -12
- package/dist/structured-grok-adapter.js +1 -5
- package/dist/structured-opencode-adapter.js +1 -5
- package/dist/structured-pi-adapter.js +9 -11
- package/dist/structured-provider-common.d.ts +11 -3
- package/dist/structured-provider-common.js +16 -23
- package/dist/structured-qoder-adapter.js +3 -8
- package/dist/structured-session-manager.js +6 -17
- package/dist/task-title.js +4 -12
- package/dist/task-types.d.ts +23 -1
- package/dist/task-types.js +26 -0
- package/dist/terminal-daemon-protocol.d.ts +0 -6
- package/dist/terminal-daemon-server.js +1 -9
- package/dist/text-utils.d.ts +7 -0
- package/dist/text-utils.js +15 -0
- package/dist/tui/attach.js +17 -93
- package/dist/tui/commands.d.ts +2 -0
- package/dist/tui/commands.js +3 -2
- package/dist/tui/index.js +28 -115
- package/dist/tui/runtime-utils.d.ts +7 -0
- package/dist/tui/runtime-utils.js +33 -0
- package/dist/tui/service-actions.d.ts +31 -0
- package/dist/tui/service-actions.js +86 -0
- package/dist/tui/snapshot.js +1 -8
- package/dist/update-helper.js +6 -35
- package/dist/web-ui/content/scripts.js +397 -497
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/content/tailwind.css +7210 -0
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/web-ui/index.js +3 -3
- package/dist/web-ui/provider-identity.d.ts +4 -0
- package/dist/web-ui/provider-identity.js +9 -0
- package/dist/web-ui/styles.js +24 -7
- package/dist/workspace-binding.d.ts +2 -0
- package/dist/workspace-binding.js +4 -3
- package/package.json +9 -12
|
@@ -8,10 +8,10 @@ import { expandHomePath } from "./middleware/path-safety.js";
|
|
|
8
8
|
import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
|
|
9
9
|
import { collectSessionTopicBlocklist } from "./session-topic.js";
|
|
10
10
|
import { resolveSessionDisplayTitle } from "./session-transport.js";
|
|
11
|
-
import {
|
|
12
|
-
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, syncDirectoryNameForWorkspace, } from "./workspace-binding.js";
|
|
11
|
+
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, normalizeProjectCwd, projectCwdForSession, isGlobalWorkspace, syncDirectoryNameForWorkspace } from "./workspace-binding.js";
|
|
13
12
|
import { archiveBoardTaskForWorkspaceTask, ensureBoardTaskForWorkspaceTask } from "./wand-task-sync.js";
|
|
14
|
-
|
|
13
|
+
import { isSessionProvider } from "./session-provider.js";
|
|
14
|
+
import { firstLayoutTabId } from "./layout-tree.js";
|
|
15
15
|
function workspaceSessionTitle(session, names = {}) {
|
|
16
16
|
return resolveSessionDisplayTitle(session, collectSessionTopicBlocklist({
|
|
17
17
|
taskName: names.taskName,
|
|
@@ -74,7 +74,7 @@ function rememberCreatedAt(target, iso) {
|
|
|
74
74
|
target.createdAt = value;
|
|
75
75
|
}
|
|
76
76
|
function parseDefaultProvider(value) {
|
|
77
|
-
return
|
|
77
|
+
return isSessionProvider(value) ? value : undefined;
|
|
78
78
|
}
|
|
79
79
|
/** Resolve + validate a workspace cwd: expand home, require an existing directory. */
|
|
80
80
|
function resolveWorkspaceCwd(raw) {
|
|
@@ -96,9 +96,6 @@ function deleteSessions(storage, sessions, sessionIds) {
|
|
|
96
96
|
storage.deleteSession(sessionId);
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
-
function isGlobalWorkspace(workspace) {
|
|
100
|
-
return workspace.kind === "global" || workspace.id === GLOBAL_WORKSPACE_ID;
|
|
101
|
-
}
|
|
102
99
|
function taskRuntimeCwd(task, workspace) {
|
|
103
100
|
return task.worktree?.path ?? task.cwd ?? workspace?.cwd ?? "";
|
|
104
101
|
}
|
|
@@ -226,11 +223,6 @@ function layoutHasTab(node, tabId) {
|
|
|
226
223
|
return node.tabs.some((tab) => tab.id === tabId);
|
|
227
224
|
return layoutHasTab(node.children[0], tabId) || layoutHasTab(node.children[1], tabId);
|
|
228
225
|
}
|
|
229
|
-
function firstLayoutTabId(node) {
|
|
230
|
-
if (node.type === "pane")
|
|
231
|
-
return node.tabs[node.active]?.id ?? node.tabs[0]?.id;
|
|
232
|
-
return firstLayoutTabId(node.children[0]) ?? firstLayoutTabId(node.children[1]);
|
|
233
|
-
}
|
|
234
226
|
/** 校验任务级工作窗口集合;旧版单棵布局会兼容升级成一个 window。 */
|
|
235
227
|
export function sanitizeTaskLayout(value) {
|
|
236
228
|
const legacy = sanitizeLayoutNode(value);
|
package/dist/server.js
CHANGED
|
@@ -24,6 +24,7 @@ import { registerLocalPreviewRoutes } from "./server-local-preview-routes.js";
|
|
|
24
24
|
import { registerSettingsRoutes } from "./server-settings-routes.js";
|
|
25
25
|
import { appTokenLoginPayload, buildStructuredChatPersonaPayload, isBrowserExtensionOrigin, resolveAppConnectCode, resolveStructuredChatAvatarPath, verifyAppToken, } from "./server-app-connect.js";
|
|
26
26
|
import { firstHeaderValue, sendRouteError } from "./server-request.js";
|
|
27
|
+
import { inferProviderFromCommand, isSessionProvider } from "./session-provider.js";
|
|
27
28
|
import { registerVaultRoutes } from "./server-vault-routes.js";
|
|
28
29
|
import { registerGithubRoutes } from "./server-github-routes.js";
|
|
29
30
|
import { registerTaskRoutes } from "./server-task-routes.js";
|
|
@@ -41,7 +42,7 @@ import { checkPackageUpdateAsync, installPackageGloballyAsync, normalizeUpdateCh
|
|
|
41
42
|
import { repairServiceUnitAfterUpdate } from "./service-self-repair.js";
|
|
42
43
|
import { computeRelaunch } from "./relaunch.js";
|
|
43
44
|
import { RuntimeConfigState } from "./runtime-config.js";
|
|
44
|
-
import {
|
|
45
|
+
import { safeServiceInstalled } from "./tui/runtime-utils.js";
|
|
45
46
|
import { checkManagedServiceUpdatePreflight, } from "./update-helper.js";
|
|
46
47
|
import { toSessionDetailDTO } from "./session-transport.js";
|
|
47
48
|
import { registerUploadRoutes } from "./upload-routes.js";
|
|
@@ -64,6 +65,16 @@ const PKG_NAME = PKG_JSON.name;
|
|
|
64
65
|
const PKG_VERSION = PKG_JSON.version;
|
|
65
66
|
const PKG_NODE_REQ = PKG_JSON.engines?.node ?? ">=22.5.0";
|
|
66
67
|
const PKG_REPO_URL = "https://github.com/co0ontty/wand";
|
|
68
|
+
/** 结构化聊天头像允许的图片类型;未知扩展名回 415。 */
|
|
69
|
+
const AVATAR_CONTENT_TYPES = {
|
|
70
|
+
".svg": "image/svg+xml",
|
|
71
|
+
".png": "image/png",
|
|
72
|
+
".jpg": "image/jpeg",
|
|
73
|
+
".jpeg": "image/jpeg",
|
|
74
|
+
".webp": "image/webp",
|
|
75
|
+
".gif": "image/gif",
|
|
76
|
+
".avif": "image/avif",
|
|
77
|
+
};
|
|
67
78
|
// ── Update check cache ──
|
|
68
79
|
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
69
80
|
const MODEL_CATALOG_AUTO_REFRESH_INTERVAL_MS = 30 * 60 * 1000;
|
|
@@ -82,6 +93,7 @@ async function checkLatestPackageVersion(channel, forceRefresh = false) {
|
|
|
82
93
|
}
|
|
83
94
|
return info;
|
|
84
95
|
}
|
|
96
|
+
// ── Build info (构建时打入的 commit SHA) + Beta 通道 ──
|
|
85
97
|
/** 读取 dist/build-info.json(由 scripts/stamp-build-info.js 在 build 时生成)。 */
|
|
86
98
|
function readBuildInfo() {
|
|
87
99
|
try {
|
|
@@ -184,6 +196,15 @@ function authenticateBearerAppToken(req, storage, config) {
|
|
|
184
196
|
return null;
|
|
185
197
|
}
|
|
186
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* 解析 PTY 会话的 provider:显式 `provider` 字段优先,其次按命令前缀识别;
|
|
201
|
+
* 都识别不出时回落到 Claude,与旧版客户端行为保持一致。
|
|
202
|
+
*/
|
|
203
|
+
function resolveProviderForCommand(provider, command) {
|
|
204
|
+
if (isSessionProvider(provider))
|
|
205
|
+
return provider;
|
|
206
|
+
return inferProviderFromCommand(command) ?? "claude";
|
|
207
|
+
}
|
|
187
208
|
// ── Startup error handling ──
|
|
188
209
|
process.on("uncaughtException", (err) => {
|
|
189
210
|
wandError("服务器异常", err.message, "请检查配置是否正确,或尝试重启服务。");
|
|
@@ -261,24 +282,15 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
261
282
|
const storage = new WandStorage(resolveDatabasePath(configPath));
|
|
262
283
|
const runtimeConfig = new RuntimeConfigState(config);
|
|
263
284
|
const authService = new AuthService(storage);
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
codex: prefStorage.getPreference("pref:defaultCodexModel", config.defaultCodexModel ?? ""),
|
|
274
|
-
opencode: prefStorage.getPreference("pref:defaultOpenCodeModel", config.defaultOpenCodeModel ?? ""),
|
|
275
|
-
grok: prefStorage.getPreference("pref:defaultGrokModel", config.defaultGrokModel ?? ""),
|
|
276
|
-
qoder: prefStorage.getPreference("pref:defaultQoderModel", config.defaultQoderModel ?? ""),
|
|
277
|
-
pi: prefStorage.getPreference("pref:defaultPiModel", config.defaultPiModel ?? ""),
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
return getProviderDefaultModels(config);
|
|
281
|
-
};
|
|
285
|
+
// 默认模型优先读存储(UI 改设置后实时生效),未设置时由 getPreference 回落到 config。
|
|
286
|
+
const getCurrentDefaultModels = () => ({
|
|
287
|
+
claude: storage.getPreference("pref:defaultModel", config.defaultModel ?? ""),
|
|
288
|
+
codex: storage.getPreference("pref:defaultCodexModel", config.defaultCodexModel ?? ""),
|
|
289
|
+
opencode: storage.getPreference("pref:defaultOpenCodeModel", config.defaultOpenCodeModel ?? ""),
|
|
290
|
+
grok: storage.getPreference("pref:defaultGrokModel", config.defaultGrokModel ?? ""),
|
|
291
|
+
qoder: storage.getPreference("pref:defaultQoderModel", config.defaultQoderModel ?? ""),
|
|
292
|
+
pi: storage.getPreference("pref:defaultPiModel", config.defaultPiModel ?? ""),
|
|
293
|
+
});
|
|
282
294
|
const getModelRefreshOptions = () => {
|
|
283
295
|
const injected = options.modelRefreshOptions?.() ?? {};
|
|
284
296
|
const currentDefaults = getCurrentDefaultModels();
|
|
@@ -390,20 +402,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
390
402
|
res.status(404).end();
|
|
391
403
|
return;
|
|
392
404
|
}
|
|
393
|
-
const
|
|
394
|
-
const contentType = ext === ".svg"
|
|
395
|
-
? "image/svg+xml"
|
|
396
|
-
: ext === ".png"
|
|
397
|
-
? "image/png"
|
|
398
|
-
: ext === ".jpg" || ext === ".jpeg"
|
|
399
|
-
? "image/jpeg"
|
|
400
|
-
: ext === ".webp"
|
|
401
|
-
? "image/webp"
|
|
402
|
-
: ext === ".gif"
|
|
403
|
-
? "image/gif"
|
|
404
|
-
: ext === ".avif"
|
|
405
|
-
? "image/avif"
|
|
406
|
-
: null;
|
|
405
|
+
const contentType = AVATAR_CONTENT_TYPES[path.extname(resolvedPath).toLowerCase()];
|
|
407
406
|
if (!contentType) {
|
|
408
407
|
res.status(415).json({ error: "不支持的头像格式。" });
|
|
409
408
|
return;
|
|
@@ -715,17 +714,7 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
715
714
|
const rawCommand = body.command?.trim() ?? "";
|
|
716
715
|
const provider = interactiveShell
|
|
717
716
|
? undefined
|
|
718
|
-
: body.provider
|
|
719
|
-
? "codex"
|
|
720
|
-
: body.provider === "opencode" || /^opencode\b/.test(rawCommand)
|
|
721
|
-
? "opencode"
|
|
722
|
-
: body.provider === "grok" || /^grok\b/.test(rawCommand)
|
|
723
|
-
? "grok"
|
|
724
|
-
: body.provider === "qoder" || /^qodercli\b/.test(rawCommand)
|
|
725
|
-
? "qoder"
|
|
726
|
-
: body.provider === "pi" || /^pi\b/.test(rawCommand)
|
|
727
|
-
? "pi"
|
|
728
|
-
: "claude";
|
|
717
|
+
: resolveProviderForCommand(body.provider, rawCommand);
|
|
729
718
|
// Older clients used the provider id as the PTY command. Qoder's executable
|
|
730
719
|
// is named qodercli, so keep those clients working while preserving custom commands.
|
|
731
720
|
const command = provider === "qoder" && rawCommand === "qoder"
|
|
@@ -845,14 +834,6 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
845
834
|
// previous web process was down; fire-and-forget, failures are logged inside.
|
|
846
835
|
void structuredSessions.recoverDetachedRuns();
|
|
847
836
|
// ── Restart endpoint (needs server + wss in scope) ──
|
|
848
|
-
function safeServiceInstalled() {
|
|
849
|
-
try {
|
|
850
|
-
return isServiceInstalled();
|
|
851
|
-
}
|
|
852
|
-
catch {
|
|
853
|
-
return false;
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
837
|
/**
|
|
857
838
|
* 统一的关服 + 重启。重启方式由 computeRelaunch 决定:
|
|
858
839
|
* - systemd 托管且已装服务 → 仅退出,交给 Restart=always 用(更新自修复后可能刚被
|
|
@@ -975,53 +956,6 @@ export async function startServer(config, configPath, options = {}) {
|
|
|
975
956
|
}, MODEL_CATALOG_AUTO_REFRESH_INTERVAL_MS);
|
|
976
957
|
modelCatalogRefreshTimer.unref();
|
|
977
958
|
}
|
|
978
|
-
// ── Auto-update endpoints ──
|
|
979
|
-
app.get("/api/auto-update", requireAdmin, (_req, res) => {
|
|
980
|
-
const web = storage.getConfigValue("autoUpdateWeb") === "true";
|
|
981
|
-
const apk = storage.getConfigValue("autoUpdateApk") === "true";
|
|
982
|
-
const dmg = storage.getConfigValue("autoUpdateDmg") === "true";
|
|
983
|
-
const cli = storage.getConfigValue("autoUpdateProviderClis") === "true";
|
|
984
|
-
res.json({ web, apk, dmg, cli });
|
|
985
|
-
});
|
|
986
|
-
app.post("/api/auto-update", requireAdmin, (req, res) => {
|
|
987
|
-
const { web, apk, dmg, cli } = req.body;
|
|
988
|
-
if (typeof web === "boolean") {
|
|
989
|
-
storage.setConfigValue("autoUpdateWeb", String(web));
|
|
990
|
-
}
|
|
991
|
-
if (typeof apk === "boolean") {
|
|
992
|
-
storage.setConfigValue("autoUpdateApk", String(apk));
|
|
993
|
-
}
|
|
994
|
-
if (typeof dmg === "boolean") {
|
|
995
|
-
storage.setConfigValue("autoUpdateDmg", String(dmg));
|
|
996
|
-
}
|
|
997
|
-
if (typeof cli === "boolean") {
|
|
998
|
-
storage.setConfigValue("autoUpdateProviderClis", String(cli));
|
|
999
|
-
}
|
|
1000
|
-
res.json({
|
|
1001
|
-
web: storage.getConfigValue("autoUpdateWeb") === "true",
|
|
1002
|
-
apk: storage.getConfigValue("autoUpdateApk") === "true",
|
|
1003
|
-
dmg: storage.getConfigValue("autoUpdateDmg") === "true",
|
|
1004
|
-
cli: storage.getConfigValue("autoUpdateProviderClis") === "true",
|
|
1005
|
-
});
|
|
1006
|
-
});
|
|
1007
|
-
// ── Update channel (stable / beta) ──
|
|
1008
|
-
app.get("/api/update-channel", requireAdmin, (_req, res) => {
|
|
1009
|
-
res.json({
|
|
1010
|
-
channel: getUpdateChannel(),
|
|
1011
|
-
build: {
|
|
1012
|
-
commit: BUILD_INFO.commit,
|
|
1013
|
-
shortCommit: BUILD_INFO.commit ? BUILD_INFO.commit.slice(0, 7) : null,
|
|
1014
|
-
builtAt: BUILD_INFO.builtAt,
|
|
1015
|
-
channel: BUILD_INFO.channel,
|
|
1016
|
-
},
|
|
1017
|
-
});
|
|
1018
|
-
});
|
|
1019
|
-
app.post("/api/update-channel", requireAdmin, (req, res) => {
|
|
1020
|
-
const body = (req.body ?? {});
|
|
1021
|
-
const channel = body.channel === "beta" ? "beta" : "stable";
|
|
1022
|
-
storage.setConfigValue("updateChannel", channel);
|
|
1023
|
-
res.json({ channel });
|
|
1024
|
-
});
|
|
1025
959
|
// Express 4 does not forward rejected route promises automatically. Every
|
|
1026
960
|
// async route above is wrapped with asyncRoute, and this final middleware
|
|
1027
961
|
// keeps parser, synchronous middleware, and async failures JSON-shaped.
|
|
@@ -1,51 +1,19 @@
|
|
|
1
1
|
import { getDefaultModelForProvider } from "./config.js";
|
|
2
2
|
import { discoverCliSystemAiConfigs, mergeSystemAiConfigs, systemAiProfiles, } from "./system-ai.js";
|
|
3
|
+
import { inferProviderFromCommand, inferProviderFromRunner, isSessionProvider } from "./session-provider.js";
|
|
3
4
|
/**
|
|
4
5
|
* Resolve the provider from every representation used by current and legacy
|
|
5
6
|
* sessions. Older persisted sessions may not have the top-level provider, but
|
|
6
7
|
* still identify Codex through structuredState, runner, or command.
|
|
7
8
|
*/
|
|
8
9
|
export function resolveSessionProvider(snapshot) {
|
|
9
|
-
if (snapshot.provider
|
|
10
|
-
|| snapshot.provider === "codex"
|
|
11
|
-
|| snapshot.provider === "opencode"
|
|
12
|
-
|| snapshot.provider === "grok"
|
|
13
|
-
|| snapshot.provider === "qoder"
|
|
14
|
-
|| snapshot.provider === "pi") {
|
|
10
|
+
if (isSessionProvider(snapshot.provider))
|
|
15
11
|
return snapshot.provider;
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|| snapshot.structuredState?.provider === "opencode"
|
|
20
|
-
|| snapshot.structuredState?.provider === "grok"
|
|
21
|
-
|| snapshot.structuredState?.provider === "qoder"
|
|
22
|
-
|| snapshot.structuredState?.provider === "pi") {
|
|
23
|
-
return snapshot.structuredState.provider;
|
|
24
|
-
}
|
|
12
|
+
const structuredProvider = snapshot.structuredState?.provider;
|
|
13
|
+
if (isSessionProvider(structuredProvider))
|
|
14
|
+
return structuredProvider;
|
|
25
15
|
const runner = snapshot.runner ?? snapshot.structuredState?.runner;
|
|
26
|
-
|
|
27
|
-
return "codex";
|
|
28
|
-
if (runner === "opencode-cli-run")
|
|
29
|
-
return "opencode";
|
|
30
|
-
if (runner === "grok-cli-headless")
|
|
31
|
-
return "grok";
|
|
32
|
-
if (runner === "qoder-cli-print")
|
|
33
|
-
return "qoder";
|
|
34
|
-
if (runner === "pi-cli-json")
|
|
35
|
-
return "pi";
|
|
36
|
-
if (runner === "claude-cli" || runner === "claude-cli-print" || runner === "claude-sdk")
|
|
37
|
-
return "claude";
|
|
38
|
-
if (/^codex\b/i.test(snapshot.command.trim()))
|
|
39
|
-
return "codex";
|
|
40
|
-
if (/^opencode\b/i.test(snapshot.command.trim()))
|
|
41
|
-
return "opencode";
|
|
42
|
-
if (/^grok\b/i.test(snapshot.command.trim()))
|
|
43
|
-
return "grok";
|
|
44
|
-
if (/^qodercli\b/i.test(snapshot.command.trim()))
|
|
45
|
-
return "qoder";
|
|
46
|
-
if (/^pi\b/i.test(snapshot.command.trim()))
|
|
47
|
-
return "pi";
|
|
48
|
-
return "claude";
|
|
16
|
+
return inferProviderFromRunner(runner) ?? inferProviderFromCommand(snapshot.command) ?? "claude";
|
|
49
17
|
}
|
|
50
18
|
function normalizeModel(value) {
|
|
51
19
|
const model = value?.trim();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SessionProvider } from "./types.js";
|
|
2
|
+
/** 全部受支持的会话 provider;顺序与 UI / 派发选择器一致。 */
|
|
3
|
+
export declare const SESSION_PROVIDERS: readonly ["claude", "codex", "opencode", "grok", "qoder", "pi"];
|
|
4
|
+
/** 运行时守卫:把配置、数据库、请求体里的任意值收敛成 SessionProvider。 */
|
|
5
|
+
export declare function isSessionProvider(value: unknown): value is SessionProvider;
|
|
6
|
+
/** provider 对应的 CLI 可执行文件名(Qoder 的 CLI 叫 qodercli)。 */
|
|
7
|
+
export declare function providerCliCommand(provider: SessionProvider): string;
|
|
8
|
+
/** 从 runner 名推断 provider;`pty` 等与 provider 无关的 runner 返回 undefined。 */
|
|
9
|
+
export declare function inferProviderFromRunner(runner: unknown): SessionProvider | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* 从启动命令推断 provider;无法识别时返回 undefined,由调用方决定默认值。
|
|
12
|
+
*
|
|
13
|
+
* 识别的是命令行的第一个 token:`claude`、`npx claude`、任意路径下的 `claude`
|
|
14
|
+
* 都算 Claude,其余 provider 要求命令以其 CLI 名开头。
|
|
15
|
+
*/
|
|
16
|
+
export declare function inferProviderFromCommand(command: string): SessionProvider | undefined;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** 全部受支持的会话 provider;顺序与 UI / 派发选择器一致。 */
|
|
2
|
+
export const SESSION_PROVIDERS = ["claude", "codex", "opencode", "grok", "qoder", "pi"];
|
|
3
|
+
const SESSION_PROVIDER_SET = new Set(SESSION_PROVIDERS);
|
|
4
|
+
/** 运行时守卫:把配置、数据库、请求体里的任意值收敛成 SessionProvider。 */
|
|
5
|
+
export function isSessionProvider(value) {
|
|
6
|
+
return typeof value === "string" && SESSION_PROVIDER_SET.has(value);
|
|
7
|
+
}
|
|
8
|
+
/** provider 对应的 CLI 可执行文件名(Qoder 的 CLI 叫 qodercli)。 */
|
|
9
|
+
export function providerCliCommand(provider) {
|
|
10
|
+
return provider === "qoder" ? "qodercli" : provider;
|
|
11
|
+
}
|
|
12
|
+
/** 从 runner 名推断 provider;`pty` 等与 provider 无关的 runner 返回 undefined。 */
|
|
13
|
+
export function inferProviderFromRunner(runner) {
|
|
14
|
+
if (runner === "claude-cli" || runner === "claude-cli-print" || runner === "claude-sdk")
|
|
15
|
+
return "claude";
|
|
16
|
+
if (runner === "codex-cli-exec")
|
|
17
|
+
return "codex";
|
|
18
|
+
if (runner === "opencode-cli-run")
|
|
19
|
+
return "opencode";
|
|
20
|
+
if (runner === "grok-cli-headless")
|
|
21
|
+
return "grok";
|
|
22
|
+
if (runner === "qoder-cli-print")
|
|
23
|
+
return "qoder";
|
|
24
|
+
if (runner === "pi-cli-json")
|
|
25
|
+
return "pi";
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 从启动命令推断 provider;无法识别时返回 undefined,由调用方决定默认值。
|
|
30
|
+
*
|
|
31
|
+
* 识别的是命令行的第一个 token:`claude`、`npx claude`、任意路径下的 `claude`
|
|
32
|
+
* 都算 Claude,其余 provider 要求命令以其 CLI 名开头。
|
|
33
|
+
*/
|
|
34
|
+
export function inferProviderFromCommand(command) {
|
|
35
|
+
const value = command.trim();
|
|
36
|
+
if (/^(?:claude|npx\s+claude|[^\s]+\/claude)(?:\s|$)/i.test(value))
|
|
37
|
+
return "claude";
|
|
38
|
+
if (/^codex\b/i.test(value))
|
|
39
|
+
return "codex";
|
|
40
|
+
if (/^opencode\b/i.test(value))
|
|
41
|
+
return "opencode";
|
|
42
|
+
if (/^grok\b/i.test(value))
|
|
43
|
+
return "grok";
|
|
44
|
+
if (/^qodercli\b/i.test(value))
|
|
45
|
+
return "qoder";
|
|
46
|
+
if (/^pi\b/i.test(value))
|
|
47
|
+
return "pi";
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
@@ -3,6 +3,15 @@ import type { StructuredSessionManager } from "./structured-session-manager.js";
|
|
|
3
3
|
import type { WandStorage } from "./storage.js";
|
|
4
4
|
import type { ExecutionMode, SessionSnapshot } from "./types.js";
|
|
5
5
|
export type SessionOwner = "structured" | "pty" | "storage";
|
|
6
|
+
type HiddenSessionStore = Pick<WandStorage, "getConfigValue" | "setConfigValue">;
|
|
7
|
+
/** 读取 `hidden_claude_session_ids`;坏数据 / 空值按空集合处理,不抛错。 */
|
|
8
|
+
export declare function readHiddenSessionIds(storage: Pick<HiddenSessionStore, "getConfigValue">): Set<string>;
|
|
9
|
+
/** 写回 `hidden_claude_session_ids`(保持既有的 JSON 数组格式)。 */
|
|
10
|
+
export declare function writeHiddenSessionIds(storage: Pick<HiddenSessionStore, "setConfigValue">, ids: Set<string>): void;
|
|
11
|
+
/** 追加若干 provider 原生 session id;无变化时不写盘。 */
|
|
12
|
+
export declare function addHiddenSessionIds(storage: HiddenSessionStore, ids: readonly string[]): void;
|
|
13
|
+
/** 移除若干 provider 原生 session id;无变化时不写盘。 */
|
|
14
|
+
export declare function removeHiddenSessionIds(storage: HiddenSessionStore, ids: readonly string[]): void;
|
|
6
15
|
/**
|
|
7
16
|
* Coordinates session ownership without merging the two runner managers.
|
|
8
17
|
* Live structured state wins over PTY, and both win over a durable fallback.
|
|
@@ -25,3 +34,4 @@ export declare class SessionRegistry {
|
|
|
25
34
|
deleteWithProviderHistory(id: string): SessionSnapshot | null;
|
|
26
35
|
private updateStored;
|
|
27
36
|
}
|
|
37
|
+
export {};
|
package/dist/session-registry.js
CHANGED
|
@@ -1,22 +1,52 @@
|
|
|
1
1
|
import { cleanupWorktreeSync } from "./git-worktree.js";
|
|
2
|
+
import { inferProviderFromCommand } from "./session-provider.js";
|
|
2
3
|
function slimSnapshot(snapshot) {
|
|
3
4
|
const { output: _output, messages: _messages, ...slim } = snapshot;
|
|
4
5
|
return { ...slim, output: "" };
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
+
/** 读取 `hidden_claude_session_ids`;坏数据 / 空值按空集合处理,不抛错。 */
|
|
8
|
+
export function readHiddenSessionIds(storage) {
|
|
7
9
|
const raw = storage.getConfigValue("hidden_claude_session_ids");
|
|
8
|
-
|
|
10
|
+
if (!raw)
|
|
11
|
+
return new Set();
|
|
9
12
|
try {
|
|
10
|
-
const parsed =
|
|
11
|
-
|
|
13
|
+
const parsed = JSON.parse(raw);
|
|
14
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : []);
|
|
12
15
|
}
|
|
13
16
|
catch {
|
|
14
|
-
|
|
17
|
+
return new Set();
|
|
15
18
|
}
|
|
16
|
-
|
|
19
|
+
}
|
|
20
|
+
/** 写回 `hidden_claude_session_ids`(保持既有的 JSON 数组格式)。 */
|
|
21
|
+
export function writeHiddenSessionIds(storage, ids) {
|
|
22
|
+
storage.setConfigValue("hidden_claude_session_ids", JSON.stringify(Array.from(ids)));
|
|
23
|
+
}
|
|
24
|
+
/** 追加若干 provider 原生 session id;无变化时不写盘。 */
|
|
25
|
+
export function addHiddenSessionIds(storage, ids) {
|
|
26
|
+
if (ids.length === 0)
|
|
27
|
+
return;
|
|
28
|
+
const hidden = readHiddenSessionIds(storage);
|
|
29
|
+
let changed = false;
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
if (hidden.has(id))
|
|
32
|
+
continue;
|
|
33
|
+
hidden.add(id);
|
|
34
|
+
changed = true;
|
|
35
|
+
}
|
|
36
|
+
if (changed)
|
|
37
|
+
writeHiddenSessionIds(storage, hidden);
|
|
38
|
+
}
|
|
39
|
+
/** 移除若干 provider 原生 session id;无变化时不写盘。 */
|
|
40
|
+
export function removeHiddenSessionIds(storage, ids) {
|
|
41
|
+
if (ids.length === 0)
|
|
17
42
|
return;
|
|
18
|
-
hidden
|
|
19
|
-
|
|
43
|
+
const hidden = readHiddenSessionIds(storage);
|
|
44
|
+
let changed = false;
|
|
45
|
+
for (const id of ids) {
|
|
46
|
+
changed = hidden.delete(id) || changed;
|
|
47
|
+
}
|
|
48
|
+
if (changed)
|
|
49
|
+
writeHiddenSessionIds(storage, hidden);
|
|
20
50
|
}
|
|
21
51
|
/**
|
|
22
52
|
* Coordinates session ownership without merging the two runner managers.
|
|
@@ -140,11 +170,8 @@ export class SessionRegistry {
|
|
|
140
170
|
return snapshot;
|
|
141
171
|
const provider = snapshot.provider
|
|
142
172
|
?? snapshot.structuredState?.provider
|
|
143
|
-
?? (
|
|
144
|
-
|
|
145
|
-
: /^opencode\b/i.test(snapshot.command.trim()) ? "opencode"
|
|
146
|
-
: /^grok\b/i.test(snapshot.command.trim()) ? "grok"
|
|
147
|
-
: /^qodercli\b/i.test(snapshot.command.trim()) ? "qoder" : "claude");
|
|
173
|
+
?? inferProviderFromCommand(snapshot.command)
|
|
174
|
+
?? "claude";
|
|
148
175
|
if (provider === "claude") {
|
|
149
176
|
this.processes.deleteClaudeHistoryFiles([{ claudeSessionId: providerSessionId, cwd: snapshot.cwd }]);
|
|
150
177
|
}
|
|
@@ -154,7 +181,7 @@ export class SessionRegistry {
|
|
|
154
181
|
else {
|
|
155
182
|
return snapshot;
|
|
156
183
|
}
|
|
157
|
-
|
|
184
|
+
addHiddenSessionIds(this.storage, [providerSessionId]);
|
|
158
185
|
return snapshot;
|
|
159
186
|
}
|
|
160
187
|
updateStored(id, update) {
|
package/dist/session-topic.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { callConfiguredAiText } from "./git-quick-commit.js";
|
|
2
2
|
import { skipAnsiSequence } from "./pty-text-utils.js";
|
|
3
|
+
import { clipAtWordBoundary } from "./text-utils.js";
|
|
3
4
|
const MAX_PROMPT_LENGTH = 12_000;
|
|
4
5
|
const PTY_TOPIC_DRAFT_MAX = 4_000;
|
|
5
6
|
/**
|
|
@@ -140,15 +141,6 @@ export function shouldAcceptGeneratedSessionTitle(title, blockedTitles = []) {
|
|
|
140
141
|
return false;
|
|
141
142
|
return !blockedTitles.some((blocked) => isSameSessionTitle(cleaned, blocked));
|
|
142
143
|
}
|
|
143
|
-
function clipTitle(value, maxLength = TITLE_MAX_LENGTH) {
|
|
144
|
-
if (value.length <= maxLength)
|
|
145
|
-
return value;
|
|
146
|
-
const sliced = value.slice(0, maxLength);
|
|
147
|
-
const lastSpace = sliced.lastIndexOf(" ");
|
|
148
|
-
if (lastSpace >= Math.floor(maxLength * 0.55))
|
|
149
|
-
return sliced.slice(0, lastSpace);
|
|
150
|
-
return sliced;
|
|
151
|
-
}
|
|
152
144
|
/** Immediate title from the user's command, without waiting for the model. */
|
|
153
145
|
export function summarizeSessionTitleFromInput(input, options = {}) {
|
|
154
146
|
const blockedTitles = options.blockedTitles ?? [];
|
|
@@ -156,9 +148,9 @@ export function summarizeSessionTitleFromInput(input, options = {}) {
|
|
|
156
148
|
.split(/\r?\n/)
|
|
157
149
|
.map((part) => part.replace(/^#+\s*/, "").replace(/\s+/g, " ").trim())
|
|
158
150
|
.filter(Boolean);
|
|
159
|
-
const fallback =
|
|
151
|
+
const fallback = clipAtWordBoundary(lines[0] ?? "", TITLE_MAX_LENGTH);
|
|
160
152
|
for (const line of lines) {
|
|
161
|
-
const title =
|
|
153
|
+
const title = clipAtWordBoundary(line, TITLE_MAX_LENGTH);
|
|
162
154
|
if (shouldAcceptGeneratedSessionTitle(title, blockedTitles))
|
|
163
155
|
return title;
|
|
164
156
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSIX shell 引用工具。
|
|
3
|
+
*
|
|
4
|
+
* 单引号是唯一在所有 POSIX shell 里都没有展开语义的引用形式;字符串里已有的
|
|
5
|
+
* 单引号用 `'\''` 收尾再续上,保证结果仍然是一个字面量。
|
|
6
|
+
*/
|
|
7
|
+
export declare function shellQuote(value: string): string;
|
|
8
|
+
/** 把一组值拼成 `'a' 'b'` 形式的参数列表(用于生成 shell 脚本里的数组字面量)。 */
|
|
9
|
+
export declare function shellQuoteAll(values: readonly string[]): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* POSIX shell 引用工具。
|
|
3
|
+
*
|
|
4
|
+
* 单引号是唯一在所有 POSIX shell 里都没有展开语义的引用形式;字符串里已有的
|
|
5
|
+
* 单引号用 `'\''` 收尾再续上,保证结果仍然是一个字面量。
|
|
6
|
+
*/
|
|
7
|
+
export function shellQuote(value) {
|
|
8
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
9
|
+
}
|
|
10
|
+
/** 把一组值拼成 `'a' 'b'` 形式的参数列表(用于生成 shell 脚本里的数组字面量)。 */
|
|
11
|
+
export function shellQuoteAll(values) {
|
|
12
|
+
return values.map(shellQuote).join(" ");
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unix 信号名与编号之间的互转。
|
|
3
|
+
*
|
|
4
|
+
* Node 只在这些编号上给出稳定语义(Linux/macOS 一致),其余平台差异较大,
|
|
5
|
+
* 因此这里固定一张小表,未知编号回落 SIGTERM、未知名字回落 0。
|
|
6
|
+
*/
|
|
7
|
+
/** 名字 → 编号;未知名字返回 0(与 waitpid 的"无信号"一致)。 */
|
|
8
|
+
export declare function signalNumberFromName(signal: string): number;
|
|
9
|
+
/** 编号 → 名字;0 / null 表示没有信号,未知编号回落 SIGTERM。 */
|
|
10
|
+
export declare function signalNameFromNumber(signal: number | null): NodeJS.Signals | null;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unix 信号名与编号之间的互转。
|
|
3
|
+
*
|
|
4
|
+
* Node 只在这些编号上给出稳定语义(Linux/macOS 一致),其余平台差异较大,
|
|
5
|
+
* 因此这里固定一张小表,未知编号回落 SIGTERM、未知名字回落 0。
|
|
6
|
+
*/
|
|
7
|
+
const SIGNAL_NUMBERS = {
|
|
8
|
+
SIGHUP: 1,
|
|
9
|
+
SIGINT: 2,
|
|
10
|
+
SIGQUIT: 3,
|
|
11
|
+
SIGILL: 4,
|
|
12
|
+
SIGTRAP: 5,
|
|
13
|
+
SIGABRT: 6,
|
|
14
|
+
SIGBUS: 7,
|
|
15
|
+
SIGFPE: 8,
|
|
16
|
+
SIGKILL: 9,
|
|
17
|
+
SIGUSR1: 10,
|
|
18
|
+
SIGSEGV: 11,
|
|
19
|
+
SIGUSR2: 12,
|
|
20
|
+
SIGPIPE: 13,
|
|
21
|
+
SIGALRM: 14,
|
|
22
|
+
SIGTERM: 15,
|
|
23
|
+
SIGCHLD: 17,
|
|
24
|
+
SIGCONT: 18,
|
|
25
|
+
SIGSTOP: 19,
|
|
26
|
+
SIGTSTP: 20,
|
|
27
|
+
};
|
|
28
|
+
const SIGNAL_NAMES = new Map(Object.entries(SIGNAL_NUMBERS).map(([name, number]) => [number, name]));
|
|
29
|
+
/** 名字 → 编号;未知名字返回 0(与 waitpid 的"无信号"一致)。 */
|
|
30
|
+
export function signalNumberFromName(signal) {
|
|
31
|
+
return SIGNAL_NUMBERS[signal] ?? 0;
|
|
32
|
+
}
|
|
33
|
+
/** 编号 → 名字;0 / null 表示没有信号,未知编号回落 SIGTERM。 */
|
|
34
|
+
export function signalNameFromNumber(signal) {
|
|
35
|
+
if (signal === null || signal === 0)
|
|
36
|
+
return null;
|
|
37
|
+
return SIGNAL_NAMES.get(signal) ?? "SIGTERM";
|
|
38
|
+
}
|