@linkshell/host 0.4.69 → 0.4.71

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.
Files changed (109) hide show
  1. package/dist/host/src/acp-settings.js +1 -1
  2. package/dist/host/src/acp-settings.js.map +1 -1
  3. package/dist/host/src/computer-preview.d.ts +1 -9
  4. package/dist/host/src/drivers/acp/antigravity.d.ts +13 -0
  5. package/dist/host/src/drivers/acp/antigravity.js +13 -0
  6. package/dist/host/src/drivers/acp/antigravity.js.map +1 -1
  7. package/dist/host/src/drivers/acp/driver.d.ts +14 -1
  8. package/dist/host/src/drivers/acp/driver.js +45 -3
  9. package/dist/host/src/drivers/acp/driver.js.map +1 -1
  10. package/dist/host/src/drivers/acp/mapper.d.ts +2 -0
  11. package/dist/host/src/drivers/acp/mapper.js +6 -1
  12. package/dist/host/src/drivers/acp/mapper.js.map +1 -1
  13. package/dist/host/src/drivers/acp/questions.js +2 -2
  14. package/dist/host/src/drivers/acp/questions.js.map +1 -1
  15. package/dist/host/src/drivers/claude/app-sessions.d.ts +58 -0
  16. package/dist/host/src/drivers/claude/app-sessions.js +183 -0
  17. package/dist/host/src/drivers/claude/app-sessions.js.map +1 -0
  18. package/dist/host/src/drivers/claude/driver.d.ts +22 -1
  19. package/dist/host/src/drivers/claude/driver.js +99 -11
  20. package/dist/host/src/drivers/claude/driver.js.map +1 -1
  21. package/dist/host/src/drivers/claude/holders.d.ts +3 -1
  22. package/dist/host/src/drivers/claude/holders.js +4 -0
  23. package/dist/host/src/drivers/claude/holders.js.map +1 -1
  24. package/dist/host/src/drivers/claude/routing.d.ts +19 -0
  25. package/dist/host/src/drivers/claude/routing.js +145 -0
  26. package/dist/host/src/drivers/claude/routing.js.map +1 -0
  27. package/dist/host/src/drivers/claude/tasks.d.ts +3 -3
  28. package/dist/host/src/drivers/claude/transcript.d.ts +393 -348
  29. package/dist/host/src/drivers/claude/transcript.js +97 -22
  30. package/dist/host/src/drivers/claude/transcript.js.map +1 -1
  31. package/dist/host/src/drivers/codex/driver.d.ts +15 -0
  32. package/dist/host/src/drivers/codex/driver.js +47 -9
  33. package/dist/host/src/drivers/codex/driver.js.map +1 -1
  34. package/dist/host/src/drivers/codex/mapper.d.ts +41 -7
  35. package/dist/host/src/drivers/codex/mapper.js +167 -26
  36. package/dist/host/src/drivers/codex/mapper.js.map +1 -1
  37. package/dist/host/src/drivers/codex/mcp-apps.d.ts +69 -0
  38. package/dist/host/src/drivers/codex/mcp-apps.js +340 -0
  39. package/dist/host/src/drivers/codex/mcp-apps.js.map +1 -0
  40. package/dist/host/src/drivers/codex/settings.d.ts +13 -0
  41. package/dist/host/src/drivers/codex/settings.js +22 -6
  42. package/dist/host/src/drivers/codex/settings.js.map +1 -1
  43. package/dist/host/src/drivers/types.d.ts +50 -1
  44. package/dist/host/src/fs.d.ts +9 -9
  45. package/dist/host/src/hub.d.ts +118 -81
  46. package/dist/host/src/hub.js +157 -13
  47. package/dist/host/src/hub.js.map +1 -1
  48. package/dist/host/src/input.d.ts +43 -89
  49. package/dist/host/src/input.js +1 -1
  50. package/dist/host/src/input.js.map +1 -1
  51. package/dist/host/src/rpc/mcp.d.ts +8 -0
  52. package/dist/host/src/rpc/mcp.js +33 -0
  53. package/dist/host/src/rpc/mcp.js.map +1 -0
  54. package/dist/host/src/rpc/server.js +16 -2
  55. package/dist/host/src/rpc/server.js.map +1 -1
  56. package/dist/host/src/screen-session.d.ts +3 -17
  57. package/dist/host/src/screen-session.js +2 -2
  58. package/dist/host/src/screen-session.js.map +1 -1
  59. package/dist/host/src/store.d.ts +7 -0
  60. package/dist/host/src/store.js +27 -3
  61. package/dist/host/src/store.js.map +1 -1
  62. package/dist/host/src/terminals.d.ts +12 -6
  63. package/dist/host/src/terminals.js +29 -2
  64. package/dist/host/src/terminals.js.map +1 -1
  65. package/dist/host/src/usage.d.ts +5 -0
  66. package/dist/host/src/usage.js +10 -2
  67. package/dist/host/src/usage.js.map +1 -1
  68. package/dist/host/tsconfig.tsbuildinfo +1 -1
  69. package/dist/wire/src/acp.d.ts +68 -430
  70. package/dist/wire/src/acp.js +5 -5
  71. package/dist/wire/src/acp.js.map +1 -1
  72. package/dist/wire/src/async-questions.d.ts +2 -10
  73. package/dist/wire/src/computer-preview.d.ts +1 -15
  74. package/dist/wire/src/computer-preview.js +1 -1
  75. package/dist/wire/src/computer-preview.js.map +1 -1
  76. package/dist/wire/src/content.d.ts +207 -372
  77. package/dist/wire/src/content.js +4 -4
  78. package/dist/wire/src/content.js.map +1 -1
  79. package/dist/wire/src/editor.d.ts +48 -592
  80. package/dist/wire/src/envelopes.d.ts +53 -0
  81. package/dist/wire/src/envelopes.js +263 -0
  82. package/dist/wire/src/envelopes.js.map +1 -0
  83. package/dist/wire/src/goal.d.ts +15 -48
  84. package/dist/wire/src/index.d.ts +2 -0
  85. package/dist/wire/src/index.js +2 -0
  86. package/dist/wire/src/index.js.map +1 -1
  87. package/dist/wire/src/live-activity.d.ts +83 -212
  88. package/dist/wire/src/live-activity.js +1 -1
  89. package/dist/wire/src/live-activity.js.map +1 -1
  90. package/dist/wire/src/model.d.ts +458 -2281
  91. package/dist/wire/src/model.js +24 -0
  92. package/dist/wire/src/model.js.map +1 -1
  93. package/dist/wire/src/relay.d.ts +41 -267
  94. package/dist/wire/src/relay.js +1 -1
  95. package/dist/wire/src/relay.js.map +1 -1
  96. package/dist/wire/src/rpc-mcp.d.ts +84 -0
  97. package/dist/wire/src/rpc-mcp.js +78 -0
  98. package/dist/wire/src/rpc-mcp.js.map +1 -0
  99. package/dist/wire/src/rpc.d.ts +6891 -31073
  100. package/dist/wire/src/rpc.js +35 -7
  101. package/dist/wire/src/rpc.js.map +1 -1
  102. package/dist/wire/src/side.d.ts +13 -61
  103. package/dist/wire/src/updates.d.ts +1771 -6680
  104. package/dist/wire/src/updates.js +14 -1
  105. package/dist/wire/src/updates.js.map +1 -1
  106. package/dist/wire/src/usage.d.ts +17 -195
  107. package/dist/wire/src/watch.d.ts +223 -1141
  108. package/dist/wire/src/workflow.d.ts +46 -139
  109. package/package.json +4 -4
