@co0ontty/wand 4.53.0 → 4.54.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.json +3 -3
- package/dist/git-quick-commit.js +1 -1
- package/dist/process-manager.js +6 -3
- package/dist/server-session-routes.js +1 -1
- package/dist/server-workspace-routes.d.ts +1 -1
- package/dist/server-workspace-routes.js +91 -37
- package/dist/session-topic.d.ts +14 -2
- package/dist/session-topic.js +107 -2
- package/dist/storage.d.ts +7 -1
- package/dist/storage.js +57 -11
- package/dist/structured-grok-adapter.js +116 -0
- package/dist/structured-opencode-adapter.js +1 -1
- package/dist/structured-pi-adapter.js +84 -16
- package/dist/structured-qoder-adapter.js +4 -0
- package/dist/terminal-daemon-client.d.ts +1 -0
- package/dist/terminal-daemon-client.js +33 -9
- package/dist/types.d.ts +6 -0
- package/dist/types.js +2 -1
- package/dist/web-ui/content/scripts.js +178 -74
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.js +2 -2
- package/dist/workspace-binding.js +2 -1
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-09-
|
|
4
|
-
"version": "4.
|
|
2
|
+
"commit": "cb99d899728b5b5a2570b5bec4fce6882449ddb8",
|
|
3
|
+
"builtAt": "2026-09-07T01:21:35.988Z",
|
|
4
|
+
"version": "4.54.0",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -1208,7 +1208,7 @@ async function runQuickCommitFallbackCli(opts, priorError) {
|
|
|
1208
1208
|
});
|
|
1209
1209
|
}
|
|
1210
1210
|
else if (provider === "opencode") {
|
|
1211
|
-
const args = ["run", "--format", "json", "--
|
|
1211
|
+
const args = ["run", "--format", "json", "--auto"];
|
|
1212
1212
|
const model = opts.model?.trim();
|
|
1213
1213
|
if (model && model !== "default")
|
|
1214
1214
|
args.push("--model", model);
|
package/dist/process-manager.js
CHANGED
|
@@ -14,7 +14,7 @@ import { buildLanguageDirective, buildManagedAutonomyDirective } from "./languag
|
|
|
14
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
15
15
|
import { getProviderCommandSessionId, getProviderResumeCommandSessionId } from "./resume-policy.js";
|
|
16
16
|
import { normalizeThinkingEffort, thinkingEffortToClaudeCliEffort, thinkingEffortToClaudeSlashEffort, thinkingEffortToCodexReasoningEffort, thinkingEffortToOpenCodeVariant, thinkingEffortToPiLevel } from "./structured-provider-common.js";
|
|
17
|
-
import { provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle,
|
|
17
|
+
import { consumePtyInputForTopic, createPtyTopicLineBuffer, provisionalSessionTopic, SessionTopicCoordinator, sessionTopicBlocklistForSnapshot, shouldAcceptGeneratedSessionTitle, } from "./session-topic.js";
|
|
18
18
|
import { getErrorMessage } from "./error-utils.js";
|
|
19
19
|
import { resolveSystemAiContext } from "./session-ai-context.js";
|
|
20
20
|
import { resolveSessionCwd } from "./session-cwd.js";
|
|
@@ -1497,8 +1497,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
1497
1497
|
console.error(`[ProcessManager] Rejecting input: session ${id} has no PTY`);
|
|
1498
1498
|
throw new SessionInputError("Session is not running.", "SESSION_NO_PTY", id, record.status);
|
|
1499
1499
|
}
|
|
1500
|
-
if (
|
|
1501
|
-
|
|
1500
|
+
if (!record.ptyTopicDraft)
|
|
1501
|
+
record.ptyTopicDraft = createPtyTopicLineBuffer();
|
|
1502
|
+
const topicInput = consumePtyInputForTopic(record.ptyTopicDraft, input, view, shortcutKey);
|
|
1503
|
+
if (topicInput)
|
|
1504
|
+
this.maybeGenerateSessionTopic(id, topicInput);
|
|
1502
1505
|
// Log shortcut key interactions for auto-confirm and mode analysis
|
|
1503
1506
|
if (shortcutKey) {
|
|
1504
1507
|
const outputLines = record.output.split("\n");
|
|
@@ -508,7 +508,7 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
508
508
|
const body = req.body;
|
|
509
509
|
try {
|
|
510
510
|
if (body.provider && body.provider !== "claude" && body.provider !== "codex" && body.provider !== "opencode" && body.provider !== "grok" && body.provider !== "qoder" && body.provider !== "pi") {
|
|
511
|
-
res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok 或
|
|
511
|
+
res.status(400).json({ error: "结构化会话当前仅支持 Claude、Codex、OpenCode、Grok、Qoder 或 Pi provider。" });
|
|
512
512
|
return;
|
|
513
513
|
}
|
|
514
514
|
const provider = body.provider === "codex" || body.provider === "opencode" || body.provider === "grok" || body.provider === "qoder" || body.provider === "pi" ? body.provider : "claude";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Express } from "express";
|
|
2
2
|
import type { SessionRegistry } from "./session-registry.js";
|
|
3
3
|
import type { WandStorage } from "./storage.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type LayoutNode, type TaskWindowLayout } from "./types.js";
|
|
5
5
|
/** 校验并清洗前端提交的布局树;非法输入返回 null(空工作空间)。 */
|
|
6
6
|
export declare function sanitizeLayout(value: unknown): LayoutNode | null;
|
|
7
7
|
/** 校验任务级工作窗口集合;旧版单棵布局会兼容升级成一个 window。 */
|
|
@@ -7,6 +7,7 @@ import { expandHomePath } from "./middleware/path-safety.js";
|
|
|
7
7
|
import { checkSessionWorktreeMergeabilityAsync, cleanupWorktreeSync, prepareSessionWorktree, resolveWorktreeTargetBranchAsync, } from "./git-worktree.js";
|
|
8
8
|
import { collectSessionTopicBlocklist } from "./session-topic.js";
|
|
9
9
|
import { resolveSessionDisplayTitle } from "./session-transport.js";
|
|
10
|
+
import { GLOBAL_WORKSPACE_ID } from "./types.js";
|
|
10
11
|
import { attachUnboundSessionsToWorkspace, backfillSessionWorkspaces, } from "./workspace-binding.js";
|
|
11
12
|
const PROVIDERS = new Set(["claude", "codex", "opencode", "grok", "qoder", "pi"]);
|
|
12
13
|
function workspaceSessionTitle(session, names = {}) {
|
|
@@ -62,6 +63,52 @@ function deleteSessions(storage, sessions, sessionIds) {
|
|
|
62
63
|
storage.deleteSession(sessionId);
|
|
63
64
|
}
|
|
64
65
|
}
|
|
66
|
+
function isGlobalWorkspace(workspace) {
|
|
67
|
+
return workspace.kind === "global" || workspace.id === GLOBAL_WORKSPACE_ID;
|
|
68
|
+
}
|
|
69
|
+
function taskRuntimeCwd(task, workspace) {
|
|
70
|
+
return task.worktree?.path ?? task.cwd ?? workspace?.cwd ?? "";
|
|
71
|
+
}
|
|
72
|
+
function createTaskForWorkspace(storage, workspace, body) {
|
|
73
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
74
|
+
if (!name)
|
|
75
|
+
throw new Error("请输入任务名称。");
|
|
76
|
+
let mountedCwd;
|
|
77
|
+
if (body.cwd !== undefined && body.cwd !== null && String(body.cwd).trim()) {
|
|
78
|
+
mountedCwd = resolveWorkspaceCwd(body.cwd);
|
|
79
|
+
}
|
|
80
|
+
const baseRef = typeof body.baseRef === "string" && body.baseRef.trim() ? body.baseRef.trim() : undefined;
|
|
81
|
+
const runCwd = mountedCwd ?? workspace.cwd;
|
|
82
|
+
const wantWorktree = body.worktree === true || (body.worktree !== false && !isGlobalWorkspace(workspace));
|
|
83
|
+
let worktree = null;
|
|
84
|
+
let worktreeError;
|
|
85
|
+
if (wantWorktree) {
|
|
86
|
+
try {
|
|
87
|
+
const setup = prepareSessionWorktree({
|
|
88
|
+
cwd: runCwd,
|
|
89
|
+
sessionId: crypto.randomUUID(),
|
|
90
|
+
spec: { taskName: name, baseRef },
|
|
91
|
+
});
|
|
92
|
+
worktree = setup.worktree;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
worktreeError = getErrorMessage(error, "无法创建 worktree,将在项目目录直接运行。");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const storedCwd = mountedCwd && mountedCwd !== workspace.cwd ? mountedCwd : undefined;
|
|
99
|
+
const task = storage.createWorkspaceTask({
|
|
100
|
+
workspaceId: workspace.id,
|
|
101
|
+
name,
|
|
102
|
+
worktree,
|
|
103
|
+
cwd: storedCwd,
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
task,
|
|
107
|
+
cwd: taskRuntimeCwd(task, workspace),
|
|
108
|
+
isolated: worktree !== null,
|
|
109
|
+
worktreeError,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
65
112
|
function workspaceWithCounts(storage, workspace, sessionCounts) {
|
|
66
113
|
const worktreeCount = storage.listWorkspaceTasks(workspace.id)
|
|
67
114
|
.filter((task) => task.worktree !== null)
|
|
@@ -185,7 +232,9 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
185
232
|
app.get("/api/workspaces", (_req, res) => {
|
|
186
233
|
backfillSessionWorkspaces(storage);
|
|
187
234
|
const sessionCounts = storage.countSessionsByWorkspace();
|
|
188
|
-
res.json(storage.listWorkspaces()
|
|
235
|
+
res.json(storage.listWorkspaces()
|
|
236
|
+
.filter((workspace) => !isGlobalWorkspace(workspace))
|
|
237
|
+
.map((workspace) => workspaceWithCounts(storage, workspace, sessionCounts)));
|
|
189
238
|
});
|
|
190
239
|
// 新建项目:名称 + 目录 + 默认 IDE,不启动会话
|
|
191
240
|
app.post("/api/workspaces", asyncRoute(async (req, res) => {
|
|
@@ -264,6 +313,10 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
264
313
|
res.status(404).json({ error: "未找到该项目。" });
|
|
265
314
|
return;
|
|
266
315
|
}
|
|
316
|
+
if (isGlobalWorkspace(existing)) {
|
|
317
|
+
res.status(400).json({ error: "不能删除全局任务空间。" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
267
320
|
const tasks = storage.listWorkspaceTasks(existing.id);
|
|
268
321
|
if (cascade) {
|
|
269
322
|
deleteSessions(storage, sessions, [
|
|
@@ -298,6 +351,22 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
298
351
|
res.json({ ok: true, layout });
|
|
299
352
|
});
|
|
300
353
|
// ── 任务(Task = 命名 + 独立 worktree + 一组标签)──
|
|
354
|
+
// 不依赖项目的独立任务:挂到隐藏的全局空间,目录可省略(使用全局临时目录)。
|
|
355
|
+
app.post("/api/tasks", asyncRoute(async (req, res) => {
|
|
356
|
+
const workspace = storage.ensureGlobalWorkspace();
|
|
357
|
+
try {
|
|
358
|
+
const created = createTaskForWorkspace(storage, workspace, req.body);
|
|
359
|
+
res.status(201).json({
|
|
360
|
+
...created.task,
|
|
361
|
+
cwd: created.cwd,
|
|
362
|
+
isolated: created.isolated,
|
|
363
|
+
worktreeError: created.worktreeError,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
res.status(400).json({ error: getErrorMessage(error, "创建任务失败。") });
|
|
368
|
+
}
|
|
369
|
+
}));
|
|
301
370
|
// 目录组为一级容器的任务聚合列表,供侧栏「任务」视图一次拉全。
|
|
302
371
|
app.get("/api/tasks", (req, res) => {
|
|
303
372
|
// 查询参数:workspaceId 过滤单目录;limit 截断每目录任务数;
|
|
@@ -317,10 +386,12 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
317
386
|
: workspaces;
|
|
318
387
|
const groups = new Map();
|
|
319
388
|
for (const workspace of visibleWorkspaces) {
|
|
389
|
+
const global = isGlobalWorkspace(workspace);
|
|
320
390
|
groups.set(workspace.id, {
|
|
321
391
|
workspaceId: workspace.id,
|
|
322
|
-
workspaceName: workspace.name,
|
|
392
|
+
workspaceName: global ? "全局" : workspace.name,
|
|
323
393
|
workspaceCwd: workspace.cwd,
|
|
394
|
+
...(global ? { global: true } : {}),
|
|
324
395
|
tasks: storage.listWorkspaceTasks(workspace.id)
|
|
325
396
|
.slice(0, taskLimit ?? undefined)
|
|
326
397
|
.map((task) => {
|
|
@@ -328,12 +399,12 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
328
399
|
const sessions = allSessions
|
|
329
400
|
.slice(0, sessionLimit ?? undefined)
|
|
330
401
|
.map((session) => ({
|
|
331
|
-
...summarize(session, { taskName: task.name, workspaceName: workspace.name }),
|
|
402
|
+
...summarize(session, { taskName: task.name, workspaceName: global ? undefined : workspace.name }),
|
|
332
403
|
workspaceTaskId: task.id,
|
|
333
404
|
}));
|
|
334
405
|
return {
|
|
335
406
|
...task,
|
|
336
|
-
cwd: task
|
|
407
|
+
cwd: taskRuntimeCwd(task, workspace),
|
|
337
408
|
isolated: task.worktree !== null,
|
|
338
409
|
sessions,
|
|
339
410
|
totalSessions: allSessions.length,
|
|
@@ -356,7 +427,8 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
356
427
|
let group = direct && !direct.synthetic ? direct : undefined;
|
|
357
428
|
const resolved = session.cwd ? path.resolve(session.cwd) : "";
|
|
358
429
|
if (!group && resolved) {
|
|
359
|
-
|
|
430
|
+
const cwdMatches = [...groups.values()].filter((candidate) => !candidate.synthetic && candidate.workspaceCwd === resolved);
|
|
431
|
+
group = cwdMatches.find((candidate) => !candidate.global) ?? cwdMatches[0];
|
|
360
432
|
}
|
|
361
433
|
// 过滤模式下不创建合成组:不属于目标目录的会话直接排除。
|
|
362
434
|
if (!group && resolved && !workspaceFilter) {
|
|
@@ -382,7 +454,9 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
382
454
|
workspaceName: group.workspaceName,
|
|
383
455
|
}));
|
|
384
456
|
}
|
|
385
|
-
const payload = [...groups.values()]
|
|
457
|
+
const payload = [...groups.values()]
|
|
458
|
+
.filter((group) => !group.global || group.tasks.length > 0 || group.standaloneSessions.length > 0)
|
|
459
|
+
.sort((left, right) => Number(Boolean(right.global)) - Number(Boolean(left.global)));
|
|
386
460
|
const revision = tasksRevision(payload);
|
|
387
461
|
if (typeof req.query.revision === "string") {
|
|
388
462
|
if (req.query.revision === revision) {
|
|
@@ -487,38 +561,18 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
487
561
|
res.status(404).json({ error: "未找到该项目。" });
|
|
488
562
|
return;
|
|
489
563
|
}
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
564
|
+
try {
|
|
565
|
+
const created = createTaskForWorkspace(storage, workspace, req.body);
|
|
566
|
+
res.status(201).json({
|
|
567
|
+
...created.task,
|
|
568
|
+
cwd: created.cwd,
|
|
569
|
+
isolated: created.isolated,
|
|
570
|
+
worktreeError: created.worktreeError,
|
|
571
|
+
});
|
|
495
572
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
let worktree = null;
|
|
499
|
-
let worktreeError;
|
|
500
|
-
if (wantWorktree) {
|
|
501
|
-
try {
|
|
502
|
-
const setup = prepareSessionWorktree({
|
|
503
|
-
cwd: workspace.cwd,
|
|
504
|
-
// 用随机短 id 作分支后缀,避免同名任务撞分支。
|
|
505
|
-
sessionId: crypto.randomUUID(),
|
|
506
|
-
spec: { taskName: name, baseRef },
|
|
507
|
-
});
|
|
508
|
-
worktree = setup.worktree;
|
|
509
|
-
}
|
|
510
|
-
catch (error) {
|
|
511
|
-
// 非 git 仓库 / 基线不存在:任务照常创建,但无 worktree 隔离。
|
|
512
|
-
worktreeError = getErrorMessage(error, "无法创建 worktree,将在项目目录直接运行。");
|
|
513
|
-
}
|
|
573
|
+
catch (error) {
|
|
574
|
+
res.status(400).json({ error: getErrorMessage(error, "创建任务失败。") });
|
|
514
575
|
}
|
|
515
|
-
const task = storage.createWorkspaceTask({ workspaceId: workspace.id, name, worktree });
|
|
516
|
-
res.status(201).json({
|
|
517
|
-
...task,
|
|
518
|
-
cwd: worktree?.path ?? workspace.cwd,
|
|
519
|
-
isolated: worktree !== null,
|
|
520
|
-
worktreeError,
|
|
521
|
-
});
|
|
522
576
|
}));
|
|
523
577
|
// 任务详情:meta + 该任务下的会话;访问即更新 lastOpenedAt
|
|
524
578
|
app.get("/api/workspace-tasks/:taskId", (req, res) => {
|
|
@@ -531,7 +585,7 @@ export function registerWorkspaceRoutes(app, storage, sessions) {
|
|
|
531
585
|
const workspace = storage.getWorkspace(task.workspaceId);
|
|
532
586
|
res.json({
|
|
533
587
|
...task,
|
|
534
|
-
cwd: task
|
|
588
|
+
cwd: taskRuntimeCwd(task, workspace),
|
|
535
589
|
sessions: storage.listSessionsByWorkspaceTask(task.id).map((session) => ({
|
|
536
590
|
...workspaceSessionSummary(session, {
|
|
537
591
|
taskName: task.name,
|
package/dist/session-topic.d.ts
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { type QuickCommitAiOptions } from "./git-quick-commit.js";
|
|
2
2
|
import type { ConversationTurn } from "./types.js";
|
|
3
3
|
/**
|
|
4
|
-
* PTY
|
|
5
|
-
*
|
|
4
|
+
* PTY 底部输入框整段提交(shortcutKey=enter_text)会总结标题。
|
|
5
|
+
* 终端直通键入没有这个标记:服务端把可打印字符拼成一行,回车后再总结。
|
|
6
|
+
* 方向键 / 单独回车 / y/n / 控制序列不能触发,避免把按键当主题。
|
|
6
7
|
*/
|
|
7
8
|
export declare function shouldGenerateSessionTopicFromPtyInput(view?: "chat" | "terminal", shortcutKey?: string): boolean;
|
|
9
|
+
export interface PtyTopicLineBuffer {
|
|
10
|
+
text: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function createPtyTopicLineBuffer(): PtyTopicLineBuffer;
|
|
13
|
+
/** Raw TTY lines that are almost certainly not a user prompt. */
|
|
14
|
+
export declare function isPtyTypedTopicCandidate(prompt: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Decide whether this PTY write completes a user prompt worth summarizing.
|
|
17
|
+
* Mutates `buffer` so keystroke-by-keystroke terminal input can be assembled.
|
|
18
|
+
*/
|
|
19
|
+
export declare function consumePtyInputForTopic(buffer: PtyTopicLineBuffer, input: string, view?: "chat" | "terminal", shortcutKey?: string): string | null;
|
|
8
20
|
export interface SessionTopic {
|
|
9
21
|
title: string;
|
|
10
22
|
description: string;
|
package/dist/session-topic.js
CHANGED
|
@@ -1,12 +1,117 @@
|
|
|
1
1
|
import { callConfiguredAiText } from "./git-quick-commit.js";
|
|
2
|
+
import { skipAnsiSequence } from "./pty-text-utils.js";
|
|
2
3
|
const MAX_PROMPT_LENGTH = 12_000;
|
|
4
|
+
const PTY_TOPIC_DRAFT_MAX = 4_000;
|
|
3
5
|
/**
|
|
4
|
-
* PTY
|
|
5
|
-
*
|
|
6
|
+
* PTY 底部输入框整段提交(shortcutKey=enter_text)会总结标题。
|
|
7
|
+
* 终端直通键入没有这个标记:服务端把可打印字符拼成一行,回车后再总结。
|
|
8
|
+
* 方向键 / 单独回车 / y/n / 控制序列不能触发,避免把按键当主题。
|
|
6
9
|
*/
|
|
7
10
|
export function shouldGenerateSessionTopicFromPtyInput(view, shortcutKey) {
|
|
8
11
|
return view !== "terminal" || shortcutKey === "enter_text";
|
|
9
12
|
}
|
|
13
|
+
export function createPtyTopicLineBuffer() {
|
|
14
|
+
return { text: "" };
|
|
15
|
+
}
|
|
16
|
+
function stripTrailingNewlines(input) {
|
|
17
|
+
return input.replace(/[\r\n]+$/g, "").trim();
|
|
18
|
+
}
|
|
19
|
+
function popCodePoint(text) {
|
|
20
|
+
const chars = Array.from(text);
|
|
21
|
+
chars.pop();
|
|
22
|
+
return chars.join("");
|
|
23
|
+
}
|
|
24
|
+
/** Raw TTY lines that are almost certainly not a user prompt. */
|
|
25
|
+
export function isPtyTypedTopicCandidate(prompt) {
|
|
26
|
+
if (!prompt)
|
|
27
|
+
return false;
|
|
28
|
+
if (/^[yn]$/i.test(prompt))
|
|
29
|
+
return false;
|
|
30
|
+
if (/^\d+$/.test(prompt))
|
|
31
|
+
return false;
|
|
32
|
+
if (/^\/[a-z][\w-]*$/i.test(prompt))
|
|
33
|
+
return false;
|
|
34
|
+
if (/[\u3400-\u9fff]/.test(prompt))
|
|
35
|
+
return true;
|
|
36
|
+
if (/\s/.test(prompt) && prompt.length >= 2)
|
|
37
|
+
return true;
|
|
38
|
+
return prompt.length >= 8;
|
|
39
|
+
}
|
|
40
|
+
function flushPtyTopicDraft(buffer) {
|
|
41
|
+
const prompt = buffer.text.replace(/\s+/g, " ").trim();
|
|
42
|
+
buffer.text = "";
|
|
43
|
+
return isPtyTypedTopicCandidate(prompt) ? prompt : null;
|
|
44
|
+
}
|
|
45
|
+
function consumeRawPtyTopicLine(buffer, input) {
|
|
46
|
+
let submitted = null;
|
|
47
|
+
let i = 0;
|
|
48
|
+
while (i < input.length) {
|
|
49
|
+
const code = input.charCodeAt(i);
|
|
50
|
+
if (code === 0x1b) {
|
|
51
|
+
i = skipAnsiSequence(input, i);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (code === 0x0d) {
|
|
55
|
+
const flushed = flushPtyTopicDraft(buffer);
|
|
56
|
+
if (flushed && !submitted)
|
|
57
|
+
submitted = flushed;
|
|
58
|
+
i += input.charCodeAt(i + 1) === 0x0a ? 2 : 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (code === 0x08 || code === 0x7f) {
|
|
62
|
+
buffer.text = popCodePoint(buffer.text);
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (code === 0x03 || code === 0x15) {
|
|
67
|
+
buffer.text = "";
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (code === 0x0a) {
|
|
72
|
+
if (buffer.text)
|
|
73
|
+
buffer.text += "\n";
|
|
74
|
+
i += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (code < 32) {
|
|
78
|
+
i += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const point = input.codePointAt(i) ?? code;
|
|
82
|
+
buffer.text += String.fromCodePoint(point);
|
|
83
|
+
i += point > 0xffff ? 2 : 1;
|
|
84
|
+
if (buffer.text.length > PTY_TOPIC_DRAFT_MAX) {
|
|
85
|
+
buffer.text = buffer.text.slice(-PTY_TOPIC_DRAFT_MAX);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return submitted;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Decide whether this PTY write completes a user prompt worth summarizing.
|
|
92
|
+
* Mutates `buffer` so keystroke-by-keystroke terminal input can be assembled.
|
|
93
|
+
*/
|
|
94
|
+
export function consumePtyInputForTopic(buffer, input, view, shortcutKey) {
|
|
95
|
+
if (view !== "terminal") {
|
|
96
|
+
buffer.text = "";
|
|
97
|
+
const prompt = stripTrailingNewlines(input);
|
|
98
|
+
return prompt && shouldGenerateSessionTopicFromPtyInput(view, shortcutKey) ? prompt : null;
|
|
99
|
+
}
|
|
100
|
+
if (shortcutKey === "enter_text") {
|
|
101
|
+
buffer.text = "";
|
|
102
|
+
return stripTrailingNewlines(input) || null;
|
|
103
|
+
}
|
|
104
|
+
if (shortcutKey) {
|
|
105
|
+
if (shortcutKey === "ctrl_c"
|
|
106
|
+
|| shortcutKey === "ctrl_u"
|
|
107
|
+
|| shortcutKey === "ctrl_d"
|
|
108
|
+
|| shortcutKey === "escape") {
|
|
109
|
+
buffer.text = "";
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return consumeRawPtyTopicLine(buffer, input);
|
|
114
|
+
}
|
|
10
115
|
const TITLE_MAX_LENGTH = 24;
|
|
11
116
|
const DESCRIPTION_MAX_LENGTH = 120;
|
|
12
117
|
function cleanTopicText(value, maxLength) {
|
package/dist/storage.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionSnapshot, ConversationTurn, StructuredSessionState, Workspace, LayoutNode, TaskWindowLayout, WorkspaceDefaultProvider, WorkspaceTask, WorkspaceTaskWorktree, WorkspaceTaskStatus } from "./types.js";
|
|
1
|
+
import { SessionSnapshot, ConversationTurn, StructuredSessionState, Workspace, LayoutNode, TaskWindowLayout, WorkspaceDefaultProvider, WorkspaceKind, WorkspaceTask, WorkspaceTaskWorktree, WorkspaceTaskStatus } from "./types.js";
|
|
2
2
|
import type { AgentActivityItem, Mission, MissionAttempt, MissionReviewComment, MissionReviewStatus, MissionStatus } from "./mission-types.js";
|
|
3
3
|
import { type PasswordVault, type PasswordVaultItem, type PasswordVaultItemFilter, type PasswordVaultItemInput } from "./password-manager.js";
|
|
4
4
|
type AuthPrincipalKind = "browser-admin" | "connected-app";
|
|
@@ -16,7 +16,9 @@ export declare function resolveDatabasePath(configPath: string): string;
|
|
|
16
16
|
export declare function ensureDatabaseFile(dbPath: string): boolean;
|
|
17
17
|
export declare class WandStorage {
|
|
18
18
|
private readonly db;
|
|
19
|
+
private readonly dbPath;
|
|
19
20
|
constructor(dbPath: string);
|
|
21
|
+
directory(): string;
|
|
20
22
|
close(): void;
|
|
21
23
|
/**
|
|
22
24
|
* Run a synchronous group of storage operations atomically. Calls must not
|
|
@@ -42,10 +44,13 @@ export declare class WandStorage {
|
|
|
42
44
|
listWorkspaces(): Workspace[];
|
|
43
45
|
getWorkspace(id: string): Workspace | null;
|
|
44
46
|
createWorkspace(input: {
|
|
47
|
+
id?: string;
|
|
45
48
|
name: string;
|
|
46
49
|
cwd: string;
|
|
50
|
+
kind?: WorkspaceKind;
|
|
47
51
|
defaultProvider?: WorkspaceDefaultProvider;
|
|
48
52
|
}): Workspace;
|
|
53
|
+
ensureGlobalWorkspace(): Workspace;
|
|
49
54
|
updateWorkspace(id: string, patch: {
|
|
50
55
|
name?: string;
|
|
51
56
|
cwd?: string;
|
|
@@ -73,6 +78,7 @@ export declare class WandStorage {
|
|
|
73
78
|
workspaceId: string;
|
|
74
79
|
name: string;
|
|
75
80
|
worktree?: WorkspaceTaskWorktree | null;
|
|
81
|
+
cwd?: string | null;
|
|
76
82
|
status?: WorkspaceTaskStatus;
|
|
77
83
|
}): WorkspaceTask;
|
|
78
84
|
updateWorkspaceTask(id: string, patch: {
|
package/dist/storage.js
CHANGED
|
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { GLOBAL_WORKSPACE_ID } from "./types.js";
|
|
5
6
|
import { normalizeSessionDirectory } from "./session-directory-tree.js";
|
|
6
7
|
import { DEFAULT_PASSWORD_VAULT_ID, DEFAULT_PASSWORD_VAULT_NAME, decryptVaultSecret, encryptVaultSecret, itemMatchesFilter, normalizePasswordItemInput, normalizeVaultName, nowIso, } from "./password-manager.js";
|
|
7
8
|
function safeJsonParse(raw) {
|
|
@@ -257,11 +258,15 @@ function sessionSelectFields() {
|
|
|
257
258
|
return `id, session_source, automation_id, provider, session_kind, runner, command, cwd, mode, status, exit_code, started_at, ended_at, output, pty_output_seq, archived, archived_at, claude_session_id, messages, queued_messages, queued_message_skills, structured_state
|
|
258
259
|
, resumed_from_session_id, auto_recovered, worktree_enabled, worktree_info, worktree_merge_status, worktree_merge_info, title, description, session_options, workspace_id, workspace_task_id`;
|
|
259
260
|
}
|
|
261
|
+
function mapWorkspaceKind(raw) {
|
|
262
|
+
return raw === "global" ? "global" : "project";
|
|
263
|
+
}
|
|
260
264
|
function mapWorkspaceRow(row) {
|
|
261
265
|
return {
|
|
262
266
|
id: row.id,
|
|
263
267
|
name: row.name,
|
|
264
268
|
cwd: row.cwd,
|
|
269
|
+
kind: mapWorkspaceKind(row.kind),
|
|
265
270
|
defaultProvider: (row.default_provider ?? undefined),
|
|
266
271
|
layout: row.layout_json ? safeJsonParse(row.layout_json) ?? null : null,
|
|
267
272
|
createdAt: row.created_at,
|
|
@@ -299,11 +304,13 @@ function mapWorkspaceTaskLayout(raw) {
|
|
|
299
304
|
return null;
|
|
300
305
|
}
|
|
301
306
|
function mapWorkspaceTaskRow(row) {
|
|
307
|
+
const cwd = typeof row.cwd === "string" && row.cwd.trim() ? row.cwd : undefined;
|
|
302
308
|
return {
|
|
303
309
|
id: row.id,
|
|
304
310
|
workspaceId: row.workspace_id,
|
|
305
311
|
name: row.name,
|
|
306
312
|
worktree: mapWorkspaceTaskWorktree(row.worktree_json),
|
|
313
|
+
...(cwd ? { cwd } : {}),
|
|
307
314
|
layout: mapWorkspaceTaskLayout(row.layout_json),
|
|
308
315
|
status: (row.status === "done" ? "done" : "active"),
|
|
309
316
|
createdAt: row.created_at,
|
|
@@ -631,6 +638,7 @@ const INIT_SQL = `
|
|
|
631
638
|
id TEXT PRIMARY KEY,
|
|
632
639
|
name TEXT NOT NULL,
|
|
633
640
|
cwd TEXT NOT NULL,
|
|
641
|
+
kind TEXT NOT NULL DEFAULT 'project',
|
|
634
642
|
default_provider TEXT,
|
|
635
643
|
layout_json TEXT,
|
|
636
644
|
created_at TEXT NOT NULL,
|
|
@@ -647,6 +655,7 @@ const INIT_SQL = `
|
|
|
647
655
|
worktree_json TEXT,
|
|
648
656
|
layout_json TEXT,
|
|
649
657
|
status TEXT NOT NULL DEFAULT 'active',
|
|
658
|
+
cwd TEXT,
|
|
650
659
|
created_at TEXT NOT NULL,
|
|
651
660
|
last_opened_at TEXT,
|
|
652
661
|
FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE
|
|
@@ -655,6 +664,18 @@ const INIT_SQL = `
|
|
|
655
664
|
CREATE INDEX IF NOT EXISTS idx_workspace_tasks_workspace ON workspace_tasks(workspace_id);
|
|
656
665
|
CREATE INDEX IF NOT EXISTS idx_workspace_tasks_last_opened ON workspace_tasks(last_opened_at);
|
|
657
666
|
`;
|
|
667
|
+
function ensureWorkspaceSchema(db) {
|
|
668
|
+
const workspaceColumns = db.prepare("PRAGMA table_info(workspaces)").all();
|
|
669
|
+
const workspaceNames = new Set(workspaceColumns.map((column) => column.name));
|
|
670
|
+
if (workspaceColumns.length > 0 && !workspaceNames.has("kind")) {
|
|
671
|
+
db.exec("ALTER TABLE workspaces ADD COLUMN kind TEXT NOT NULL DEFAULT 'project'");
|
|
672
|
+
}
|
|
673
|
+
const taskColumns = db.prepare("PRAGMA table_info(workspace_tasks)").all();
|
|
674
|
+
const taskNames = new Set(taskColumns.map((column) => column.name));
|
|
675
|
+
if (taskColumns.length > 0 && !taskNames.has("cwd")) {
|
|
676
|
+
db.exec("ALTER TABLE workspace_tasks ADD COLUMN cwd TEXT");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
658
679
|
export function ensureDatabaseFile(dbPath) {
|
|
659
680
|
const dir = path.dirname(dbPath);
|
|
660
681
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -664,6 +685,7 @@ export function ensureDatabaseFile(dbPath) {
|
|
|
664
685
|
db.exec(INIT_SQL);
|
|
665
686
|
ensureAuthSessionSchema(db);
|
|
666
687
|
ensureCommandSessionSchema(db);
|
|
688
|
+
ensureWorkspaceSchema(db);
|
|
667
689
|
{
|
|
668
690
|
const missionColumns = db.prepare("PRAGMA table_info(missions)").all();
|
|
669
691
|
if (missionColumns.length > 0 && !missionColumns.some((column) => column.name === "task_id")) {
|
|
@@ -676,17 +698,23 @@ export function ensureDatabaseFile(dbPath) {
|
|
|
676
698
|
}
|
|
677
699
|
export class WandStorage {
|
|
678
700
|
db;
|
|
701
|
+
dbPath;
|
|
679
702
|
constructor(dbPath) {
|
|
680
703
|
const dir = path.dirname(dbPath);
|
|
681
704
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
682
705
|
chmodSync(dir, 0o700);
|
|
706
|
+
this.dbPath = dbPath;
|
|
683
707
|
this.db = new DatabaseSync(dbPath);
|
|
684
708
|
chmodSync(dbPath, 0o600);
|
|
685
709
|
this.db.exec(INIT_SQL);
|
|
686
710
|
ensureAuthSessionSchema(this.db);
|
|
687
711
|
ensureCommandSessionSchema(this.db);
|
|
712
|
+
ensureWorkspaceSchema(this.db);
|
|
688
713
|
this.ensureDefaultPasswordVault();
|
|
689
714
|
}
|
|
715
|
+
directory() {
|
|
716
|
+
return path.dirname(this.dbPath);
|
|
717
|
+
}
|
|
690
718
|
close() {
|
|
691
719
|
this.db.close();
|
|
692
720
|
}
|
|
@@ -786,33 +814,49 @@ export class WandStorage {
|
|
|
786
814
|
// ============ Workspaces(多标签 / 分屏项目)============
|
|
787
815
|
listWorkspaces() {
|
|
788
816
|
const rows = this.db
|
|
789
|
-
.prepare("SELECT id, name, cwd, default_provider, layout_json, created_at, last_opened_at FROM workspaces ORDER BY COALESCE(last_opened_at, created_at) DESC")
|
|
817
|
+
.prepare("SELECT id, name, cwd, kind, default_provider, layout_json, created_at, last_opened_at FROM workspaces ORDER BY COALESCE(last_opened_at, created_at) DESC")
|
|
790
818
|
.all();
|
|
791
819
|
return rows.map(mapWorkspaceRow);
|
|
792
820
|
}
|
|
793
821
|
getWorkspace(id) {
|
|
794
822
|
const row = this.db
|
|
795
|
-
.prepare("SELECT id, name, cwd, default_provider, layout_json, created_at, last_opened_at FROM workspaces WHERE id = ?")
|
|
823
|
+
.prepare("SELECT id, name, cwd, kind, default_provider, layout_json, created_at, last_opened_at FROM workspaces WHERE id = ?")
|
|
796
824
|
.get(id);
|
|
797
825
|
return row ? mapWorkspaceRow(row) : null;
|
|
798
826
|
}
|
|
799
827
|
createWorkspace(input) {
|
|
800
|
-
const id = crypto.randomUUID();
|
|
828
|
+
const id = input.id?.trim() || crypto.randomUUID();
|
|
829
|
+
const kind = input.kind === "global" ? "global" : "project";
|
|
801
830
|
const createdAt = nowIso();
|
|
802
831
|
this.db
|
|
803
|
-
.prepare(`INSERT INTO workspaces (id, name, cwd, default_provider, layout_json, created_at, last_opened_at)
|
|
804
|
-
VALUES (?, ?, ?, ?, NULL, ?, NULL)`)
|
|
805
|
-
.run(id, input.name, input.cwd, input.defaultProvider ?? null, createdAt);
|
|
832
|
+
.prepare(`INSERT INTO workspaces (id, name, cwd, kind, default_provider, layout_json, created_at, last_opened_at)
|
|
833
|
+
VALUES (?, ?, ?, ?, ?, NULL, ?, NULL)`)
|
|
834
|
+
.run(id, input.name, input.cwd, kind, input.defaultProvider ?? null, createdAt);
|
|
806
835
|
return {
|
|
807
836
|
id,
|
|
808
837
|
name: input.name,
|
|
809
838
|
cwd: input.cwd,
|
|
839
|
+
kind,
|
|
810
840
|
defaultProvider: input.defaultProvider,
|
|
811
841
|
layout: null,
|
|
812
842
|
createdAt,
|
|
813
843
|
lastOpenedAt: null,
|
|
814
844
|
};
|
|
815
845
|
}
|
|
846
|
+
ensureGlobalWorkspace() {
|
|
847
|
+
const existing = this.getWorkspace(GLOBAL_WORKSPACE_ID)
|
|
848
|
+
?? this.listWorkspaces().find((workspace) => workspace.kind === "global");
|
|
849
|
+
if (existing)
|
|
850
|
+
return existing;
|
|
851
|
+
const scratch = path.join(this.directory(), "scratch");
|
|
852
|
+
mkdirSync(scratch, { recursive: true, mode: 0o700 });
|
|
853
|
+
return this.createWorkspace({
|
|
854
|
+
id: GLOBAL_WORKSPACE_ID,
|
|
855
|
+
name: "全局任务",
|
|
856
|
+
cwd: scratch,
|
|
857
|
+
kind: "global",
|
|
858
|
+
});
|
|
859
|
+
}
|
|
816
860
|
updateWorkspace(id, patch) {
|
|
817
861
|
const assignments = [];
|
|
818
862
|
const values = [];
|
|
@@ -894,7 +938,7 @@ export class WandStorage {
|
|
|
894
938
|
// ── Workspace tasks(任务 = 命名 + 独立 worktree + 一组标签)──
|
|
895
939
|
listWorkspaceTasks(workspaceId) {
|
|
896
940
|
const rows = this.db
|
|
897
|
-
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, created_at, last_opened_at
|
|
941
|
+
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, cwd, created_at, last_opened_at
|
|
898
942
|
FROM workspace_tasks WHERE workspace_id = ?
|
|
899
943
|
ORDER BY COALESCE(last_opened_at, created_at) DESC`)
|
|
900
944
|
.all(workspaceId);
|
|
@@ -902,7 +946,7 @@ export class WandStorage {
|
|
|
902
946
|
}
|
|
903
947
|
getWorkspaceTask(id) {
|
|
904
948
|
const row = this.db
|
|
905
|
-
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, created_at, last_opened_at
|
|
949
|
+
.prepare(`SELECT id, workspace_id, name, worktree_json, layout_json, status, cwd, created_at, last_opened_at
|
|
906
950
|
FROM workspace_tasks WHERE id = ?`)
|
|
907
951
|
.get(id);
|
|
908
952
|
return row ? mapWorkspaceTaskRow(row) : null;
|
|
@@ -911,15 +955,17 @@ export class WandStorage {
|
|
|
911
955
|
const id = crypto.randomUUID();
|
|
912
956
|
const createdAt = nowIso();
|
|
913
957
|
const status = input.status ?? "active";
|
|
958
|
+
const cwd = typeof input.cwd === "string" && input.cwd.trim() ? input.cwd.trim() : null;
|
|
914
959
|
this.db
|
|
915
|
-
.prepare(`INSERT INTO workspace_tasks (id, workspace_id, name, worktree_json, layout_json, status, created_at, last_opened_at)
|
|
916
|
-
VALUES (?, ?, ?, ?, NULL, ?, ?, NULL)`)
|
|
917
|
-
.run(id, input.workspaceId, input.name, input.worktree ? JSON.stringify(input.worktree) : null, status, createdAt);
|
|
960
|
+
.prepare(`INSERT INTO workspace_tasks (id, workspace_id, name, worktree_json, layout_json, status, cwd, created_at, last_opened_at)
|
|
961
|
+
VALUES (?, ?, ?, ?, NULL, ?, ?, ?, NULL)`)
|
|
962
|
+
.run(id, input.workspaceId, input.name, input.worktree ? JSON.stringify(input.worktree) : null, status, cwd, createdAt);
|
|
918
963
|
return {
|
|
919
964
|
id,
|
|
920
965
|
workspaceId: input.workspaceId,
|
|
921
966
|
name: input.name,
|
|
922
967
|
worktree: input.worktree ?? null,
|
|
968
|
+
...(cwd ? { cwd } : {}),
|
|
923
969
|
layout: null,
|
|
924
970
|
status,
|
|
925
971
|
createdAt,
|