@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.
@@ -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