@co0ontty/wand 1.10.0 → 1.14.2
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/README.md +48 -12
- package/dist/config.d.ts +2 -1
- package/dist/config.js +51 -0
- package/dist/message-truncator.d.ts +16 -0
- package/dist/message-truncator.js +76 -0
- package/dist/process-manager.d.ts +4 -0
- package/dist/process-manager.js +74 -21
- package/dist/server-session-routes.js +29 -1
- package/dist/server.js +276 -11
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +10 -0
- package/dist/types.d.ts +28 -0
- package/dist/web-ui/content/scripts.js +782 -67
- package/dist/web-ui/content/styles.css +160 -27
- package/dist/ws-broadcast.d.ts +3 -2
- package/dist/ws-broadcast.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# wand
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@co0ontty/wand)
|
|
4
|
+
[](https://github.com/co0ontty/wand/blob/master/LICENSE)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](https://github.com/co0ontty/wand/commits/master)
|
|
7
|
+
|
|
8
|
+
通过浏览器远程访问和管理本地 CLI 工具的 Web 控制台。专为 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) 和 [Codex](https://github.com/openai/codex) 设计,支持终端和结构化对话双视图、会话持久化与恢复、权限管控、文件浏览、Android 客户端等功能。
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<img src="docs/screenshots/chat-view.png" width="800" alt="结构化对话视图" />
|
|
12
|
+
</p>
|
|
4
13
|
|
|
5
14
|
## 安装
|
|
6
15
|
|
|
@@ -22,6 +31,43 @@ wand web
|
|
|
22
31
|
|
|
23
32
|
安装完成后打开浏览器访问终端中提示的地址即可。
|
|
24
33
|
|
|
34
|
+
## 功能
|
|
35
|
+
|
|
36
|
+
<p align="center">
|
|
37
|
+
<img src="docs/screenshots/home.png" width="800" alt="欢迎页" />
|
|
38
|
+
</p>
|
|
39
|
+
|
|
40
|
+
### 核心
|
|
41
|
+
|
|
42
|
+
- **双视图模式** — 终端原始输出和结构化对话视图可随时切换,同一会话两种呈现
|
|
43
|
+
- **多 Provider 支持** — 同时支持 Claude Code 和 Codex,可按需创建不同类型的会话
|
|
44
|
+
- **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复;会话列表显示摘要,懒加载
|
|
45
|
+
- **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略;工具调用自动分组与审批统计
|
|
46
|
+
|
|
47
|
+
### 交互体验
|
|
48
|
+
|
|
49
|
+
- **结构化对话** — 代码块语法高亮、工具调用折叠/展开、多问题分组渲染、Token 用量按轮累计
|
|
50
|
+
- **个性化角色** — 像素风猫咪头像(赛博虎妞 / 勤劳初二),支持自定义对话角色名称
|
|
51
|
+
- **消息排队** — 在 AI 思考时可继续输入,消息自动排队发送
|
|
52
|
+
- **文件浏览器** — 内置路径浏览和搜索功能
|
|
53
|
+
|
|
54
|
+
### 部署与访问
|
|
55
|
+
|
|
56
|
+
- **PWA 支持** — 可添加到主屏幕作为独立应用使用
|
|
57
|
+
- **Android 客户端** — WebView 壳应用,支持加密连接码分发、APK 自动更新检查、原生通知推送、启动器图标切换
|
|
58
|
+
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
59
|
+
- **版本管理** — 内置更新检查与升级提示
|
|
60
|
+
|
|
61
|
+
## 截图
|
|
62
|
+
|
|
63
|
+
| 登录 | 对话视图 |
|
|
64
|
+
|:---:|:---:|
|
|
65
|
+
| <img src="docs/screenshots/login.png" width="400" alt="登录页" /> | <img src="docs/screenshots/chat-view.png" width="400" alt="对话视图" /> |
|
|
66
|
+
|
|
67
|
+
| 终端 PTY 视图 |
|
|
68
|
+
|:---:|
|
|
69
|
+
| <img src="docs/screenshots/terminal-pty.png" width="800" alt="终端 PTY 视图" /> |
|
|
70
|
+
|
|
25
71
|
## 配置
|
|
26
72
|
|
|
27
73
|
配置文件位于 `~/.wand/config.json`,首次 `wand init` 时自动生成。
|
|
@@ -43,17 +89,6 @@ wand config:set port 9443
|
|
|
43
89
|
| `password` | (随机生成) | 登录密码 |
|
|
44
90
|
| `language` | `""` | Claude 回复语言偏好 |
|
|
45
91
|
|
|
46
|
-
## 功能
|
|
47
|
-
|
|
48
|
-
- **双视图模式** — 终端原始输出和结构化对话视图可随时切换
|
|
49
|
-
- **会话管理** — 创建、归档、恢复会话;支持从 Claude 原生历史记录恢复;会话列表显示摘要
|
|
50
|
-
- **权限控制** — 可视化权限提示,支持逐次确认、单次批准、本轮记忆等策略;工具调用自动分组
|
|
51
|
-
- **文件浏览器** — 内置路径浏览和搜索功能
|
|
52
|
-
- **多种运行模式** — full-access / default / auto-edit 等 Claude 运行模式
|
|
53
|
-
- **个性化** — 像素风猫咪头像、回复语言偏好设置
|
|
54
|
-
- **PWA 支持** — 可添加到主屏幕作为独立应用使用
|
|
55
|
-
- **HTTPS** — 可选自签证书,适合远程或移动端访问
|
|
56
|
-
|
|
57
92
|
## 开发
|
|
58
93
|
|
|
59
94
|
```bash
|
|
@@ -84,6 +119,7 @@ src/
|
|
|
84
119
|
session-logger.ts # 文件日志 ~/.wand/sessions/
|
|
85
120
|
resume-policy.ts # Claude 历史绑定与恢复策略
|
|
86
121
|
web-ui/ # 服务端渲染的前端 HTML/CSS/JS
|
|
122
|
+
android/ # Android WebView 壳应用
|
|
87
123
|
```
|
|
88
124
|
|
|
89
125
|
数据存储在 `~/.wand/` 下:`config.json`(配置)、`wand.db`(SQLite)、`sessions/`(日志)。
|
package/dist/config.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ExecutionMode, WandConfig } from "./types.js";
|
|
1
|
+
import { CardExpandDefaults, ExecutionMode, WandConfig } from "./types.js";
|
|
2
2
|
export declare const defaultConfig: () => WandConfig;
|
|
3
3
|
export declare function resolveConfigPath(inputPath?: string): string;
|
|
4
4
|
export declare function resolveConfigDir(configPath: string): string;
|
|
5
5
|
export declare function hasConfigFile(configPath: string): boolean;
|
|
6
6
|
export declare function ensureConfig(configPath: string): Promise<WandConfig>;
|
|
7
7
|
export declare function saveConfig(configPath: string, config: WandConfig): Promise<void>;
|
|
8
|
+
export declare function normalizeCardDefaults(input: unknown): CardExpandDefaults;
|
|
8
9
|
export declare function isExecutionMode(value: unknown): value is ExecutionMode;
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
@@ -16,6 +17,8 @@ export const defaultConfig = () => ({
|
|
|
16
17
|
allowedCommandPrefixes: [],
|
|
17
18
|
shortcutLogMaxBytes: 10 * 1024 * 1024,
|
|
18
19
|
language: "",
|
|
20
|
+
android: defaultAndroidApkConfig(),
|
|
21
|
+
cardDefaults: defaultCardExpandDefaults(),
|
|
19
22
|
commandPresets: [
|
|
20
23
|
{
|
|
21
24
|
label: "Claude",
|
|
@@ -79,6 +82,49 @@ export async function saveConfig(configPath, config) {
|
|
|
79
82
|
await mkdir(path.dirname(configPath), { recursive: true });
|
|
80
83
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
81
84
|
}
|
|
85
|
+
function defaultCardExpandDefaults() {
|
|
86
|
+
return {
|
|
87
|
+
editCards: false,
|
|
88
|
+
inlineTools: false,
|
|
89
|
+
terminal: false,
|
|
90
|
+
thinking: false,
|
|
91
|
+
toolGroup: false,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export function normalizeCardDefaults(input) {
|
|
95
|
+
if (!input || typeof input !== "object")
|
|
96
|
+
return defaultCardExpandDefaults();
|
|
97
|
+
const raw = input;
|
|
98
|
+
return {
|
|
99
|
+
editCards: typeof raw.editCards === "boolean" ? raw.editCards : false,
|
|
100
|
+
inlineTools: typeof raw.inlineTools === "boolean" ? raw.inlineTools : false,
|
|
101
|
+
terminal: typeof raw.terminal === "boolean" ? raw.terminal : false,
|
|
102
|
+
thinking: typeof raw.thinking === "boolean" ? raw.thinking : false,
|
|
103
|
+
toolGroup: typeof raw.toolGroup === "boolean" ? raw.toolGroup : false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function defaultAndroidApkConfig() {
|
|
107
|
+
return {
|
|
108
|
+
enabled: false,
|
|
109
|
+
apkDir: "android",
|
|
110
|
+
currentApkFile: "",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function normalizeAndroidApkConfig(input) {
|
|
114
|
+
if (!input || typeof input !== "object")
|
|
115
|
+
return undefined;
|
|
116
|
+
const defaults = defaultAndroidApkConfig();
|
|
117
|
+
const androidInput = input;
|
|
118
|
+
return {
|
|
119
|
+
enabled: typeof androidInput.enabled === "boolean" ? androidInput.enabled : defaults.enabled,
|
|
120
|
+
apkDir: typeof androidInput.apkDir === "string" && androidInput.apkDir.trim()
|
|
121
|
+
? androidInput.apkDir.trim()
|
|
122
|
+
: defaults.apkDir,
|
|
123
|
+
currentApkFile: typeof androidInput.currentApkFile === "string"
|
|
124
|
+
? androidInput.currentApkFile.trim()
|
|
125
|
+
: defaults.currentApkFile,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
82
128
|
function normalizeStructuredChatPersona(input) {
|
|
83
129
|
if (!input || typeof input !== "object")
|
|
84
130
|
return undefined;
|
|
@@ -132,6 +178,11 @@ function mergeWithDefaults(input) {
|
|
|
132
178
|
: defaults.commandPresets,
|
|
133
179
|
structuredChatPersona: normalizeStructuredChatPersona(input.structuredChatPersona),
|
|
134
180
|
language: typeof input.language === "string" ? input.language.trim() : defaults.language,
|
|
181
|
+
appSecret: typeof input.appSecret === "string" && input.appSecret.length >= 32
|
|
182
|
+
? input.appSecret
|
|
183
|
+
: crypto.randomBytes(32).toString("hex"),
|
|
184
|
+
android: normalizeAndroidApkConfig(input.android) ?? defaults.android,
|
|
185
|
+
cardDefaults: normalizeCardDefaults(input.cardDefaults),
|
|
135
186
|
};
|
|
136
187
|
}
|
|
137
188
|
export function isExecutionMode(value) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncates tool_result content in messages for WebSocket transport.
|
|
3
|
+
* Cards that are collapsed by default have their large results replaced
|
|
4
|
+
* with a summary, and clients fetch full content on-demand via API.
|
|
5
|
+
*/
|
|
6
|
+
import type { CardExpandDefaults, ConversationTurn } from "./types.js";
|
|
7
|
+
/**
|
|
8
|
+
* Truncate messages for WebSocket transport. Tool results for collapsed card
|
|
9
|
+
* types are replaced with a short summary when they exceed the threshold.
|
|
10
|
+
*
|
|
11
|
+
* @param messages - Original messages array (not mutated)
|
|
12
|
+
* @param cardDefaults - Current card expand defaults from config
|
|
13
|
+
* @param streamingTurnIndex - Index of the currently streaming turn (-1 if none).
|
|
14
|
+
* Tool results in the streaming turn are never truncated.
|
|
15
|
+
*/
|
|
16
|
+
export declare function truncateMessagesForTransport(messages: ConversationTurn[], cardDefaults: CardExpandDefaults, streamingTurnIndex?: number): ConversationTurn[];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncates tool_result content in messages for WebSocket transport.
|
|
3
|
+
* Cards that are collapsed by default have their large results replaced
|
|
4
|
+
* with a summary, and clients fetch full content on-demand via API.
|
|
5
|
+
*/
|
|
6
|
+
const TRUNCATION_THRESHOLD = 200;
|
|
7
|
+
const SUMMARY_LENGTH = 100;
|
|
8
|
+
/** Tool name → cardDefaults field mapping */
|
|
9
|
+
function isToolDefaultCollapsed(toolName, defaults) {
|
|
10
|
+
switch (toolName) {
|
|
11
|
+
case "Read":
|
|
12
|
+
case "Glob":
|
|
13
|
+
case "Grep":
|
|
14
|
+
case "WebFetch":
|
|
15
|
+
case "WebSearch":
|
|
16
|
+
case "TodoRead":
|
|
17
|
+
return defaults.inlineTools !== true;
|
|
18
|
+
case "Bash":
|
|
19
|
+
return defaults.terminal !== true;
|
|
20
|
+
case "Edit":
|
|
21
|
+
case "Write":
|
|
22
|
+
case "MultiEdit":
|
|
23
|
+
return defaults.editCards !== true;
|
|
24
|
+
default:
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function getContentString(content) {
|
|
29
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Truncate messages for WebSocket transport. Tool results for collapsed card
|
|
33
|
+
* types are replaced with a short summary when they exceed the threshold.
|
|
34
|
+
*
|
|
35
|
+
* @param messages - Original messages array (not mutated)
|
|
36
|
+
* @param cardDefaults - Current card expand defaults from config
|
|
37
|
+
* @param streamingTurnIndex - Index of the currently streaming turn (-1 if none).
|
|
38
|
+
* Tool results in the streaming turn are never truncated.
|
|
39
|
+
*/
|
|
40
|
+
export function truncateMessagesForTransport(messages, cardDefaults, streamingTurnIndex = -1) {
|
|
41
|
+
return messages.map((turn, turnIndex) => {
|
|
42
|
+
// Never truncate the currently streaming turn
|
|
43
|
+
if (turnIndex === streamingTurnIndex)
|
|
44
|
+
return turn;
|
|
45
|
+
// Build tool_use_id → tool_name map from this turn's blocks
|
|
46
|
+
const toolNameMap = new Map();
|
|
47
|
+
for (const block of turn.content) {
|
|
48
|
+
if (block.type === "tool_use") {
|
|
49
|
+
toolNameMap.set(block.id, block.name);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let changed = false;
|
|
53
|
+
const truncatedContent = turn.content.map((block) => {
|
|
54
|
+
if (block.type !== "tool_result")
|
|
55
|
+
return block;
|
|
56
|
+
const result = block;
|
|
57
|
+
// Never truncate errors
|
|
58
|
+
if (result.is_error)
|
|
59
|
+
return block;
|
|
60
|
+
const toolName = toolNameMap.get(result.tool_use_id) || "";
|
|
61
|
+
if (!isToolDefaultCollapsed(toolName, cardDefaults))
|
|
62
|
+
return block;
|
|
63
|
+
const contentStr = getContentString(result.content);
|
|
64
|
+
if (contentStr.length <= TRUNCATION_THRESHOLD)
|
|
65
|
+
return block;
|
|
66
|
+
changed = true;
|
|
67
|
+
return {
|
|
68
|
+
...result,
|
|
69
|
+
content: contentStr.slice(0, SUMMARY_LENGTH) + "…",
|
|
70
|
+
_truncated: true,
|
|
71
|
+
_originalSize: contentStr.length,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
return changed ? { ...turn, content: truncatedContent } : turn;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -45,6 +45,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
45
45
|
provider?: SessionProvider;
|
|
46
46
|
}): SessionSnapshot;
|
|
47
47
|
list(): SessionSnapshot[];
|
|
48
|
+
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
49
|
+
listSlim(): SessionSnapshot[];
|
|
48
50
|
hasClaudeSessionFile(cwd: string, claudeSessionId: string): boolean;
|
|
49
51
|
private claudeHistoryCache;
|
|
50
52
|
private static readonly HISTORY_CACHE_TTL_MS;
|
|
@@ -64,6 +66,8 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
64
66
|
private deleteClaudeCache;
|
|
65
67
|
runStartupCommands(): SessionSnapshot[];
|
|
66
68
|
private snapshot;
|
|
69
|
+
/** Lightweight snapshot for list views — omits output and messages. */
|
|
70
|
+
private snapshotSlim;
|
|
67
71
|
private isPermissionBlocked;
|
|
68
72
|
private defaultAutonomyPolicy;
|
|
69
73
|
resolveEscalation(id: string, requestId: string, resolution?: "approve_once" | "approve_turn" | "deny"): SessionSnapshot;
|
package/dist/process-manager.js
CHANGED
|
@@ -8,6 +8,7 @@ import pty from "node-pty";
|
|
|
8
8
|
import { SessionLogger } from "./session-logger.js";
|
|
9
9
|
import { SessionLifecycleManager } from "./session-lifecycle.js";
|
|
10
10
|
import { ClaudePtyBridge } from "./claude-pty-bridge.js";
|
|
11
|
+
import { truncateMessagesForTransport } from "./message-truncator.js";
|
|
11
12
|
import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, normalizePromptText } from "./pty-text-utils.js";
|
|
12
13
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
13
14
|
import { getResumeCommandSessionId, hasRealConversationMessages, } from "./resume-policy.js";
|
|
@@ -863,6 +864,13 @@ export class ProcessManager extends EventEmitter {
|
|
|
863
864
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
|
|
864
865
|
.map((session) => this.snapshot(session));
|
|
865
866
|
}
|
|
867
|
+
/** Return lightweight snapshots for the session list (no output/messages). */
|
|
868
|
+
listSlim() {
|
|
869
|
+
this.archiveExpiredSessions();
|
|
870
|
+
return Array.from(this.sessions.values())
|
|
871
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt))
|
|
872
|
+
.map((session) => this.snapshotSlim(session));
|
|
873
|
+
}
|
|
866
874
|
hasClaudeSessionFile(cwd, claudeSessionId) {
|
|
867
875
|
return isClaudeSessionFileAvailable(cwd, claudeSessionId);
|
|
868
876
|
}
|
|
@@ -1171,6 +1179,43 @@ export class ProcessManager extends EventEmitter {
|
|
|
1171
1179
|
autoApprovePermissions: record.autoApprovePermissions || undefined,
|
|
1172
1180
|
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined,
|
|
1173
1181
|
summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
|
|
1182
|
+
currentTaskTitle: record.status === "running" ? record.currentTask?.title ?? undefined : undefined,
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
/** Lightweight snapshot for list views — omits output and messages. */
|
|
1186
|
+
snapshotSlim(record) {
|
|
1187
|
+
const messages = record.ptyBridge?.getMessages() ?? record.messages;
|
|
1188
|
+
return {
|
|
1189
|
+
id: record.id,
|
|
1190
|
+
sessionKind: "pty",
|
|
1191
|
+
provider: record.provider,
|
|
1192
|
+
runner: "pty",
|
|
1193
|
+
command: record.command,
|
|
1194
|
+
cwd: record.cwd,
|
|
1195
|
+
mode: record.mode,
|
|
1196
|
+
worktreeEnabled: record.worktreeEnabled ?? false,
|
|
1197
|
+
worktree: record.worktree ?? null,
|
|
1198
|
+
autonomyPolicy: record.autonomyPolicy,
|
|
1199
|
+
approvalPolicy: record.approvalPolicy,
|
|
1200
|
+
allowedScopes: record.allowedScopes,
|
|
1201
|
+
status: record.status,
|
|
1202
|
+
exitCode: record.exitCode,
|
|
1203
|
+
startedAt: record.startedAt,
|
|
1204
|
+
endedAt: record.endedAt,
|
|
1205
|
+
output: "",
|
|
1206
|
+
archived: record.archived,
|
|
1207
|
+
archivedAt: record.archivedAt,
|
|
1208
|
+
permissionBlocked: this.isPermissionBlocked(record),
|
|
1209
|
+
pendingEscalation: record.pendingEscalation || undefined,
|
|
1210
|
+
lastEscalationResult: record.lastEscalationResult || undefined,
|
|
1211
|
+
claudeSessionId: record.claudeSessionId || null,
|
|
1212
|
+
resumedFromSessionId: record.resumedFromSessionId ?? undefined,
|
|
1213
|
+
resumedToSessionId: record.resumedToSessionId ?? undefined,
|
|
1214
|
+
autoRecovered: record.autoRecovered ?? false,
|
|
1215
|
+
autoApprovePermissions: record.autoApprovePermissions || undefined,
|
|
1216
|
+
approvalStats: record.approvalStats.total > 0 ? record.approvalStats : undefined,
|
|
1217
|
+
summary: deriveSessionSummary(messages, record.currentTask?.title ?? null),
|
|
1218
|
+
currentTaskTitle: record.status === "running" ? record.currentTask?.title ?? undefined : undefined,
|
|
1174
1219
|
};
|
|
1175
1220
|
}
|
|
1176
1221
|
isPermissionBlocked(record) {
|
|
@@ -1438,31 +1483,39 @@ export class ProcessManager extends EventEmitter {
|
|
|
1438
1483
|
case "output.raw":
|
|
1439
1484
|
// Sync record.output from bridge before emitting so the event carries fresh data
|
|
1440
1485
|
record.output = record.ptyBridge?.getRawOutput() ?? record.output;
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1486
|
+
{
|
|
1487
|
+
const rawMessages = record.ptyBridge?.getMessages() ?? [];
|
|
1488
|
+
const messages = truncateMessagesForTransport(rawMessages, this.config.cardDefaults ?? {}, rawMessages.length - 1);
|
|
1489
|
+
// Emit output event for terminal view
|
|
1490
|
+
this.emitEvent({
|
|
1491
|
+
type: "output",
|
|
1492
|
+
sessionId: event.sessionId,
|
|
1493
|
+
data: {
|
|
1494
|
+
chunk: event.data.chunk,
|
|
1495
|
+
output: record.output,
|
|
1496
|
+
messages,
|
|
1497
|
+
permissionBlocked: this.isPermissionBlocked(record),
|
|
1498
|
+
},
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1452
1501
|
break;
|
|
1453
1502
|
case "output.chat":
|
|
1454
1503
|
// Sync record.output from bridge before emitting so the event carries fresh data
|
|
1455
1504
|
record.output = record.ptyBridge?.getRawOutput() ?? record.output;
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1505
|
+
{
|
|
1506
|
+
const rawMessages = record.ptyBridge?.getMessages() ?? [];
|
|
1507
|
+
const messages = truncateMessagesForTransport(rawMessages, this.config.cardDefaults ?? {}, rawMessages.length - 1);
|
|
1508
|
+
// Emit output event with updated messages for chat view
|
|
1509
|
+
this.emitEvent({
|
|
1510
|
+
type: "output",
|
|
1511
|
+
sessionId: event.sessionId,
|
|
1512
|
+
data: {
|
|
1513
|
+
output: record.output,
|
|
1514
|
+
messages,
|
|
1515
|
+
permissionBlocked: this.isPermissionBlocked(record),
|
|
1516
|
+
},
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1466
1519
|
break;
|
|
1467
1520
|
case "permission.prompt": {
|
|
1468
1521
|
const data = event.data;
|
|
@@ -71,6 +71,11 @@ function listAllSessions(processes, structured) {
|
|
|
71
71
|
return [...structured.list(), ...processes.list()]
|
|
72
72
|
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
73
73
|
}
|
|
74
|
+
/** Lightweight session list — omits output and messages to reduce payload. */
|
|
75
|
+
function listAllSessionsSlim(processes, structured) {
|
|
76
|
+
return [...structured.listSlim(), ...processes.listSlim()]
|
|
77
|
+
.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
|
|
78
|
+
}
|
|
74
79
|
function requireWorktreeSession(snapshot) {
|
|
75
80
|
if (!snapshot) {
|
|
76
81
|
throw new Error("未找到该会话。");
|
|
@@ -133,7 +138,7 @@ function isMergeActionAllowed(snapshot) {
|
|
|
133
138
|
}
|
|
134
139
|
export function registerSessionRoutes(app, processes, structured, storage, defaultMode) {
|
|
135
140
|
app.get("/api/sessions", (_req, res) => {
|
|
136
|
-
const all =
|
|
141
|
+
const all = listAllSessionsSlim(processes, structured);
|
|
137
142
|
console.log("[WAND] GET /api/sessions count:", all.length, "sessions:", all.map(s => ({ id: s.id.substring(0, 8), kind: s.sessionKind, runner: s.runner, status: s.status })));
|
|
138
143
|
res.json(all);
|
|
139
144
|
});
|
|
@@ -178,6 +183,29 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
178
183
|
res.status(400).json({ error: getErrorMessage(error, "无法发送结构化消息。") });
|
|
179
184
|
}
|
|
180
185
|
});
|
|
186
|
+
// ── Tool content lazy-load endpoint ──
|
|
187
|
+
app.get("/api/sessions/:id/tool-content/:toolUseId", (req, res) => {
|
|
188
|
+
const snapshot = getSessionById(processes, structured, req.params.id);
|
|
189
|
+
if (!snapshot) {
|
|
190
|
+
res.status(404).json({ error: "未找到该会话。" });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const toolUseId = req.params.toolUseId;
|
|
194
|
+
const messages = snapshot.messages ?? [];
|
|
195
|
+
for (const turn of messages) {
|
|
196
|
+
for (const block of turn.content) {
|
|
197
|
+
if (block.type === "tool_result" && block.tool_use_id === toolUseId) {
|
|
198
|
+
res.json({
|
|
199
|
+
tool_use_id: block.tool_use_id,
|
|
200
|
+
content: block.content,
|
|
201
|
+
is_error: block.is_error || false,
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
res.status(404).json({ error: "未找到该工具结果。" });
|
|
208
|
+
});
|
|
181
209
|
app.post("/api/sessions/:id/worktree/merge/check", (req, res) => {
|
|
182
210
|
try {
|
|
183
211
|
const current = requireWorktreeSession(getLatestSessionSnapshot(processes, structured, storage, req.params.id));
|