@linkshell/host 0.4.39 → 0.4.42
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/host/src/host.js +2 -0
- package/dist/host/src/host.js.map +1 -1
- package/dist/host/src/index.d.ts +1 -0
- package/dist/host/src/index.js +1 -0
- package/dist/host/src/index.js.map +1 -1
- package/dist/host/src/input.js +3 -0
- package/dist/host/src/input.js.map +1 -1
- package/dist/host/src/rpc/server.d.ts +2 -0
- package/dist/host/src/rpc/server.js +3 -0
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/usage.d.ts +39 -0
- package/dist/host/src/usage.js +217 -0
- package/dist/host/src/usage.js.map +1 -0
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/index.d.ts +2 -0
- package/dist/wire/src/index.js +2 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/rpc.d.ts +177 -0
- package/dist/wire/src/rpc.js +2 -0
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/usage.d.ts +204 -0
- package/dist/wire/src/usage.js +17 -0
- package/dist/wire/src/usage.js.map +1 -0
- package/dist/wire/src/watch.d.ts +1482 -0
- package/dist/wire/src/watch.js +734 -0
- package/dist/wire/src/watch.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { classifySessionPhase, isLiveActivityActionCurrent, liveActivityActionSchema, liveActivityActions, liveActivityDetailLink, liveActivitySessionTask, needsAttention, needsUserInput, phasePriority, plainPreview, sessionTitle, terminalState, tidyCommand, cleanToolTitle, } from "./live-activity.js";
|
|
3
|
+
export const watchComputerSchema = z.object({
|
|
4
|
+
key: z.string().min(1).max(512),
|
|
5
|
+
name: z.string().min(1).max(80),
|
|
6
|
+
online: z.boolean(),
|
|
7
|
+
selected: z.boolean(),
|
|
8
|
+
platform: z.enum(["darwin", "win32", "linux", "unknown"]).optional(),
|
|
9
|
+
});
|
|
10
|
+
export const watchWorkspaceSchema = z.object({
|
|
11
|
+
path: z.string().min(1).max(240),
|
|
12
|
+
name: z.string().min(1).max(80),
|
|
13
|
+
});
|
|
14
|
+
export const watchConfigOptionValueSchema = z.object({
|
|
15
|
+
value: z.string().min(1).max(120),
|
|
16
|
+
name: z.string().min(1).max(80),
|
|
17
|
+
description: z.string().max(140).optional(),
|
|
18
|
+
});
|
|
19
|
+
export const watchConfigOptionSchema = z.object({
|
|
20
|
+
id: z.string().min(1).max(64),
|
|
21
|
+
name: z.string().min(1).max(64),
|
|
22
|
+
category: z.enum(["model", "model_config", "mode", "effort", "other"]).default("other"),
|
|
23
|
+
current: z.string().max(120),
|
|
24
|
+
currentLabel: z.string().max(80).optional(),
|
|
25
|
+
values: z.array(watchConfigOptionValueSchema).max(12),
|
|
26
|
+
});
|
|
27
|
+
export const watchActionSchema = liveActivityActionSchema;
|
|
28
|
+
export const watchPlanProgressSchema = z.object({
|
|
29
|
+
completed: z.number().int().min(0),
|
|
30
|
+
total: z.number().int().min(1),
|
|
31
|
+
currentStep: z.string().max(120).optional(),
|
|
32
|
+
});
|
|
33
|
+
export const watchTimelineItemSchema = z.object({
|
|
34
|
+
id: z.string().min(1).max(128),
|
|
35
|
+
kind: z.enum([
|
|
36
|
+
"user",
|
|
37
|
+
"assistant",
|
|
38
|
+
"thought",
|
|
39
|
+
"tool",
|
|
40
|
+
"diff",
|
|
41
|
+
"subagent",
|
|
42
|
+
"image",
|
|
43
|
+
"plan",
|
|
44
|
+
"permission",
|
|
45
|
+
"question",
|
|
46
|
+
"error",
|
|
47
|
+
]),
|
|
48
|
+
title: z.string().max(120).optional(),
|
|
49
|
+
text: z.string().max(600),
|
|
50
|
+
detail: z.string().max(500).optional(),
|
|
51
|
+
status: z.enum(["running", "completed", "failed", "waiting"]).optional(),
|
|
52
|
+
filePath: z.string().max(200).optional(),
|
|
53
|
+
diffAdded: z.number().int().min(0).optional(),
|
|
54
|
+
diffRemoved: z.number().int().min(0).optional(),
|
|
55
|
+
subagentType: z.string().max(64).optional(),
|
|
56
|
+
subagentName: z.string().max(64).optional(),
|
|
57
|
+
subagentModel: z.string().max(80).optional(),
|
|
58
|
+
imageBase64: z.string().max(16_384).optional(),
|
|
59
|
+
imageMimeType: z.string().max(64).optional(),
|
|
60
|
+
});
|
|
61
|
+
export const watchSessionSchema = z.object({
|
|
62
|
+
id: z.string().min(1).max(128),
|
|
63
|
+
title: z.string().max(120),
|
|
64
|
+
subtitle: z.string().max(240),
|
|
65
|
+
project: z.string().max(80),
|
|
66
|
+
cwd: z.string().max(240).optional(),
|
|
67
|
+
branch: z.string().max(80).optional(),
|
|
68
|
+
agent: z.string().min(1).max(80),
|
|
69
|
+
model: z.string().max(80).optional(),
|
|
70
|
+
mode: z.string().max(80).optional(),
|
|
71
|
+
subagents: z
|
|
72
|
+
.object({
|
|
73
|
+
total: z.number().int().min(0),
|
|
74
|
+
running: z.number().int().min(0),
|
|
75
|
+
})
|
|
76
|
+
.optional(),
|
|
77
|
+
configOptions: z.array(watchConfigOptionSchema).max(6).optional(),
|
|
78
|
+
phase: z.enum([
|
|
79
|
+
"waiting_for_approval",
|
|
80
|
+
"waiting_for_input",
|
|
81
|
+
"failed",
|
|
82
|
+
"running",
|
|
83
|
+
"starting",
|
|
84
|
+
"completed",
|
|
85
|
+
"stale",
|
|
86
|
+
]),
|
|
87
|
+
updatedAt: z.number(),
|
|
88
|
+
pinned: z.boolean().optional(),
|
|
89
|
+
promptTitle: z.string().max(200).optional(),
|
|
90
|
+
commandPreview: z.string().max(200).optional(),
|
|
91
|
+
detailPreview: z.string().max(240).optional(),
|
|
92
|
+
goal: z.string().max(160).optional(),
|
|
93
|
+
planProgress: watchPlanProgressSchema.optional(),
|
|
94
|
+
actions: z.array(watchActionSchema).max(3).optional(),
|
|
95
|
+
hasMoreActions: z.boolean().optional(),
|
|
96
|
+
hasEarlier: z.boolean().optional(),
|
|
97
|
+
items: z.array(watchTimelineItemSchema).max(60).optional(),
|
|
98
|
+
canCancel: z.boolean(),
|
|
99
|
+
canSend: z.boolean(),
|
|
100
|
+
handoff: z.enum(["none", "can_takeover", "can_release"]),
|
|
101
|
+
deepLink: z.string().max(2048),
|
|
102
|
+
});
|
|
103
|
+
export const watchTerminalSchema = z.object({
|
|
104
|
+
id: z.string().min(1).max(128),
|
|
105
|
+
title: z.string().max(120),
|
|
106
|
+
subtitle: z.string().max(160),
|
|
107
|
+
project: z.string().max(80),
|
|
108
|
+
cwd: z.string().max(240).optional(),
|
|
109
|
+
command: z.string().max(160).optional(),
|
|
110
|
+
outputPreview: z.string().max(2400).optional(),
|
|
111
|
+
phase: z.enum(["running", "failed", "completed", "stale"]),
|
|
112
|
+
activeAt: z.number(),
|
|
113
|
+
exitCode: z.number().int().optional(),
|
|
114
|
+
deepLink: z.string().max(2048),
|
|
115
|
+
});
|
|
116
|
+
export const watchSnapshotSchema = z.object({
|
|
117
|
+
version: z.literal(1),
|
|
118
|
+
generatedAt: z.number(),
|
|
119
|
+
computerKey: z.string().max(512),
|
|
120
|
+
computerName: z.string().max(80),
|
|
121
|
+
online: z.boolean(),
|
|
122
|
+
statusLabel: z.string().max(80),
|
|
123
|
+
waitingCount: z.number().int().min(0),
|
|
124
|
+
runningCount: z.number().int().min(0),
|
|
125
|
+
failedCount: z.number().int().min(0),
|
|
126
|
+
completedCount: z.number().int().min(0),
|
|
127
|
+
availableAgents: z.array(z.string().min(1).max(48)).max(12).optional(),
|
|
128
|
+
workspaces: z.array(watchWorkspaceSchema).max(10).optional(),
|
|
129
|
+
computers: z.array(watchComputerSchema).max(12),
|
|
130
|
+
sessions: z.array(watchSessionSchema).max(12),
|
|
131
|
+
terminals: z.array(watchTerminalSchema).max(8),
|
|
132
|
+
});
|
|
133
|
+
export const watchCommandSchema = z.discriminatedUnion("kind", [
|
|
134
|
+
z.object({
|
|
135
|
+
id: z.string().min(1).max(128),
|
|
136
|
+
kind: z.literal("action"),
|
|
137
|
+
action: watchActionSchema,
|
|
138
|
+
}),
|
|
139
|
+
z.object({
|
|
140
|
+
id: z.string().min(1).max(128),
|
|
141
|
+
kind: z.literal("prompt"),
|
|
142
|
+
computerKey: z.string().min(1).max(512),
|
|
143
|
+
sessionId: z.string().min(1).max(128),
|
|
144
|
+
text: z.string().min(1).max(2000),
|
|
145
|
+
now: z.boolean().optional(),
|
|
146
|
+
}),
|
|
147
|
+
z.object({
|
|
148
|
+
id: z.string().min(1).max(128),
|
|
149
|
+
kind: z.literal("cancel"),
|
|
150
|
+
computerKey: z.string().min(1).max(512),
|
|
151
|
+
sessionId: z.string().min(1).max(128),
|
|
152
|
+
}),
|
|
153
|
+
z.object({
|
|
154
|
+
id: z.string().min(1).max(128),
|
|
155
|
+
kind: z.literal("takeover"),
|
|
156
|
+
computerKey: z.string().min(1).max(512),
|
|
157
|
+
sessionId: z.string().min(1).max(128),
|
|
158
|
+
}),
|
|
159
|
+
z.object({
|
|
160
|
+
id: z.string().min(1).max(128),
|
|
161
|
+
kind: z.literal("release"),
|
|
162
|
+
computerKey: z.string().min(1).max(512),
|
|
163
|
+
sessionId: z.string().min(1).max(128),
|
|
164
|
+
}),
|
|
165
|
+
z.object({
|
|
166
|
+
id: z.string().min(1).max(128),
|
|
167
|
+
kind: z.literal("set-config"),
|
|
168
|
+
computerKey: z.string().min(1).max(512),
|
|
169
|
+
sessionId: z.string().min(1).max(128),
|
|
170
|
+
optionId: z.string().min(1).max(64),
|
|
171
|
+
value: z.string().min(1).max(120),
|
|
172
|
+
}),
|
|
173
|
+
z.object({
|
|
174
|
+
id: z.string().min(1).max(128),
|
|
175
|
+
kind: z.literal("open-session"),
|
|
176
|
+
computerKey: z.string().min(1).max(512),
|
|
177
|
+
sessionId: z.string().min(1).max(128),
|
|
178
|
+
}),
|
|
179
|
+
z.object({
|
|
180
|
+
id: z.string().min(1).max(128),
|
|
181
|
+
kind: z.literal("load-earlier"),
|
|
182
|
+
computerKey: z.string().min(1).max(512),
|
|
183
|
+
sessionId: z.string().min(1).max(128),
|
|
184
|
+
}),
|
|
185
|
+
z.object({
|
|
186
|
+
id: z.string().min(1).max(128),
|
|
187
|
+
kind: z.literal("create-session"),
|
|
188
|
+
computerKey: z.string().min(1).max(512),
|
|
189
|
+
agent: z.string().min(1).max(48),
|
|
190
|
+
cwd: z.string().min(1).max(240),
|
|
191
|
+
prompt: z.string().max(2000).optional(),
|
|
192
|
+
}),
|
|
193
|
+
z.object({
|
|
194
|
+
id: z.string().min(1).max(128),
|
|
195
|
+
kind: z.literal("create-terminal"),
|
|
196
|
+
computerKey: z.string().min(1).max(512),
|
|
197
|
+
cwd: z.string().max(240).optional(),
|
|
198
|
+
command: z.string().max(500).optional(),
|
|
199
|
+
}),
|
|
200
|
+
z.object({
|
|
201
|
+
id: z.string().min(1).max(128),
|
|
202
|
+
kind: z.literal("open-terminal"),
|
|
203
|
+
computerKey: z.string().min(1).max(512),
|
|
204
|
+
terminalId: z.string().min(1).max(128),
|
|
205
|
+
}),
|
|
206
|
+
z.object({
|
|
207
|
+
id: z.string().min(1).max(128),
|
|
208
|
+
kind: z.literal("terminal-input"),
|
|
209
|
+
computerKey: z.string().min(1).max(512),
|
|
210
|
+
terminalId: z.string().min(1).max(128),
|
|
211
|
+
data: z.string().min(1).max(2000),
|
|
212
|
+
}),
|
|
213
|
+
z.object({
|
|
214
|
+
id: z.string().min(1).max(128),
|
|
215
|
+
kind: z.literal("close-terminal"),
|
|
216
|
+
computerKey: z.string().min(1).max(512),
|
|
217
|
+
terminalId: z.string().min(1).max(128),
|
|
218
|
+
}),
|
|
219
|
+
z.object({
|
|
220
|
+
id: z.string().min(1).max(128),
|
|
221
|
+
kind: z.literal("select-computer"),
|
|
222
|
+
computerKey: z.string().min(1).max(512),
|
|
223
|
+
}),
|
|
224
|
+
z.object({
|
|
225
|
+
id: z.string().min(1).max(128),
|
|
226
|
+
kind: z.literal("refresh"),
|
|
227
|
+
computerKey: z.string().max(512).optional(),
|
|
228
|
+
}),
|
|
229
|
+
]);
|
|
230
|
+
export const DEFAULT_WATCH_QUICK_PROMPTS = [
|
|
231
|
+
{ id: "continue", label: "继续推进", icon: "play.fill", prompt: "请继续推进当前任务。" },
|
|
232
|
+
{ id: "test", label: "运行测试", icon: "checkmark.shield.fill", prompt: "请运行相关测试并汇报结果。" },
|
|
233
|
+
{ id: "summary", label: "总结进展", icon: "text.alignleft", prompt: "请简明总结目前的进展与下一步计划。" },
|
|
234
|
+
{ id: "fix", label: "排查修复", icon: "wrench.and.screwdriver.fill", prompt: "请分析当前报错原因并直接修复验证。" },
|
|
235
|
+
];
|
|
236
|
+
function cut(value, max) {
|
|
237
|
+
return [...(value ?? "")].slice(0, max).join("");
|
|
238
|
+
}
|
|
239
|
+
function baseName(path) {
|
|
240
|
+
const parts = path.split(/[\\/]/).filter(Boolean);
|
|
241
|
+
return parts[parts.length - 1] ?? path;
|
|
242
|
+
}
|
|
243
|
+
function extractCommandPreview(session) {
|
|
244
|
+
const permTool = session.permission?.tool;
|
|
245
|
+
if (permTool?.title) {
|
|
246
|
+
const cleaned = cleanToolTitle(permTool.title);
|
|
247
|
+
return cut(permTool.kind === "execute" ? tidyCommand(cleaned) : cleaned, 160) || undefined;
|
|
248
|
+
}
|
|
249
|
+
if (session.activity?.kind === "tool" && session.activity.title) {
|
|
250
|
+
const cleaned = cleanToolTitle(session.activity.title);
|
|
251
|
+
return cut(session.activity.toolKind === "execute" ? tidyCommand(cleaned) : cleaned, 160) || undefined;
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
function extractPromptTitle(session) {
|
|
256
|
+
if (!needsAttention(session))
|
|
257
|
+
return undefined;
|
|
258
|
+
const questionText = session.permission?.questions?.[0]?.text?.trim();
|
|
259
|
+
if (questionText)
|
|
260
|
+
return cut(questionText, 180);
|
|
261
|
+
const permTitle = session.permission?.title?.trim();
|
|
262
|
+
if (permTitle)
|
|
263
|
+
return cut(permTitle, 180);
|
|
264
|
+
const asyncTitle = session.asyncQuestions?.[0]?.title?.trim();
|
|
265
|
+
if (asyncTitle)
|
|
266
|
+
return cut(asyncTitle, 180);
|
|
267
|
+
return needsUserInput(session) ? "Agent 正在等待你的回答" : "需要你的权限确认";
|
|
268
|
+
}
|
|
269
|
+
function fallbackTimelineItems(session, commandPreview, preview) {
|
|
270
|
+
const items = [];
|
|
271
|
+
if (session.title?.trim()) {
|
|
272
|
+
items.push({
|
|
273
|
+
id: `${session.id}:title`,
|
|
274
|
+
kind: "user",
|
|
275
|
+
title: "任务",
|
|
276
|
+
text: cut(session.title.trim(), 240),
|
|
277
|
+
status: "completed",
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
if (commandPreview) {
|
|
281
|
+
items.push({
|
|
282
|
+
id: `${session.id}:cmd`,
|
|
283
|
+
kind: "tool",
|
|
284
|
+
title: session.permission ? "等待权限确认" : "执行命令",
|
|
285
|
+
text: cut(commandPreview, 200),
|
|
286
|
+
status: session.permission ? "waiting" : session.state === "running" ? "running" : "completed",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (preview && preview !== session.title?.trim()) {
|
|
290
|
+
items.push({
|
|
291
|
+
id: `${session.id}:preview`,
|
|
292
|
+
kind: session.state === "error" ? "error" : "assistant",
|
|
293
|
+
title: session.state === "error" ? "报错信息" : "最新回复",
|
|
294
|
+
text: cut(preview, 360),
|
|
295
|
+
status: session.state === "error" ? "failed" : session.state === "running" ? "running" : "completed",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
return items;
|
|
299
|
+
}
|
|
300
|
+
export function sliceTimelinePreservingUsers(items, maxItems) {
|
|
301
|
+
if (items.length <= maxItems)
|
|
302
|
+
return items;
|
|
303
|
+
const tail = items.slice(-maxItems);
|
|
304
|
+
if (tail.some((item) => item.kind === "user"))
|
|
305
|
+
return tail;
|
|
306
|
+
const keepUsers = maxItems >= 6 ? 2 : 1;
|
|
307
|
+
const earlierUsers = items
|
|
308
|
+
.slice(0, items.length - maxItems)
|
|
309
|
+
.filter((item) => item.kind === "user")
|
|
310
|
+
.slice(-keepUsers);
|
|
311
|
+
if (earlierUsers.length === 0)
|
|
312
|
+
return tail;
|
|
313
|
+
return [...earlierUsers, ...tail.slice(earlierUsers.length)];
|
|
314
|
+
}
|
|
315
|
+
export function buildWatchSession(session, online, computerKey = "", options = {}) {
|
|
316
|
+
const task = liveActivitySessionTask(session, online);
|
|
317
|
+
const phase = task.phase;
|
|
318
|
+
const handoff = session.driver === "desktop"
|
|
319
|
+
? "can_takeover"
|
|
320
|
+
: session.driver === "remote" && (options.tier === "handoff" || session.agent === "claude")
|
|
321
|
+
? "can_release"
|
|
322
|
+
: "none";
|
|
323
|
+
const canSend = online && options.tier !== "terminal" && session.driver !== "desktop";
|
|
324
|
+
const canCancel = online && (session.state === "running" || session.state === "waiting" || session.state === "unknown");
|
|
325
|
+
const { actions, hasMoreActions } = online && session.driver !== "desktop"
|
|
326
|
+
? liveActivityActions(session, computerKey)
|
|
327
|
+
: { actions: [], hasMoreActions: online && needsAttention(session) };
|
|
328
|
+
const preview = session.preview ? cut(plainPreview(session.preview), 200) : undefined;
|
|
329
|
+
const commandPreview = extractCommandPreview(session);
|
|
330
|
+
const promptTitle = extractPromptTitle(session);
|
|
331
|
+
const branch = session.worktree?.branch ? cut(session.worktree.branch.replace(/^refs\/heads\//, ""), 64) : undefined;
|
|
332
|
+
const configOptions = options.configOptions?.slice(0, 6);
|
|
333
|
+
const modelOpt = configOptions?.find((c) => c.category === "model" || c.id === "model");
|
|
334
|
+
const modeOpt = configOptions?.find((c) => c.category === "mode" || c.id === "mode" || c.id === "sandbox");
|
|
335
|
+
const model = cut(session.model || modelOpt?.currentLabel || modelOpt?.current, 64) || undefined;
|
|
336
|
+
const mode = cut(options.mode || modeOpt?.currentLabel || modeOpt?.current, 64) || undefined;
|
|
337
|
+
const subagents = session.subagents && session.subagents.total > 0
|
|
338
|
+
? {
|
|
339
|
+
total: Math.max(0, session.subagents.total),
|
|
340
|
+
running: Math.max(0, session.subagents.running),
|
|
341
|
+
}
|
|
342
|
+
: undefined;
|
|
343
|
+
const planProgress = options.planProgress && options.planProgress.total > 0
|
|
344
|
+
? {
|
|
345
|
+
completed: Math.max(0, Math.min(options.planProgress.completed, options.planProgress.total)),
|
|
346
|
+
total: options.planProgress.total,
|
|
347
|
+
...(options.planProgress.currentStep ? { currentStep: cut(options.planProgress.currentStep, 100) } : {}),
|
|
348
|
+
}
|
|
349
|
+
: undefined;
|
|
350
|
+
const maxItems = Math.max(4, Math.min(options.maxItems ?? 14, 56));
|
|
351
|
+
const rawItems = options.items?.length ? options.items : fallbackTimelineItems(session, commandPreview, preview);
|
|
352
|
+
const items = maxItems > 14
|
|
353
|
+
? rawItems.slice(-maxItems)
|
|
354
|
+
: sliceTimelinePreservingUsers(rawItems, maxItems);
|
|
355
|
+
const hasEarlier = Boolean(options.hasEarlier || rawItems.length > items.length);
|
|
356
|
+
return {
|
|
357
|
+
id: session.id,
|
|
358
|
+
title: cut(sessionTitle(session), 96),
|
|
359
|
+
subtitle: cut(session.driver === "desktop" && needsAttention(session)
|
|
360
|
+
? "在电脑终端等待确认 · 可接管后在腕表处理"
|
|
361
|
+
: task.subtitle, 200),
|
|
362
|
+
project: cut(baseName(session.worktree?.source ?? session.cwd), 64),
|
|
363
|
+
cwd: cut(session.worktree?.source ?? session.cwd, 200),
|
|
364
|
+
...(branch ? { branch } : {}),
|
|
365
|
+
agent: cut(session.agent, 48),
|
|
366
|
+
...(model ? { model } : {}),
|
|
367
|
+
...(mode ? { mode } : {}),
|
|
368
|
+
...(subagents ? { subagents } : {}),
|
|
369
|
+
...(configOptions?.length ? { configOptions } : {}),
|
|
370
|
+
phase,
|
|
371
|
+
updatedAt: session.updatedAt,
|
|
372
|
+
...(options.pinned ? { pinned: true } : {}),
|
|
373
|
+
...(promptTitle ? { promptTitle } : {}),
|
|
374
|
+
...(commandPreview ? { commandPreview } : {}),
|
|
375
|
+
...(preview ? { detailPreview: preview } : {}),
|
|
376
|
+
...(options.goal ? { goal: cut(options.goal, 120) } : {}),
|
|
377
|
+
...(planProgress ? { planProgress } : {}),
|
|
378
|
+
...(actions.length ? { actions } : {}),
|
|
379
|
+
...(hasMoreActions ? { hasMoreActions: true } : {}),
|
|
380
|
+
...(hasEarlier ? { hasEarlier: true } : {}),
|
|
381
|
+
...(items.length ? { items } : {}),
|
|
382
|
+
canCancel,
|
|
383
|
+
canSend,
|
|
384
|
+
handoff,
|
|
385
|
+
deepLink: liveActivityDetailLink(computerKey, "session", session.id),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
export function buildWatchTerminal(terminal, online, computerKey = "", outputPreview) {
|
|
389
|
+
const state = terminalState(terminal);
|
|
390
|
+
const phase = !online ? "stale" : state.failed ? "failed" : state.busy ? "running" : "completed";
|
|
391
|
+
return {
|
|
392
|
+
id: terminal.id,
|
|
393
|
+
title: cut(state.title || "终端", 96),
|
|
394
|
+
subtitle: cut(state.detail || baseName(terminal.cwd), 120),
|
|
395
|
+
project: cut(baseName(terminal.cwd), 64),
|
|
396
|
+
cwd: cut(terminal.cwd, 200),
|
|
397
|
+
...(terminal.command ? { command: cut(terminal.command, 140) } : {}),
|
|
398
|
+
...(outputPreview?.trim() ? { outputPreview: cut(outputPreview.trim(), 1800) } : {}),
|
|
399
|
+
phase,
|
|
400
|
+
activeAt: terminal.activeAt,
|
|
401
|
+
...(typeof terminal.exitCode === "number" ? { exitCode: terminal.exitCode } : {}),
|
|
402
|
+
deepLink: liveActivityDetailLink(computerKey, "terminal", terminal.id),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Builds the structured Apple Watch companion snapshot for the selected computer.
|
|
407
|
+
* Prioritizes sessions needing approval/input, then errors, then running, then pinned/recent.
|
|
408
|
+
*/
|
|
409
|
+
export function buildWatchSnapshot(computerName, sessions, terminals, online, options = {}) {
|
|
410
|
+
const computerKey = options.computerKey ?? "";
|
|
411
|
+
const pinnedSet = new Set(options.pinnedSessionIds ?? []);
|
|
412
|
+
const goals = options.goals ?? {};
|
|
413
|
+
const plans = options.plans ?? {};
|
|
414
|
+
const timelines = options.timelines ?? {};
|
|
415
|
+
const hasEarlierMap = options.hasEarlier ?? {};
|
|
416
|
+
const itemLimits = options.itemLimits ?? {};
|
|
417
|
+
const modes = options.modes ?? {};
|
|
418
|
+
const configs = options.configs ?? {};
|
|
419
|
+
const terminalOutputs = options.terminalOutputs ?? {};
|
|
420
|
+
const tiers = options.agentTiers ?? {};
|
|
421
|
+
const now = options.now ?? Date.now();
|
|
422
|
+
const liveSessions = sessions.filter((s) => !s.archived);
|
|
423
|
+
const waitingCount = liveSessions.filter((s) => needsAttention(s)).length;
|
|
424
|
+
const busyTerminals = terminals.filter((t) => terminalState(t).busy);
|
|
425
|
+
const failedTerminals = terminals.filter((t) => terminalState(t).failed);
|
|
426
|
+
const runningSessions = liveSessions.filter((s) => !needsAttention(s) && s.state === "running");
|
|
427
|
+
const failedSessions = liveSessions.filter((s) => !needsAttention(s) && s.state === "error");
|
|
428
|
+
const completedSessions = liveSessions.filter((s) => !needsAttention(s) && s.state === "idle");
|
|
429
|
+
const runningCount = runningSessions.length + busyTerminals.length;
|
|
430
|
+
const failedCount = failedSessions.length + failedTerminals.length;
|
|
431
|
+
const completedCount = completedSessions.length;
|
|
432
|
+
const sortedSessions = [...liveSessions].sort((a, b) => {
|
|
433
|
+
const pa = phasePriority(classifySessionPhase(a, online));
|
|
434
|
+
const pb = phasePriority(classifySessionPhase(b, online));
|
|
435
|
+
if (pa !== pb)
|
|
436
|
+
return pa - pb;
|
|
437
|
+
const pinA = Number(pinnedSet.has(a.id));
|
|
438
|
+
const pinB = Number(pinnedSet.has(b.id));
|
|
439
|
+
if (pinA !== pinB)
|
|
440
|
+
return pinB - pinA;
|
|
441
|
+
if (a.updatedAt !== b.updatedAt)
|
|
442
|
+
return b.updatedAt - a.updatedAt;
|
|
443
|
+
return a.id.localeCompare(b.id);
|
|
444
|
+
});
|
|
445
|
+
const sortedTerminals = [...terminals].sort((a, b) => {
|
|
446
|
+
const sa = terminalState(a);
|
|
447
|
+
const sb = terminalState(b);
|
|
448
|
+
const rank = (s) => (s.failed ? 0 : s.busy ? 1 : 2);
|
|
449
|
+
if (rank(sa) !== rank(sb))
|
|
450
|
+
return rank(sa) - rank(sb);
|
|
451
|
+
if (a.activeAt !== b.activeAt)
|
|
452
|
+
return b.activeAt - a.activeAt;
|
|
453
|
+
return a.id.localeCompare(b.id);
|
|
454
|
+
});
|
|
455
|
+
const statusLabel = !online
|
|
456
|
+
? "连接中断 · 正在重连"
|
|
457
|
+
: waitingCount > 0
|
|
458
|
+
? `${waitingCount} 项等待你的确认`
|
|
459
|
+
: failedCount > 0
|
|
460
|
+
? `${failedCount} 项执行出错`
|
|
461
|
+
: runningCount > 0
|
|
462
|
+
? `${runningCount} 项任务正在运行`
|
|
463
|
+
: liveSessions.length > 0
|
|
464
|
+
? "全部任务已就绪"
|
|
465
|
+
: "暂无活跃任务";
|
|
466
|
+
const watchComputers = options.computers?.length
|
|
467
|
+
? options.computers.slice(0, 12).map((c) => ({
|
|
468
|
+
key: cut(c.key, 512),
|
|
469
|
+
name: cut(c.name, 64),
|
|
470
|
+
online: c.online,
|
|
471
|
+
selected: c.selected,
|
|
472
|
+
...(c.platform ? { platform: c.platform } : {}),
|
|
473
|
+
}))
|
|
474
|
+
: computerKey
|
|
475
|
+
? [{ key: cut(computerKey, 512), name: cut(computerName || "电脑", 64), online, selected: true }]
|
|
476
|
+
: [];
|
|
477
|
+
const seenPaths = new Set();
|
|
478
|
+
const workspaces = [];
|
|
479
|
+
const addWorkspace = (rawPath, rawName) => {
|
|
480
|
+
const path = rawPath?.trim();
|
|
481
|
+
if (!path || seenPaths.has(path) || workspaces.length >= 8)
|
|
482
|
+
return;
|
|
483
|
+
seenPaths.add(path);
|
|
484
|
+
workspaces.push({
|
|
485
|
+
path: cut(path, 200),
|
|
486
|
+
name: cut(rawName?.trim() || baseName(path) || path, 64),
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
for (const ws of options.workspaces ?? [])
|
|
490
|
+
addWorkspace(ws.path, ws.name);
|
|
491
|
+
for (const s of sortedSessions)
|
|
492
|
+
addWorkspace(s.worktree?.source ?? s.cwd);
|
|
493
|
+
for (const t of sortedTerminals)
|
|
494
|
+
addWorkspace(t.cwd);
|
|
495
|
+
const availableAgents = (options.availableAgents?.length
|
|
496
|
+
? options.availableAgents
|
|
497
|
+
: ["claude", "codex", "gemini", "cursor", "opencode"])
|
|
498
|
+
.map((id) => cut(id.trim(), 48))
|
|
499
|
+
.filter(Boolean)
|
|
500
|
+
.slice(0, 10);
|
|
501
|
+
const snapshot = {
|
|
502
|
+
version: 1,
|
|
503
|
+
generatedAt: now,
|
|
504
|
+
computerKey: cut(computerKey, 512),
|
|
505
|
+
computerName: cut(computerName || "电脑", 64),
|
|
506
|
+
online,
|
|
507
|
+
statusLabel: cut(statusLabel, 80),
|
|
508
|
+
waitingCount,
|
|
509
|
+
runningCount,
|
|
510
|
+
failedCount,
|
|
511
|
+
completedCount,
|
|
512
|
+
availableAgents,
|
|
513
|
+
...(workspaces.length ? { workspaces } : {}),
|
|
514
|
+
computers: watchComputers,
|
|
515
|
+
sessions: sortedSessions.slice(0, 10).map((session) => {
|
|
516
|
+
const goal = goals[session.id];
|
|
517
|
+
return buildWatchSession(session, online, computerKey, {
|
|
518
|
+
pinned: pinnedSet.has(session.id),
|
|
519
|
+
goal: goal?.status === "active" ? goal.objective : undefined,
|
|
520
|
+
planProgress: plans[session.id],
|
|
521
|
+
tier: tiers[session.agent],
|
|
522
|
+
items: timelines[session.id],
|
|
523
|
+
hasEarlier: hasEarlierMap[session.id],
|
|
524
|
+
maxItems: itemLimits[session.id],
|
|
525
|
+
mode: modes[session.id],
|
|
526
|
+
configOptions: configs[session.id],
|
|
527
|
+
});
|
|
528
|
+
}),
|
|
529
|
+
terminals: sortedTerminals.slice(0, 6).map((terminal) => buildWatchTerminal(terminal, online, computerKey, terminalOutputs[terminal.id])),
|
|
530
|
+
};
|
|
531
|
+
return watchSnapshotContent(snapshot);
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* Bounds a WatchSnapshot so it fits comfortably within WatchConnectivity's
|
|
535
|
+
* applicationContext / message budget (<= 24 KB), never truncating action IDs.
|
|
536
|
+
*/
|
|
537
|
+
export function watchSnapshotContent(snapshot) {
|
|
538
|
+
const next = {
|
|
539
|
+
...snapshot,
|
|
540
|
+
computerName: cut(snapshot.computerName, 64),
|
|
541
|
+
statusLabel: cut(snapshot.statusLabel, 80),
|
|
542
|
+
...(snapshot.availableAgents?.length
|
|
543
|
+
? { availableAgents: snapshot.availableAgents.slice(0, 10).map((a) => cut(a, 48)) }
|
|
544
|
+
: {}),
|
|
545
|
+
...(snapshot.workspaces?.length
|
|
546
|
+
? {
|
|
547
|
+
workspaces: snapshot.workspaces.slice(0, 8).map((w) => ({
|
|
548
|
+
path: cut(w.path, 200),
|
|
549
|
+
name: cut(w.name, 64),
|
|
550
|
+
})),
|
|
551
|
+
}
|
|
552
|
+
: {}),
|
|
553
|
+
computers: snapshot.computers.slice(0, 12).map((c) => ({ ...c, name: cut(c.name, 64) })),
|
|
554
|
+
sessions: snapshot.sessions.slice(0, 10).map((s) => {
|
|
555
|
+
const validActions = (s.actions ?? []).filter((a) => watchActionSchema.safeParse(a).success).slice(0, 3);
|
|
556
|
+
const rawValidItems = (s.items ?? []).filter((item) => Boolean(item.id && item.text?.trim()));
|
|
557
|
+
const itemCap = rawValidItems.length > 14 ? Math.min(rawValidItems.length, 48) : 14;
|
|
558
|
+
const validItems = (itemCap > 14
|
|
559
|
+
? rawValidItems.slice(-itemCap)
|
|
560
|
+
: sliceTimelinePreservingUsers(rawValidItems, itemCap)).map((item) => ({
|
|
561
|
+
...item,
|
|
562
|
+
id: cut(item.id, 120),
|
|
563
|
+
...(item.title ? { title: cut(item.title, 80) } : {}),
|
|
564
|
+
text: cut(item.text, 360),
|
|
565
|
+
...(item.detail ? { detail: cut(item.detail, 360) } : {}),
|
|
566
|
+
...(item.filePath ? { filePath: cut(item.filePath, 160) } : {}),
|
|
567
|
+
...(item.subagentType ? { subagentType: cut(item.subagentType, 48) } : {}),
|
|
568
|
+
...(item.subagentName ? { subagentName: cut(item.subagentName, 48) } : {}),
|
|
569
|
+
...(item.subagentModel ? { subagentModel: cut(item.subagentModel, 64) } : {}),
|
|
570
|
+
...(item.imageBase64 ? { imageBase64: item.imageBase64.slice(0, 12_000) } : {}),
|
|
571
|
+
...(item.imageMimeType ? { imageMimeType: cut(item.imageMimeType, 48) } : {}),
|
|
572
|
+
}));
|
|
573
|
+
const validConfigs = (s.configOptions ?? []).slice(0, 4).map((opt) => ({
|
|
574
|
+
...opt,
|
|
575
|
+
id: cut(opt.id, 48),
|
|
576
|
+
name: cut(opt.name, 48),
|
|
577
|
+
current: cut(opt.current, 80),
|
|
578
|
+
...(opt.currentLabel ? { currentLabel: cut(opt.currentLabel, 64) } : {}),
|
|
579
|
+
values: opt.values.slice(0, 8).map((v) => ({
|
|
580
|
+
value: cut(v.value, 80),
|
|
581
|
+
name: cut(v.name, 64),
|
|
582
|
+
...(v.description ? { description: cut(v.description, 100) } : {}),
|
|
583
|
+
})),
|
|
584
|
+
}));
|
|
585
|
+
const hasEarlier = s.hasEarlier || validItems.length < rawValidItems.length || undefined;
|
|
586
|
+
return {
|
|
587
|
+
...s,
|
|
588
|
+
title: cut(s.title, 80),
|
|
589
|
+
subtitle: cut(s.subtitle, 160),
|
|
590
|
+
project: cut(s.project, 48),
|
|
591
|
+
...(s.cwd ? { cwd: cut(s.cwd, 200) } : {}),
|
|
592
|
+
...(s.branch ? { branch: cut(s.branch, 48) } : {}),
|
|
593
|
+
...(s.model ? { model: cut(s.model, 64) } : {}),
|
|
594
|
+
...(s.mode ? { mode: cut(s.mode, 64) } : {}),
|
|
595
|
+
...(validConfigs.length ? { configOptions: validConfigs } : {}),
|
|
596
|
+
...(s.promptTitle ? { promptTitle: cut(s.promptTitle, 140) } : {}),
|
|
597
|
+
...(s.commandPreview ? { commandPreview: cut(s.commandPreview, 140) } : {}),
|
|
598
|
+
...(s.detailPreview ? { detailPreview: cut(s.detailPreview, 160) } : {}),
|
|
599
|
+
...(s.goal ? { goal: cut(s.goal, 100) } : {}),
|
|
600
|
+
...(s.planProgress
|
|
601
|
+
? {
|
|
602
|
+
planProgress: {
|
|
603
|
+
completed: Math.max(0, Math.min(s.planProgress.completed, s.planProgress.total)),
|
|
604
|
+
total: Math.max(1, s.planProgress.total),
|
|
605
|
+
...(s.planProgress.currentStep ? { currentStep: cut(s.planProgress.currentStep, 80) } : {}),
|
|
606
|
+
},
|
|
607
|
+
}
|
|
608
|
+
: {}),
|
|
609
|
+
...(validActions.length ? { actions: validActions } : { actions: undefined }),
|
|
610
|
+
hasMoreActions: s.hasMoreActions || validActions.length < (s.actions?.length ?? 0) || undefined,
|
|
611
|
+
...(hasEarlier ? { hasEarlier: true } : { hasEarlier: undefined }),
|
|
612
|
+
...(validItems.length ? { items: validItems } : {}),
|
|
613
|
+
};
|
|
614
|
+
}),
|
|
615
|
+
terminals: snapshot.terminals.slice(0, 6).map((t) => ({
|
|
616
|
+
...t,
|
|
617
|
+
title: cut(t.title, 80),
|
|
618
|
+
subtitle: cut(t.subtitle, 120),
|
|
619
|
+
project: cut(t.project, 48),
|
|
620
|
+
...(t.cwd ? { cwd: cut(t.cwd, 200) } : {}),
|
|
621
|
+
...(t.command ? { command: cut(t.command, 120) } : {}),
|
|
622
|
+
...(t.outputPreview ? { outputPreview: cut(t.outputPreview, 1400) } : {}),
|
|
623
|
+
})),
|
|
624
|
+
};
|
|
625
|
+
const size = () => new TextEncoder().encode(JSON.stringify(next)).length;
|
|
626
|
+
if (size() > 24_000) {
|
|
627
|
+
next.sessions = next.sessions.map((s, idx) => {
|
|
628
|
+
const isExpanded = (s.items?.length ?? 0) > 14;
|
|
629
|
+
const cap = isExpanded ? 32 : idx < 4 ? 10 : 4;
|
|
630
|
+
return {
|
|
631
|
+
...s,
|
|
632
|
+
hasEarlier: s.hasEarlier || (s.items?.length ?? 0) > cap || undefined,
|
|
633
|
+
items: s.items
|
|
634
|
+
? (isExpanded ? s.items.slice(-cap) : sliceTimelinePreservingUsers(s.items, cap)).map((i) => ({
|
|
635
|
+
...i,
|
|
636
|
+
text: cut(i.text, idx < 4 || isExpanded ? 260 : 160),
|
|
637
|
+
...(i.detail ? { detail: cut(i.detail, 150) } : {}),
|
|
638
|
+
imageBase64: idx === 0 ? i.imageBase64 : undefined,
|
|
639
|
+
}))
|
|
640
|
+
: undefined,
|
|
641
|
+
};
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
if (size() > 24_000) {
|
|
645
|
+
next.terminals = next.terminals.map((t) => ({
|
|
646
|
+
...t,
|
|
647
|
+
outputPreview: t.outputPreview ? cut(t.outputPreview, 800) : undefined,
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
if (size() > 24_000) {
|
|
651
|
+
next.sessions = next.sessions.map((s, idx) => {
|
|
652
|
+
const isExpanded = (s.items?.length ?? 0) > 14;
|
|
653
|
+
const cap = isExpanded ? 24 : idx < 3 ? 6 : 3;
|
|
654
|
+
return {
|
|
655
|
+
...s,
|
|
656
|
+
configOptions: idx < 2 || isExpanded ? s.configOptions : undefined,
|
|
657
|
+
hasEarlier: s.hasEarlier || (s.items?.length ?? 0) > cap || undefined,
|
|
658
|
+
items: s.items
|
|
659
|
+
? (isExpanded ? s.items.slice(-cap) : sliceTimelinePreservingUsers(s.items, cap)).map((i) => ({
|
|
660
|
+
...i,
|
|
661
|
+
text: cut(i.text, 160),
|
|
662
|
+
detail: undefined,
|
|
663
|
+
imageBase64: undefined,
|
|
664
|
+
}))
|
|
665
|
+
: undefined,
|
|
666
|
+
};
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
while (size() > 24_000 && next.terminals.length > 2)
|
|
670
|
+
next.terminals.pop();
|
|
671
|
+
while (size() > 24_000 && next.sessions.length > 4)
|
|
672
|
+
next.sessions.pop();
|
|
673
|
+
if (size() > 24_000) {
|
|
674
|
+
next.sessions = next.sessions.map((s) => ({
|
|
675
|
+
...s,
|
|
676
|
+
detailPreview: s.detailPreview ? cut(s.detailPreview, 60) : undefined,
|
|
677
|
+
commandPreview: s.commandPreview ? cut(s.commandPreview, 60) : undefined,
|
|
678
|
+
items: undefined,
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
return next;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Validates a command originating from Apple Watch against the connected computer
|
|
685
|
+
* and live session state before executing any RPC on the host.
|
|
686
|
+
*/
|
|
687
|
+
export function isWatchCommandCurrent(command, computerKey, session, permissions = []) {
|
|
688
|
+
if (command.kind === "select-computer") {
|
|
689
|
+
return Boolean(command.computerKey.trim());
|
|
690
|
+
}
|
|
691
|
+
if (command.kind === "refresh") {
|
|
692
|
+
return !command.computerKey || command.computerKey === computerKey;
|
|
693
|
+
}
|
|
694
|
+
if (command.kind === "open-session" || command.kind === "load-earlier") {
|
|
695
|
+
return command.computerKey === computerKey && Boolean(command.sessionId.trim());
|
|
696
|
+
}
|
|
697
|
+
if (command.kind === "create-session") {
|
|
698
|
+
return (command.computerKey === computerKey &&
|
|
699
|
+
Boolean(command.agent.trim()) &&
|
|
700
|
+
Boolean(command.cwd.trim()));
|
|
701
|
+
}
|
|
702
|
+
if (command.kind === "create-terminal") {
|
|
703
|
+
return command.computerKey === computerKey;
|
|
704
|
+
}
|
|
705
|
+
if (command.kind === "open-terminal" || command.kind === "close-terminal") {
|
|
706
|
+
return command.computerKey === computerKey && Boolean(command.terminalId.trim());
|
|
707
|
+
}
|
|
708
|
+
if (command.kind === "terminal-input") {
|
|
709
|
+
return (command.computerKey === computerKey &&
|
|
710
|
+
Boolean(command.terminalId.trim()) &&
|
|
711
|
+
Boolean(command.data.length > 0));
|
|
712
|
+
}
|
|
713
|
+
if (command.kind === "action") {
|
|
714
|
+
if (session?.driver === "desktop")
|
|
715
|
+
return false;
|
|
716
|
+
return isLiveActivityActionCurrent(command.action, computerKey, session, permissions);
|
|
717
|
+
}
|
|
718
|
+
if (!session || session.archived || command.computerKey !== computerKey || command.sessionId !== session.id) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
switch (command.kind) {
|
|
722
|
+
case "prompt":
|
|
723
|
+
return Boolean(command.text.trim()) && session.driver !== "desktop";
|
|
724
|
+
case "set-config":
|
|
725
|
+
return Boolean(command.optionId.trim()) && Boolean(command.value.trim()) && session.driver !== "desktop";
|
|
726
|
+
case "cancel":
|
|
727
|
+
return session.state === "running" || session.state === "waiting" || session.state === "unknown";
|
|
728
|
+
case "takeover":
|
|
729
|
+
return session.driver === "desktop";
|
|
730
|
+
case "release":
|
|
731
|
+
return session.driver === "remote";
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
//# sourceMappingURL=watch.js.map
|