@@ -1,4 +1,4 @@
1
- import { type GoalChange, type BackgroundTask, type AgentInfo, type ContentBlock, type SessionEvent, type SessionSummary, type SubagentInfo, type QuestionAnswer, type GitInfo, type ProjectSummary, type WorktreeEntry, type SessionNotice, type AcpAgentSettings, type AcpRemoteAgent, type PendingPermissionSummary, type EditorRequest, type SideChat } from "@linkshell/wire";
1
+ import { type GoalChange, type BackgroundTask, type AgentInfo, type ContentBlock, type SessionEvent, type SessionSummary, type SubagentInfo, type QuestionAnswer, type GitInfo, type ProjectSummary, type WorktreeEntry, type SessionNotice, type AcpAgentSettings, type AcpRemoteAgent, type PendingPermissionSummary, type EditorRequest, type SideChat, type MethodResult } from "@linkshell/wire";
2
2
  import type { AcpSettingsStore } from "./acp-settings.js";
3
3
  import type { AgentDriver, DesktopController, DesktopLaunchContext, DriverHost, LaunchSpec } from "./drivers/types.js";
4
4
  import { type HostStore } from "./store.js";
@@ -9,6 +9,17 @@ export interface Subscriber {
9
9
  /** The backlog that follows starts after `startSeq`, not where the subscriber left off. */
10
10
  window?(startSeq: number): void;
11
11
  }
