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