12
+ /**
13
+ * Archived here or in the app, unless it was unarchived here after the app
14
+ * archived it (`local.at` 0: chosen before times were kept, so it never outranks the app).
15
+ */
16
+ export declare function mergedArchive(local: {
17
+ archived: boolean;
18
+ at: number;
19
+ }, app: {
20
+ archived: boolean;
21
+ since: number;
22
+ } | undefined): boolean;
12
23
  /**
13
24
  * The host's source of truth for sessions. Drivers push native events in;
14
25
  * the hub logs them with a per-session seq, keeps summaries current and fans
@@ -77,47 +88,53 @@ export declare class SessionHub {
77
88
  pendingInteractions(): {
78
89
  agent: string;
79
90
  request: {
91
+ requestId: string;
92
+ title: string;
80
93
  options: {
94
+ optionId: string;
81
95
  name: string;
82
96
  kind: "allow_once" | "allow_always" | "reject_once" | "reject_always";
83
- optionId: string;
84
97
  }[];
85
- title: string;
86
- requestId: string;
98
+ childSessionId?: string | undefined;
87
99
  toolCallId?: string | undefined;
100
+ detail?: string | undefined;
88
101
  tool?: {
89
102
  title?: string | undefined;
90
103
  kind?: "search" | "delete" | "other" | "read" | "edit" | "move" | "execute" | "think" | "fetch" | "switch_mode" | undefined;
104
+ rawInput?: unknown;
91
105
  content?: ({
92
106
  type: "content";
93
107
  content: {
94
108
  type: "text";
95
109
  text: string;
96
- annotations?: import("zod").objectOutputType<{
97
- audience: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodArray<import("zod").ZodEnum<["user", "assistant"]>, "many">>>, ("user" | "assistant")[] | undefined, ("user" | "assistant")[] | null | undefined>;
98
- priority: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodNumber>>, number | undefined, number | null | undefined>;
99
- lastModified: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>, string | undefined, string | null | undefined>;
100
- }, import("zod").ZodTypeAny, "passthrough"> | undefined;
110
+ annotations?: {
111
+ [x: string]: unknown;
112
+ audience?: ("user" | "assistant")[] | undefined;
113
+ priority?: number | undefined;
114
+ lastModified?: string | undefined;
115
+ } | undefined;
101
116
  _meta?: Record<string, unknown> | undefined;
102
117
  } | {
103
118
  type: "image";
104
119
  mimeType: string;
105
- annotations?: import("zod").objectOutputType<{
106
- audience: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodArray<import("zod").ZodEnum<["user", "assistant"]>, "many">>>, ("user" | "assistant")[] | undefined, ("user" | "assistant")[] | null | undefined>;
107
- priority: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodNumber>>, number | undefined, number | null | undefined>;
108
- lastModified: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>, string | undefined, string | null | undefined>;
109
- }, import("zod").ZodTypeAny, "passthrough"> | undefined;
120
+ annotations?: {
121
+ [x: string]: unknown;
122
+ audience?: ("user" | "assistant")[] | undefined;
123
+ priority?: number | undefined;
124
+ lastModified?: string | undefined;
125
+ } | undefined;
110
126
  _meta?: Record<string, unknown> | undefined;
111
127
  data?: string | undefined;
112
128
  uri?: string | undefined;
113
129
  } | {
114
130
  type: "audio";
115
131
  mimeType: string;
116
- annotations?: import("zod").objectOutputType<{
117
- audience: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodArray<import("zod").ZodEnum<["user", "assistant"]>, "many">>>, ("user" | "assistant")[] | undefined, ("user" | "assistant")[] | null | undefined>;
118
- priority: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodNumber>>, number | undefined, number | null | undefined>;
119
- lastModified: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>, string | undefined, string | null | undefined>;
120
- }, import("zod").ZodTypeAny, "passthrough"> | undefined;
132
+ annotations?: {
133
+ [x: string]: unknown;
134
+ audience?: ("user" | "assistant")[] | undefined;
135
+ priority?: number | undefined;
136
+ lastModified?: string | undefined;
137
+ } | undefined;
121
138
  _meta?: Record<string, unknown> | undefined;
122
139
  data?: string | undefined;
123
140
  uri?: string | undefined;
@@ -125,69 +142,66 @@ export declare class SessionHub {
125
142
  type: "resource";
126
143
  resource: {
127
144
  uri: string;
128
- text?: string | undefined;
129
- _meta?: Record<string, unknown> | undefined;
130
145
  mimeType?: string | undefined;
146
+ text?: string | undefined;
131
147
  blob?: string | undefined;
132
148
  assetUri?: string | undefined;
149
+ _meta?: Record<string, unknown> | undefined;
133
150
  };
134
- annotations?: import("zod").objectOutputType<{
135
- audience: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodArray<import("zod").ZodEnum<["user", "assistant"]>, "many">>>, ("user" | "assistant")[] | undefined, ("user" | "assistant")[] | null | undefined>;
136
- priority: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodNumber>>, number | undefined, number | null | undefined>;
137
- lastModified: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>, string | undefined, string | null | undefined>;
138
- }, import("zod").ZodTypeAny, "passthrough"> | undefined;
151
+ annotations?: {
152
+ [x: string]: unknown;
153
+ audience?: ("user" | "assistant")[] | undefined;
154
+ priority?: number | undefined;
155
+ lastModified?: string | undefined;
156
+ } | undefined;
139
157
  _meta?: Record<string, unknown> | undefined;
140
158
  } | {
141
159
  type: "resource_link";
142
160
  uri: string;
143
161
  name: string;
144
- annotations?: import("zod").objectOutputType<{
145
- audience: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodArray<import("zod").ZodEnum<["user", "assistant"]>, "many">>>, ("user" | "assistant")[] | undefined, ("user" | "assistant")[] | null | undefined>;
146
- priority: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodNumber>>, number | undefined, number | null | undefined>;
147
- lastModified: import("zod").ZodEffects<import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>, string | undefined, string | null | undefined>;
148
- }, import("zod").ZodTypeAny, "passthrough"> | undefined;
162
+ kind?: "file" | "skill" | "agent" | undefined;
163
+ annotations?: {
164
+ [x: string]: unknown;
165
+ audience?: ("user" | "assistant")[] | undefined;
166
+ priority?: number | undefined;
167
+ lastModified?: string | undefined;
168
+ } | undefined;
149
169
  _meta?: Record<string, unknown> | undefined;
150
- mimeType?: string | undefined;
151
170
  title?: string | undefined;
152
171
  description?: string | undefined;
172
+ mimeType?: string | undefined;
153
173
  size?: number | undefined;
154
- kind?: "file" | "skill" | "agent" | undefined;
155
174
  };
156
175
  } | {
157
- path: string;
158
176
  type: "diff";
177
+ path: string;
159
178
  newText: string;
160
179
  oldText?: string | null | undefined;
161
180
  } | {
162
- path: string;
163
181
  type: "patch";
164
- diff: string;
182
+ path: string;
165
183
  change: "add" | "delete" | "update";
184
+ diff: string;
166
185
  movePath?: string | undefined;
167
186
  } | {
168
187
  type: "terminal";
169
188
  terminalId: string;
170
189
  })[] | undefined;
171
- rawInput?: unknown;
172
190
  locations?: {
173
191
  path: string;
174
192
  line?: number | undefined;
175
193
  }[] | undefined;
176
194
  } | undefined;
177
- detail?: string | undefined;
195
+ url?: {
196
+ url: string;
197
+ elicitationId: string;
198
+ } | undefined;
178
199
  questions?: {
200
+ id: string;
179
201
  text: string;
180
202
  kind: "text" | "choice" | "choices";
181
- id: string;
182
- secret?: boolean | undefined;
183
- options?: {
184
- value: string;
185
- label: string;
186
- description?: string | undefined;
187
- }[] | undefined;
188
- other?: boolean | undefined;
189
203
  constraints?: {
190
- type: "string" | "number" | "boolean" | "integer" | "array";
204
+ type: "string" | "number" | "boolean" | "array" | "integer";
191
205
  minimum?: number | undefined;
192
206
  maximum?: number | undefined;
193
207
  minLength?: number | undefined;
@@ -199,41 +213,24 @@ export declare class SessionHub {
199
213
  } | undefined;
200
214
  defaults?: string[] | undefined;
201
215
  header?: string | undefined;
216
+ options?: {
217
+ value: string;
218
+ label: string;
219
+ description?: string | undefined;
220
+ }[] | undefined;
221
+ other?: boolean | undefined;
222
+ secret?: boolean | undefined;
202
223
  required?: boolean | undefined;
203
224
  }[] | undefined;
204
- childSessionId?: string | undefined;
205
- url?: {
206
- url: string;
207
- elicitationId: string;
208
- } | undefined;
209
225
  };
210
226
  }[];
211
227
  editor(input: EditorRequest): Promise<{
228
+ editorId?: string | undefined;
212
229
  path?: string | undefined;
213
230
  text?: string | undefined;
214
231
  version?: number | undefined;
215
- preview?: {
216
- path: string;
217
- kind: "edit" | "jump" | "searchAndReplace";
218
- id: string;
219
- before: string;
220
- after: string;
221
- position?: {
222
- line: number;
223
- character: number;
224
- } | undefined;
225
- } | undefined;
226
- position?: {
227
- line: number;
228
- character: number;
229
- } | undefined;
230
- editorId?: string | undefined;
231
232
  suggestions?: ({
232
- uri: string;
233
- kind: "edit";
234
- id: string;
235
233
  edits: {
236
- newText: string;
237
234
  range: {
238
235
  start: {
239
236
  line: number;
@@ -244,36 +241,55 @@ export declare class SessionHub {
244
241
  character: number;
245
242
  };
246
243
  };
244
+ newText: string;
247
245
  }[];
246
+ id: string;
247
+ uri: string;
248
+ kind: "edit";
248
249
  cursorPosition?: {
249
250
  line: number;
250
251
  character: number;
251
252
  } | null | undefined;
252
253
  } | {
253
- uri: string;
254
- kind: "jump";
255
- id: string;
256
254
  position: {
257
255
  line: number;
258
256
  character: number;
259
257
  };
260
- } | {
261
- uri: string;
262
- kind: "rename";
263
258
  id: string;
259
+ uri: string;
260
+ kind: "jump";
261
+ } | {
264
262
  position: {
265
263
  line: number;
266
264
  character: number;
267
265
  };
268
266
  newName: string;
267
+ id: string;
268
+ uri: string;
269
+ kind: "rename";
269
270
  } | {
270
- replace: string;
271
271
  search: string;
272
+ replace: string;
273
+ id: string;
272
274
  uri: string;
273
275
  kind: "searchAndReplace";
274
- id: string;
275
276
  isRegex?: boolean | null | undefined;
276
277
  })[] | undefined;
278
+ preview?: {
279
+ id: string;
280
+ path: string;
281
+ before: string;
282
+ after: string;
283
+ kind: "edit" | "jump" | "searchAndReplace";
284
+ position?: {
285
+ line: number;
286
+ character: number;
287
+ } | undefined;
288
+ } | undefined;
289
+ position?: {
290
+ line: number;
291
+ character: number;
292
+ } | undefined;
277
293
  }>;
278
294
  configureAcp(agent: string, settings: AcpAgentSettings, sessionId?: string): Promise<void>;
279
295
  authenticate(agent: string, methodId?: string, logout?: boolean): Promise<{
@@ -289,9 +305,9 @@ export declare class SessionHub {
289
305
  baseUrl?: string;
290
306
  headers?: Record<string, string>;
291
307
  }): Promise<{
292
- required: boolean;
293
308
  providerId: string;
294
309
  supported: string[];
310
+ required: boolean;
295
311
  current?: {
296
312
  apiType: string;
297
313
  baseUrl: string;
@@ -315,6 +331,11 @@ export declare class SessionHub {
315
331
  /** Asked for every project each time the list is: remembered for a moment. */
316
332
  private branchOf;
317
333
  getSession(sessionId: string): SessionSummary;
334
+ /** A session and its agent's driver, unchecked: for what one driver offers itself (`mcp.*`), which says what it needs of it. */
335
+ sessionDriver(sessionId: string): {
336
+ session: SessionSummary;
337
+ driver: AgentDriver | undefined;
338
+ };
318
339
  /** Adds the live-only fields (current activity, first pending permission) to a stored summary. */
319
340
  /** Marks a session that works in one of LinkShell's worktrees. */
320
341
  private withWorktree;
@@ -322,7 +343,20 @@ export declare class SessionHub {
322
343
  private worktrees;
323
344
  private readonly pendingQuestions;
324
345
  private questionsOf;
346
+ /** Per session, the session it was forked from (null: none), read from the store once. */
347
+ private readonly forkOrigins;
348
+ /** Per session, the sessions whose lineage names it: their rows change when it comes or goes. */
349
+ private readonly related;
350
+ private forkOrigin;
351
+ /** Where the session came from, naming only sessions this host has: a list can't nest under one it doesn't show. */
352
+ private withLineage;
353
+ /** As the Claude app nests: a session still in use stands alone when the one it came from is archived. */
354
+ private firstKnown;
355
+ /** The session came, went or was (un)archived: the rows that name it as parent or origin say so now. */
356
+ private announceRelated;
325
357
  private decorate;
358
+ /** What a new session of `agent` could run on (see `agents.models`): nothing from a driver that can't tell before a session exists. */
359
+ agentModels(agent: string, cwd?: string): Promise<MethodResult<"agents.models">>;
326
360
  createSession(input: {
327
361
  agent: string;
328
362
  cwd: string;
@@ -416,6 +450,9 @@ export declare class SessionHub {
416
450
  private sendHeld;
417
451
  onRemoved(listener: (sessionId: string) => void): () => void;
418
452
  archive(sessionId: string, archived: boolean): Promise<SessionSummary>;
453
+ /** Folds in the archive state of the agent's app that LinkShell only reads (see `mergedArchive`). */
454
+ private reconcileAppArchive;
455
+ private localArchive;
419
456
  rename(sessionId: string, title: string): Promise<SessionSummary>;
420
457
  /**
421
458
  * Deletes a session. A worktree only it used goes too when nothing in it
@@ -1,4 +1,4 @@
1
- import { RpcError, questionReplies, questionAnswerError, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
1
+ import { RpcError, questionReplies, readUserText, questionAnswerError, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
2
2
  import { inOrder } from "./drivers/acp/driver.js";
3
3
  import { canForkNatively } from "./drivers/behavior.js";
4
4
  import { homedir } from "node:os";
@@ -57,6 +57,24 @@ function worktreeLossMessage(state) {
57
57
  const RENAMED = "renamed";
58
58
  /** Driver-state key: the conversation a forked session's agent is still to be told, with its first message. */
59
59
  const CARRY = "carry";
60
+ /** Driver-state key: the session a session was forked from (made here, or as its agent recorded it). */
61
+ const FORKED_FROM = "forkedFrom";
62
+ /**
63
+ * Driver-state keys, for agents with an app LinkShell only reads (`appArchive`):
64
+ * LinkShell's own archive choice and when it was made, and the app's state as
65
+ * last seen and since when.
66
+ */
67
+ const LOCAL_ARCHIVE = "archive-local";
68
+ const APP_ARCHIVE = "archive-app";
69
+ /**
70
+ * Archived here or in the app, unless it was unarchived here after the app
71
+ * archived it (`local.at` 0: chosen before times were kept, so it never outranks the app).
72
+ */
73
+ export function mergedArchive(local, app) {
74
+ if (local.archived)
75
+ return true;
76
+ return !!app?.archived && !(local.at > app.since);
77
+ }
60
78
  /** A returning client catches up from where it was, unless it missed more than this. */
61
79
  const CATCH_UP = { events: 800, bytes: 4 * 1024 * 1024 };
62
80
  /** State a client needs from before its window: the latest of each applies. */
@@ -172,6 +190,17 @@ export class SessionHub {
172
190
  this.emitSummary(stored);
173
191
  },
174
192
  removed: (agent, nativeId) => this.forget(sessionIdFor(agent, nativeId)),
193
+ nativeRecordsChanged: (agent, nativeIds) => {
194
+ for (const nativeId of nativeIds) {
195
+ const stored = this.store.getSession(sessionIdFor(agent, nativeId));
196
+ if (!stored)
197
+ continue;
198
+ const merged = this.reconcileAppArchive(stored);
199
+ this.emitSummary(merged);
200
+ if (merged.archived !== stored.archived)
201
+ this.announceRelated(merged.id);
202
+ }
203
+ },
175
204
  desktop: (agent, nativeId) => this.desktops.get(sessionIdFor(agent, nativeId)),
176
205
  state: (agent, nativeId) => {
177
206
  const sessionId = sessionIdFor(agent, nativeId);
@@ -180,6 +209,10 @@ export class SessionHub {
180
209
  set: (key, value) => this.store.setDriverState(sessionId, key, value),
181
210
  };
182
211
  },
212
+ agentState: (agent) => ({
213
+ get: (key) => this.store.getAgentState(agent, key),
214
+ set: (key, value) => this.store.setAgentState(agent, key, value),
215
+ }),
183
216
  log: (message) => this.log(message),
184
217
  };
185
218
  constructor(store, drivers, log = () => { },
@@ -291,6 +324,7 @@ export class SessionHub {
291
324
  else if (availability === "available" && this.store.isNativeMissing(session.id)) {
292
325
  this.store.setNativeMissing(session.id, false);
293
326
  this.emitSummary(session);
327
+ this.announceRelated(session.id);
294
328
  }
295
329
  }
296
330
  hideMissingSession(session) {
@@ -305,6 +339,7 @@ export class SessionHub {
305
339
  driver?.watched?.(session.nativeId, false);
306
340
  this.live.delete(session.id);
307
341
  this.emitRemoved(session.id);
342
+ this.announceRelated(session.id);
308
343
  void driver?.detach(session.nativeId).catch((error) => this.log(`[hub] detach missing ${session.id}: ${String(error)}`));
309
344
  }
310
345
  async stop() {
@@ -425,6 +460,8 @@ export class SessionHub {
425
460
  await existing.configureAcp?.(services.settings.settings(id));
426
461
  await existing.stop();
427
462
  this.drivers.delete(id);
463
+ // What the old definition's agent reported (its models) needn't hold for the new one.
464
+ this.store.clearAgentState(id);
428
465
  }
429
466
  if (save && driver) {
430
467
  services.settings.saveAgent(save);
@@ -493,6 +530,11 @@ export class SessionHub {
493
530
  throw RpcError.app("not_found", "这个会话的原始记录已不存在,已从列表隐藏;本地缓存仍保留。");
494
531
  return this.decorate(summary);
495
532
  }
533
+ /** A session and its agent's driver, unchecked: for what one driver offers itself (`mcp.*`), which says what it needs of it. */
534
+ sessionDriver(sessionId) {
535
+ const session = this.getSession(sessionId);
536
+ return { session, driver: this.drivers.get(session.agent) };
537
+ }
496
538
  /** Adds the live-only fields (current activity, first pending permission) to a stored summary. */
497
539
  /** Marks a session that works in one of LinkShell's worktrees. */
498
540
  withWorktree(summary) {
@@ -512,8 +554,55 @@ export class SessionHub {
512
554
  }
513
555
  return questions;
514
556
  }
557
+ /** Per session, the session it was forked from (null: none), read from the store once. */
558
+ forkOrigins = new Map();
559
+ /** Per session, the sessions whose lineage names it: their rows change when it comes or goes. */
560
+ related = new Map();
561
+ forkOrigin(sessionId) {
562
+ let origin = this.forkOrigins.get(sessionId);
563
+ if (origin === undefined) {
564
+ origin = this.store.getDriverState(sessionId, FORKED_FROM) ?? null;
565
+ this.forkOrigins.set(sessionId, origin);
566
+ }
567
+ return origin ?? undefined;
568
+ }
569
+ /** Where the session came from, naming only sessions this host has: a list can't nest under one it doesn't show. */
570
+ withLineage(summary) {
571
+ const native = this.drivers.get(summary.agent)?.lineage?.(summary.nativeId);
572
+ const ids = (nativeIds) => nativeIds?.map((nativeId) => sessionIdFor(summary.agent, nativeId)) ?? [];
573
+ const origin = this.forkOrigin(summary.id);
574
+ const parentId = this.firstKnown(summary, ids(native?.parent));
575
+ const forkedFrom = this.firstKnown(summary, [...(origin ? [origin] : []), ...ids(native?.forkedFrom)]);
576
+ const appId = native?.appId;
577
+ if (!parentId && !forkedFrom && !appId)
578
+ return summary;
579
+ return { ...summary, ...(parentId ? { parentId } : {}), ...(forkedFrom ? { forkedFrom } : {}), ...(appId ? { appId } : {}) };
580
+ }
581
+ /** As the Claude app nests: a session still in use stands alone when the one it came from is archived. */
582
+ firstKnown(summary, candidates) {
583
+ let found;
584
+ for (const candidate of candidates) {
585
+ if (candidate === summary.id)
586
+ continue;
587
+ let children = this.related.get(candidate);
588
+ if (!children)
589
+ this.related.set(candidate, (children = new Set()));
590
+ children.add(summary.id);
591
+ if (found)
592
+ continue;
593
+ const other = this.store.getSession(candidate);
594
+ if (other && !(other.archived && !summary.archived) && !this.store.isNativeMissing(candidate))
595
+ found = candidate;
596
+ }
597
+ return found;
598
+ }
599
+ /** The session came, went or was (un)archived: the rows that name it as parent or origin say so now. */
600
+ announceRelated(sessionId) {
601
+ for (const child of this.related.get(sessionId) ?? [])
602
+ this.announce(child);
603
+ }
515
604
  decorate(summary) {
516
- summary = { ...summary, asyncQuestions: this.questionsOf(summary.id) };
605
+ summary = this.withLineage({ ...summary, asyncQuestions: this.questionsOf(summary.id) });
517
606
  const live = this.live.get(summary.id);
518
607
  if (!live)
519
608
  return this.withWorktree(summary);
@@ -561,6 +650,11 @@ export class SessionHub {
561
650
  }
562
651
  return decorated;
563
652
  }
653
+ /** What a new session of `agent` could run on (see `agents.models`): nothing from a driver that can't tell before a session exists. */
654
+ async agentModels(agent, cwd) {
655
+ const driver = this.requireDriver(agent);
656
+ return (await driver.listModels?.(cwd)) ?? { models: [] };
657
+ }
564
658
  async createSession(input) {
565
659
  const driver = this.requireDriver(input.agent);
566
660
  const worktree = input.worktree ? await this.newWorktree(input.cwd, promptLabel(input.prompt)) : undefined;
@@ -615,7 +709,7 @@ export class SessionHub {
615
709
  }
616
710
  if (!discovered)
617
711
  return await this.forkByReplay(summary, driver, cwd, cut);
618
- this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title });
712
+ this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title, forkedFrom: summary.nativeId });
619
713
  return this.getSession(sessionIdFor(driver.id, discovered.nativeId));
620
714
  }
621
715
  catch (error) {
@@ -633,7 +727,7 @@ export class SessionHub {
633
727
  const start = this.store.pageStart(summary.id, cut);
634
728
  const events = this.store.readEvents(summary.id, start, PAGE.maxEvents + 1, cut).filter((event) => isConversation(event.update));
635
729
  const discovered = await driver.createSession({ cwd });
636
- this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title });
730
+ this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title, forkedFrom: summary.nativeId });
637
731
  const forkId = sessionIdFor(driver.id, discovered.nativeId);
638
732
  await this.ensureAttached(forkId);
639
733
  const live = this.liveFor(forkId);
@@ -1185,10 +1279,40 @@ export class SessionHub {
1185
1279
  const driver = this.drivers.get(summary.agent);
1186
1280
  if (driver?.archive)
1187
1281
  await driver.archive(summary.nativeId, archived);
1188
- const updated = this.store.patchSession(sessionId, { archived });
1282
+ // Kept apart from the app's, with when: unarchiving here outranks an older archive there.
1283
+ if (driver?.appArchive)
1284
+ this.store.setDriverState(sessionId, LOCAL_ARCHIVE, JSON.stringify({ archived, at: Date.now() }));
1285
+ const updated = this.reconcileAppArchive(this.store.patchSession(sessionId, { archived }));
1189
1286
  this.emitSummary(updated);
1287
+ this.announceRelated(sessionId);
1190
1288
  return this.decorate(updated);
1191
1289
  }
1290
+ /** Folds in the archive state of the agent's app that LinkShell only reads (see `mergedArchive`). */
1291
+ reconcileAppArchive(summary) {
1292
+ const reported = this.drivers.get(summary.agent)?.appArchive?.(summary.nativeId);
1293
+ // Not in that app (or not read yet): the flag is LinkShell's alone, or stays as last merged.
1294
+ if (!reported)
1295
+ return summary;
1296
+ const saved = this.store.getDriverState(summary.id, APP_ARCHIVE);
1297
+ const seen = saved ? JSON.parse(saved) : undefined;
1298
+ if (!seen && !reported.archived)
1299
+ return summary;
1300
+ // Only a change counts as the app archiving it: the app rewrites its record for much else.
1301
+ const app = seen?.archived === reported.archived ? seen : { archived: reported.archived, since: reported.at };
1302
+ if (app !== seen)
1303
+ this.store.setDriverState(summary.id, APP_ARCHIVE, JSON.stringify(app));
1304
+ const archived = mergedArchive(this.localArchive(summary), app);
1305
+ return archived === summary.archived ? summary : this.store.patchSession(summary.id, { archived });
1306
+ }
1307
+ localArchive(summary) {
1308
+ const saved = this.store.getDriverState(summary.id, LOCAL_ARCHIVE);
1309
+ if (saved)
1310
+ return JSON.parse(saved);
1311
+ // Until the app's state is first merged in, the flag is LinkShell's own choice.
1312
+ const local = { archived: summary.archived, at: 0 };
1313
+ this.store.setDriverState(summary.id, LOCAL_ARCHIVE, JSON.stringify(local));
1314
+ return local;
1315
+ }
1192
1316
  async rename(sessionId, title) {
1193
1317
  const summary = this.getSession(sessionId);
1194
1318
  const name = compact(title, TITLE_LENGTH);
@@ -1249,7 +1373,9 @@ export class SessionHub {
1249
1373
  this.previews.forget(sessionId);
1250
1374
  this.sides.forget(sessionId);
1251
1375
  this.store.removeSession(sessionId);
1376
+ this.forkOrigins.delete(sessionId);
1252
1377
  this.emitRemoved(sessionId);
1378
+ this.announceRelated(sessionId);
1253
1379
  }
1254
1380
  emitRemoved(sessionId) {
1255
1381
  for (const listener of this.removedListeners) {
@@ -1431,10 +1557,19 @@ export class SessionHub {
1431
1557
  // Items without a recorded time get the time of the one before, never "now".
1432
1558
  // (An agent that only says when a session last changed gives that as its creation too.)
1433
1559
  let ts = Math.min(summary.createdAt, history.find((item) => item.ts !== undefined)?.ts ?? summary.createdAt);
1434
- for (const item of history) {
1560
+ // The log only grows at its end. A message before what it already has is one an earlier
1561
+ // reading skipped (a reader that now shows more), not something that happened since:
1562
+ // added now it would land at the bottom, out of place, so it is only marked as seen.
1563
+ const logged = history.map((item) => this.store.isItemLogged(sessionId, item.itemId));
1564
+ const lastLogged = logged.lastIndexOf(true);
1565
+ for (const [index, item] of history.entries()) {
1435
1566
  ts = Math.min(Math.max(item.ts ?? ts, ts), summary.updatedAt);
1436
- if (this.store.isItemLogged(sessionId, item.itemId))
1567
+ if (logged[index])
1437
1568
  continue;
1569
+ if (index < lastLogged && item.updates.every((update) => update.sessionUpdate === "user_message_chunk")) {
1570
+ this.store.markItemLogged(sessionId, item.itemId);
1571
+ continue;
1572
+ }
1438
1573
  for (const update of item.updates)
1439
1574
  this.commit(sessionId, update, undefined, ts);
1440
1575
  this.store.markItemLogged(sessionId, item.itemId);
@@ -1500,9 +1635,9 @@ export class SessionHub {
1500
1635
  updatedAt: session.updatedAt,
1501
1636
  });
1502
1637
  const { created } = stored;
1503
- const summary = session.archived !== undefined && session.archived !== stored.summary.archived
1638
+ const summary = this.reconcileAppArchive(session.archived !== undefined && session.archived !== stored.summary.archived
1504
1639
  ? this.store.patchSession(stored.summary.id, { archived: session.archived })
1505
- : stored.summary;
1640
+ : stored.summary);
1506
1641
  const changed = !!before &&
1507
1642
  (before.title !== summary.title ||
1508
1643
  before.state !== summary.state ||
@@ -1517,8 +1652,16 @@ export class SessionHub {
1517
1652
  if (questionsChanged)
1518
1653
  this.store.setDriverState(summary.id, "asyncQuestions", JSON.stringify(session.asyncQuestions));
1519
1654
  }
1520
- if (created || changed || questionsChanged || restored)
1655
+ const origin = session.forkedFrom === undefined ? undefined : sessionIdFor(agent, session.forkedFrom);
1656
+ const forked = origin !== undefined && origin !== summary.id && origin !== this.forkOrigin(summary.id);
1657
+ if (forked) {
1658
+ this.store.setDriverState(summary.id, FORKED_FROM, origin);
1659
+ this.forkOrigins.set(summary.id, origin);
1660
+ }
1661
+ if (created || changed || questionsChanged || restored || forked)
1521
1662
  this.emitSummary(summary);
1663
+ if (created || restored || before?.archived !== summary.archived)
1664
+ this.announceRelated(summary.id);
1522
1665
  }
1523
1666
  ingest(sessionId, update, itemId, ts) {
1524
1667
  if (this.store.isNativeMissing(sessionId))
@@ -1595,8 +1738,9 @@ export class SessionHub {
1595
1738
  if (update.parentToolCallId || !update.content)
1596
1739
  break;
1597
1740
  const text = update.content.filter((block) => block.type === "text").map((block) => block.text).join("");
1598
- if (update.role === "user" && !before.title && text.trim())
1599
- patch.title = compact(text, TITLE_LENGTH);
1741
+ // A title is the person's words, not the context their client or Claude wrapped around them.
1742
+ if (update.role === "user" && !before.title && readUserText(text).text.trim())
1743
+ patch.title = compact(readUserText(text).text, TITLE_LENGTH);
1600
1744
  if (update.role === "agent" && text.trim())
1601
1745
  patch.preview = compact(text, PREVIEW_LENGTH);
1602
1746
  if (live.turnActive && update.role === "agent")
@@ -1604,7 +1748,7 @@ export class SessionHub {
1604
1748
  break;
1605
1749
  }
1606
1750
  case "user_message_chunk": {
1607
- const text = update.content.type === "text" ? update.content.text : "";
1751
+ const text = update.content.type === "text" ? readUserText(update.content.text).text : "";
1608
1752
  if (text && !before.title)
1609
1753
  patch.title = compact(text, TITLE_LENGTH);
1610
1754
  if (text && !before.preview)