@lapis-notes/ai 0.1.0

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 (266) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/LICENSE.md +8 -0
  3. package/README.md +20 -0
  4. package/dist/ai-plugin.d.ts +88 -0
  5. package/dist/ai-plugin.js +742 -0
  6. package/dist/bootstrap/build.d.ts +15 -0
  7. package/dist/bootstrap/build.js +28 -0
  8. package/dist/bootstrap/folder-instructions.d.ts +4 -0
  9. package/dist/bootstrap/folder-instructions.js +49 -0
  10. package/dist/bootstrap/format.d.ts +2 -0
  11. package/dist/bootstrap/format.js +46 -0
  12. package/dist/bootstrap/instructions.d.ts +1 -0
  13. package/dist/bootstrap/instructions.js +8 -0
  14. package/dist/bootstrap/types.d.ts +36 -0
  15. package/dist/bootstrap/types.js +3 -0
  16. package/dist/catalog/ai-catalog-panel.svelte +742 -0
  17. package/dist/catalog/ai-catalog-panel.svelte.d.ts +13 -0
  18. package/dist/catalog/ai-catalog-view-type.d.ts +1 -0
  19. package/dist/catalog/ai-catalog-view-type.js +1 -0
  20. package/dist/catalog/ai-catalog-view.d.ts +22 -0
  21. package/dist/catalog/ai-catalog-view.js +48 -0
  22. package/dist/catalog/filter.d.ts +3 -0
  23. package/dist/catalog/filter.js +24 -0
  24. package/dist/catalog/inventory.d.ts +21 -0
  25. package/dist/catalog/inventory.js +255 -0
  26. package/dist/catalog/tree.d.ts +12 -0
  27. package/dist/catalog/tree.js +60 -0
  28. package/dist/catalog/types.d.ts +44 -0
  29. package/dist/catalog/types.js +1 -0
  30. package/dist/chat/ai-approval-card.svelte +54 -0
  31. package/dist/chat/ai-approval-card.svelte.d.ts +9 -0
  32. package/dist/chat/ai-chat-panel.svelte +1288 -0
  33. package/dist/chat/ai-chat-panel.svelte.d.ts +50 -0
  34. package/dist/chat/ai-inventory-result.css +55 -0
  35. package/dist/chat/ai-inventory-result.svelte +63 -0
  36. package/dist/chat/ai-inventory-result.svelte.d.ts +5 -0
  37. package/dist/chat/ai-question-card.svelte +132 -0
  38. package/dist/chat/ai-question-card.svelte.d.ts +9 -0
  39. package/dist/chat/ai-view-bootstrap.d.ts +13 -0
  40. package/dist/chat/ai-view-bootstrap.js +86 -0
  41. package/dist/chat/ai-view-panel.svelte +89 -0
  42. package/dist/chat/ai-view-panel.svelte.d.ts +13 -0
  43. package/dist/chat/ai-view-type.d.ts +1 -0
  44. package/dist/chat/ai-view-type.js +1 -0
  45. package/dist/chat/ai-view.d.ts +60 -0
  46. package/dist/chat/ai-view.js +121 -0
  47. package/dist/chat/chat-controller.svelte.d.ts +82 -0
  48. package/dist/chat/chat-controller.svelte.js +1926 -0
  49. package/dist/chat/chat-items.d.ts +88 -0
  50. package/dist/chat/chat-items.js +9 -0
  51. package/dist/chat/chat-mentions.d.ts +15 -0
  52. package/dist/chat/chat-mentions.js +77 -0
  53. package/dist/chat/chat-result-views.d.ts +5 -0
  54. package/dist/chat/chat-result-views.js +15 -0
  55. package/dist/chat/chat-session.d.ts +28 -0
  56. package/dist/chat/chat-session.js +50 -0
  57. package/dist/chat/chat-status.d.ts +1 -0
  58. package/dist/chat/chat-status.js +3 -0
  59. package/dist/chat/chat-time.d.ts +18 -0
  60. package/dist/chat/chat-time.js +90 -0
  61. package/dist/chat/chat-tool-display.d.ts +17 -0
  62. package/dist/chat/chat-tool-display.js +213 -0
  63. package/dist/chat/chat-tool-identity.d.ts +4 -0
  64. package/dist/chat/chat-tool-identity.js +42 -0
  65. package/dist/chat/chat-trace.d.ts +6 -0
  66. package/dist/chat/chat-trace.js +256 -0
  67. package/dist/chat/live-runtime-unavailable.d.ts +1 -0
  68. package/dist/chat/live-runtime-unavailable.js +1 -0
  69. package/dist/commands/agent.d.ts +8 -0
  70. package/dist/commands/agent.js +38 -0
  71. package/dist/commands/catalog.d.ts +18 -0
  72. package/dist/commands/catalog.js +115 -0
  73. package/dist/commands/discovery.d.ts +36 -0
  74. package/dist/commands/discovery.js +151 -0
  75. package/dist/commands/groups.d.ts +16 -0
  76. package/dist/commands/groups.js +121 -0
  77. package/dist/commands/inspect.d.ts +18 -0
  78. package/dist/commands/inspect.js +29 -0
  79. package/dist/commands/markdown.d.ts +21 -0
  80. package/dist/commands/markdown.js +117 -0
  81. package/dist/commands/parser.d.ts +4 -0
  82. package/dist/commands/parser.js +24 -0
  83. package/dist/commands/reserved.d.ts +4 -0
  84. package/dist/commands/reserved.js +107 -0
  85. package/dist/commands/router.d.ts +17 -0
  86. package/dist/commands/router.js +170 -0
  87. package/dist/commands/seed.d.ts +4 -0
  88. package/dist/commands/seed.js +8 -0
  89. package/dist/commands/types.d.ts +54 -0
  90. package/dist/commands/types.js +1 -0
  91. package/dist/commands/user-agents-node.d.ts +3 -0
  92. package/dist/commands/user-agents-node.js +52 -0
  93. package/dist/commands/user-agents.d.ts +22 -0
  94. package/dist/commands/user-agents.js +40 -0
  95. package/dist/conversations/approval-grants.d.ts +20 -0
  96. package/dist/conversations/approval-grants.js +75 -0
  97. package/dist/conversations/binding-state.d.ts +6 -0
  98. package/dist/conversations/binding-state.js +35 -0
  99. package/dist/conversations/context-handoff.d.ts +25 -0
  100. package/dist/conversations/context-handoff.js +207 -0
  101. package/dist/conversations/conversation-index-coordinator.d.ts +18 -0
  102. package/dist/conversations/conversation-index-coordinator.js +81 -0
  103. package/dist/conversations/conversation-index.d.ts +16 -0
  104. package/dist/conversations/conversation-index.js +125 -0
  105. package/dist/conversations/conversation-locator.d.ts +2 -0
  106. package/dist/conversations/conversation-locator.js +20 -0
  107. package/dist/conversations/conversation-palette.d.ts +18 -0
  108. package/dist/conversations/conversation-palette.js +29 -0
  109. package/dist/conversations/conversation-repository.d.ts +39 -0
  110. package/dist/conversations/conversation-repository.js +213 -0
  111. package/dist/conversations/handoff-summary.d.ts +48 -0
  112. package/dist/conversations/handoff-summary.js +269 -0
  113. package/dist/conversations/hashes.d.ts +5 -0
  114. package/dist/conversations/hashes.js +40 -0
  115. package/dist/conversations/history-creation-scope.d.ts +12 -0
  116. package/dist/conversations/history-creation-scope.js +44 -0
  117. package/dist/conversations/jsonl.d.ts +10 -0
  118. package/dist/conversations/jsonl.js +41 -0
  119. package/dist/conversations/memory-transcript-store.d.ts +15 -0
  120. package/dist/conversations/memory-transcript-store.js +119 -0
  121. package/dist/conversations/paths.d.ts +14 -0
  122. package/dist/conversations/paths.js +92 -0
  123. package/dist/conversations/redaction.d.ts +13 -0
  124. package/dist/conversations/redaction.js +94 -0
  125. package/dist/conversations/reveal-scope.d.ts +3 -0
  126. package/dist/conversations/reveal-scope.js +8 -0
  127. package/dist/conversations/scope-resolver.d.ts +11 -0
  128. package/dist/conversations/scope-resolver.js +18 -0
  129. package/dist/conversations/scope-tree.d.ts +10 -0
  130. package/dist/conversations/scope-tree.js +71 -0
  131. package/dist/conversations/transcript-projection.d.ts +9 -0
  132. package/dist/conversations/transcript-projection.js +409 -0
  133. package/dist/conversations/transcript-store.d.ts +24 -0
  134. package/dist/conversations/transcript-store.js +27 -0
  135. package/dist/conversations/types.d.ts +224 -0
  136. package/dist/conversations/types.js +12 -0
  137. package/dist/conversations/validation.d.ts +4 -0
  138. package/dist/conversations/validation.js +383 -0
  139. package/dist/conversations/vault-transcript-store.d.ts +22 -0
  140. package/dist/conversations/vault-transcript-store.js +219 -0
  141. package/dist/core/event-queue.d.ts +6 -0
  142. package/dist/core/event-queue.js +39 -0
  143. package/dist/core/types.d.ts +241 -0
  144. package/dist/core/types.js +50 -0
  145. package/dist/history/ai-history-panel.svelte +937 -0
  146. package/dist/history/ai-history-panel.svelte.d.ts +17 -0
  147. package/dist/history/ai-history-view-type.d.ts +1 -0
  148. package/dist/history/ai-history-view-type.js +1 -0
  149. package/dist/history/ai-history-view.d.ts +26 -0
  150. package/dist/history/ai-history-view.js +51 -0
  151. package/dist/history/conversation-history-tree.d.ts +15 -0
  152. package/dist/history/conversation-history-tree.js +71 -0
  153. package/dist/host/agent-workspace.d.ts +6 -0
  154. package/dist/host/agent-workspace.js +16 -0
  155. package/dist/host/create-host-runtimes.d.ts +2 -0
  156. package/dist/host/create-host-runtimes.js +14 -0
  157. package/dist/host/desktop-process-host.d.ts +6 -0
  158. package/dist/host/desktop-process-host.js +74 -0
  159. package/dist/host/host-runtime-availability.d.ts +1 -0
  160. package/dist/host/host-runtime-availability.js +3 -0
  161. package/dist/host/process-host.d.ts +31 -0
  162. package/dist/host/process-host.js +6 -0
  163. package/dist/index.d.ts +69 -0
  164. package/dist/index.js +49 -0
  165. package/dist/jsonl/ai-jsonl-preview.d.ts +27 -0
  166. package/dist/jsonl/ai-jsonl-preview.js +68 -0
  167. package/dist/jsonl/ai-jsonl-view-type.d.ts +1 -0
  168. package/dist/jsonl/ai-jsonl-view-type.js +1 -0
  169. package/dist/jsonl/ai-jsonl-view.d.ts +16 -0
  170. package/dist/jsonl/ai-jsonl-view.js +57 -0
  171. package/dist/jsonl/ai-jsonl-view.svelte +260 -0
  172. package/dist/jsonl/ai-jsonl-view.svelte.d.ts +9 -0
  173. package/dist/memory/candidate-extractor.d.ts +25 -0
  174. package/dist/memory/candidate-extractor.js +191 -0
  175. package/dist/memory/forget-confirmation.d.ts +3 -0
  176. package/dist/memory/forget-confirmation.js +45 -0
  177. package/dist/memory/hashes.d.ts +5 -0
  178. package/dist/memory/hashes.js +37 -0
  179. package/dist/memory/memory-ingestion-coordinator.d.ts +19 -0
  180. package/dist/memory/memory-ingestion-coordinator.js +108 -0
  181. package/dist/memory/memory-record-store.d.ts +62 -0
  182. package/dist/memory/memory-record-store.js +369 -0
  183. package/dist/memory/memory-scheduler.d.ts +21 -0
  184. package/dist/memory/memory-scheduler.js +143 -0
  185. package/dist/memory/memory-tools.d.ts +61 -0
  186. package/dist/memory/memory-tools.js +99 -0
  187. package/dist/memory/native-memory-service.d.ts +72 -0
  188. package/dist/memory/native-memory-service.js +1256 -0
  189. package/dist/memory/paths.d.ts +13 -0
  190. package/dist/memory/paths.js +60 -0
  191. package/dist/memory/runtime-consolidation-provider.d.ts +18 -0
  192. package/dist/memory/runtime-consolidation-provider.js +154 -0
  193. package/dist/memory/types.d.ts +139 -0
  194. package/dist/memory/types.js +1 -0
  195. package/dist/providers/acp-model-provider.d.ts +11 -0
  196. package/dist/providers/acp-model-provider.js +99 -0
  197. package/dist/providers/codex-model-catalog.d.ts +7 -0
  198. package/dist/providers/codex-model-catalog.js +179 -0
  199. package/dist/providers/codex-model-provider.d.ts +12 -0
  200. package/dist/providers/codex-model-provider.js +42 -0
  201. package/dist/providers/model-provider.d.ts +26 -0
  202. package/dist/providers/model-provider.js +26 -0
  203. package/dist/public-components.d.ts +3 -0
  204. package/dist/public-components.js +3 -0
  205. package/dist/registry/runtime-registry.d.ts +11 -0
  206. package/dist/registry/runtime-registry.js +71 -0
  207. package/dist/registry/select-runtime.d.ts +9 -0
  208. package/dist/registry/select-runtime.js +31 -0
  209. package/dist/runtime-adapters.d.ts +16 -0
  210. package/dist/runtime-adapters.js +9 -0
  211. package/dist/runtimes/acp/acp-event-mapper.d.ts +56 -0
  212. package/dist/runtimes/acp/acp-event-mapper.js +237 -0
  213. package/dist/runtimes/acp/acp-runtime.d.ts +46 -0
  214. package/dist/runtimes/acp/acp-runtime.js +165 -0
  215. package/dist/runtimes/acp/desktop-acp-backend.d.ts +16 -0
  216. package/dist/runtimes/acp/desktop-acp-backend.js +243 -0
  217. package/dist/runtimes/codex/app-server-protocol.d.ts +28 -0
  218. package/dist/runtimes/codex/app-server-protocol.js +274 -0
  219. package/dist/runtimes/codex/codex-runtime.d.ts +24 -0
  220. package/dist/runtimes/codex/codex-runtime.js +318 -0
  221. package/dist/runtimes/fake/fake-runtime.d.ts +51 -0
  222. package/dist/runtimes/fake/fake-runtime.js +257 -0
  223. package/dist/sessions/plugin-data.d.ts +8 -0
  224. package/dist/sessions/plugin-data.js +29 -0
  225. package/dist/sessions/session-store.d.ts +47 -0
  226. package/dist/sessions/session-store.js +117 -0
  227. package/dist/settings/acp-agents.d.ts +8 -0
  228. package/dist/settings/acp-agents.js +11 -0
  229. package/dist/settings/ai-settings-tab.d.ts +8 -0
  230. package/dist/settings/ai-settings-tab.js +249 -0
  231. package/dist/settings/ai-settings.d.ts +43 -0
  232. package/dist/settings/ai-settings.js +147 -0
  233. package/dist/settings/app-tool-setting-rows.d.ts +16 -0
  234. package/dist/settings/app-tool-setting-rows.js +30 -0
  235. package/dist/settings/register-ai-settings.d.ts +3 -0
  236. package/dist/settings/register-ai-settings.js +393 -0
  237. package/dist/skills/activation.d.ts +2 -0
  238. package/dist/skills/activation.js +20 -0
  239. package/dist/skills/bundled/lapis-notes.d.ts +3 -0
  240. package/dist/skills/bundled/lapis-notes.js +52 -0
  241. package/dist/skills/bundled/research.d.ts +6 -0
  242. package/dist/skills/bundled/research.js +29 -0
  243. package/dist/skills/discovery.d.ts +21 -0
  244. package/dist/skills/discovery.js +227 -0
  245. package/dist/skills/manifest.d.ts +3 -0
  246. package/dist/skills/manifest.js +39 -0
  247. package/dist/skills/parser.d.ts +12 -0
  248. package/dist/skills/parser.js +124 -0
  249. package/dist/skills/registry.d.ts +21 -0
  250. package/dist/skills/registry.js +89 -0
  251. package/dist/skills/seed.d.ts +5 -0
  252. package/dist/skills/seed.js +18 -0
  253. package/dist/skills/skill-tools.d.ts +8 -0
  254. package/dist/skills/skill-tools.js +113 -0
  255. package/dist/skills/types.d.ts +67 -0
  256. package/dist/skills/types.js +1 -0
  257. package/dist/styles.css +514 -0
  258. package/dist/tools/app-tool-host.d.ts +47 -0
  259. package/dist/tools/app-tool-host.js +419 -0
  260. package/dist/tools/desktop-app-tool-bridge.d.ts +23 -0
  261. package/dist/tools/desktop-app-tool-bridge.js +223 -0
  262. package/dist/tools/mcp-server-registry.d.ts +7 -0
  263. package/dist/tools/mcp-server-registry.js +30 -0
  264. package/manifest.json +34 -0
  265. package/package.json +83 -0
  266. package/styles.css +1 -0
@@ -0,0 +1,1926 @@
1
+ import { interruptPendingInteractions } from "../sessions/session-store";
2
+ import { relocateConversationLocation } from "../conversations/conversation-locator";
3
+ import { buildConversationContextHandoff, } from "../conversations/context-handoff";
4
+ import { effectiveAgentBinding, reduceAgentBindings, } from "../conversations/binding-state";
5
+ import { transcriptEntryHash } from "../conversations/hashes";
6
+ import { projectChatItemsToTranscript, projectTranscriptToChatItems, } from "../conversations/transcript-projection";
7
+ import { approvalGrantIdentity, persistentDecisionForRequest, persistentDecisionFromOption, upsertApprovalGrant, } from "../conversations/approval-grants";
8
+ import { CONVERSATION_SCHEMA_VERSION, ConversationUnavailableError, } from "../conversations/types";
9
+ import { extractMentionPaths, mergeAttachmentPaths } from "./chat-mentions";
10
+ import { applyStoredSessionResumePolicy, chatSessionId, loadStoredChatSession, snapshotStoredChatSession, } from "./chat-session";
11
+ import { createChatItemId, } from "./chat-items";
12
+ import { applyAgentEventToChatItems, isVisibleAgentStatus, markApprovalResponse, markQuestionResponse, } from "./chat-trace";
13
+ import { APP_TOOL_MCP_SERVER_NAME } from "../tools/mcp-server-registry";
14
+ import { buildAvailableSkillsManifest } from "../skills/manifest";
15
+ import { SkillSnapshotStore } from "../skills/registry";
16
+ import { SlashCommandRouter } from "../commands/router";
17
+ import { composerAgentLabel, parseAgentCommand } from "../commands/agent";
18
+ import { buildAgentBootstrap, buildSessionBootstrap } from "../bootstrap/build";
19
+ import { hasHostFilesystemPath } from "../skills/manifest";
20
+ import { formatSlashHelp } from "../commands/groups";
21
+ import { formatContextNotice, formatScopeNotice } from "../commands/inspect";
22
+ import { normalizeConversationScope } from "../conversations/paths";
23
+ import { conversationsInScopeTree } from "../conversations/scope-tree";
24
+ export class AiChatController {
25
+ items = $state.raw([]);
26
+ busy = $state(false);
27
+ commandWorking = $state(false);
28
+ error = $state(null);
29
+ usage = $state(null);
30
+ appToolsUnavailableReason = $state(null);
31
+ bindings = $state.raw([]);
32
+ location = $state.raw(null);
33
+ conversationStatus = $state(null);
34
+ conversationPinned = $state(false);
35
+ directoryContext = $state("");
36
+ pickerEntries = $state.raw([]);
37
+ session = null;
38
+ runtime;
39
+ unavailableReason;
40
+ mcpServers;
41
+ store;
42
+ repository;
43
+ sessionId;
44
+ workspace;
45
+ request;
46
+ #sessionRequest;
47
+ #createdAt;
48
+ #persistQueue = Promise.resolve();
49
+ #terminalCheckpoint = Promise.resolve();
50
+ #activeBindingId;
51
+ #activeBinding;
52
+ #usageDirty = false;
53
+ #sessionContexts = new Map();
54
+ #cancelledSessions = new WeakSet();
55
+ #turnId = 0;
56
+ #createConversation;
57
+ #onLocationChange;
58
+ #selectRuntime;
59
+ #appToolBridge;
60
+ #unsubscribeAppToolEvents;
61
+ #skills;
62
+ #skillSnapshots;
63
+ #slashRouter;
64
+ #appToolHost;
65
+ #skillContext;
66
+ #readVaultText;
67
+ #memoryRecall;
68
+ #handoffSummaries;
69
+ #onComposerDefaults;
70
+ #refreshSkills = false;
71
+ #activeRecallAbort;
72
+ #pendingHandoffs = new WeakMap();
73
+ #approvalGrants = [];
74
+ #followedScope;
75
+ #desiredFollowScope;
76
+ #followGeneration = 0;
77
+ constructor(runtime, unavailableReason = null, mcpServers = [], options = {}) {
78
+ this.runtime = runtime;
79
+ this.unavailableReason = unavailableReason;
80
+ this.mcpServers = mcpServers;
81
+ this.store = options.store;
82
+ this.repository = options.repository;
83
+ this.location = options.location ?? null;
84
+ if (this.location) {
85
+ this.directoryContext = this.location.scopeDir;
86
+ this.#followedScope = this.location.scopeDir;
87
+ this.#desiredFollowScope = this.location.scopeDir;
88
+ }
89
+ this.#createConversation = options.createConversation;
90
+ this.#onLocationChange = options.onLocationChange;
91
+ this.#selectRuntime = options.selectRuntime;
92
+ this.#appToolBridge = options.appToolBridge;
93
+ this.#skills = options.skills;
94
+ this.#skillSnapshots = options.skillSnapshots ?? new SkillSnapshotStore();
95
+ this.#slashRouter = options.slashRouter;
96
+ this.#appToolHost = options.appToolHost;
97
+ this.#skillContext = options.skillContext;
98
+ this.#readVaultText = options.readVaultText;
99
+ this.#memoryRecall = options.memoryRecall;
100
+ this.#handoffSummaries = options.handoffSummaries;
101
+ this.#onComposerDefaults = options.onComposerDefaults;
102
+ this.#unsubscribeAppToolEvents = options.appToolBridge?.subscribe((event) => {
103
+ void this.#consumeAppToolEvent(event);
104
+ });
105
+ this.workspace = options.workspace;
106
+ this.request = options.request ?? {};
107
+ this.#sessionRequest = this.request;
108
+ this.sessionId =
109
+ options.sessionId ??
110
+ chatSessionId(options.workspace, runtime.id, this.request.agent ?? "default");
111
+ }
112
+ async restore() {
113
+ if (this.repository && this.location) {
114
+ try {
115
+ await this.#restoreConversation();
116
+ }
117
+ catch (error) {
118
+ this.#releaseUnreadableLocation(error);
119
+ }
120
+ return;
121
+ }
122
+ let stored = await loadStoredChatSession(this.store, this.sessionId);
123
+ if (!stored) {
124
+ const legacy = await loadStoredChatSession(this.store, chatSessionId(this.workspace));
125
+ if (legacy &&
126
+ legacy.runtime === this.runtime.id &&
127
+ (legacy.agent === this.request.agent ||
128
+ (!legacy.agent && this.request.agent === "codex"))) {
129
+ stored = legacy;
130
+ }
131
+ }
132
+ if (!stored)
133
+ return;
134
+ this.#createdAt = stored.createdAt;
135
+ this.#sessionRequest = {
136
+ ...this.request,
137
+ agent: stored.agent ?? this.request.agent,
138
+ model: stored.model ?? this.request.model,
139
+ thinking: stored.thinking ?? this.request.thinking,
140
+ workspace: stored.workspace ?? this.workspace,
141
+ };
142
+ this.items = [...stored.items];
143
+ this.usage = stored.usage ? { ...stored.usage } : null;
144
+ let resumed = false;
145
+ if (this.runtime.capabilities().resume && this.runtime.resume) {
146
+ try {
147
+ this.session = await this.runtime.resume(stored.runtimeSessionId, this.#sessionRequest);
148
+ resumed = true;
149
+ }
150
+ catch {
151
+ this.session = null;
152
+ }
153
+ }
154
+ const restored = applyStoredSessionResumePolicy({
155
+ stored,
156
+ runtime: this.runtime,
157
+ resumed,
158
+ });
159
+ this.items = restored.items;
160
+ if (resumed && this.session)
161
+ void this.#consume(this.session);
162
+ await this.#persist(restored.interrupted);
163
+ }
164
+ async openConversation(location) {
165
+ if (!this.repository)
166
+ return;
167
+ await this.#closeAppToolBinding();
168
+ await this.session?.close().catch(() => undefined);
169
+ this.session = null;
170
+ this.busy = false;
171
+ this.error = null;
172
+ this.conversationStatus = null;
173
+ this.location = { ...location };
174
+ this.directoryContext = location.scopeDir;
175
+ this.#followedScope = location.scopeDir;
176
+ this.#desiredFollowScope = location.scopeDir;
177
+ this.pickerEntries = [];
178
+ this.#onLocationChange?.(this.location);
179
+ try {
180
+ await this.#restoreConversation();
181
+ }
182
+ catch (error) {
183
+ this.#releaseUnreadableLocation(error);
184
+ }
185
+ }
186
+ get activeBindingId() {
187
+ return this.#activeBindingId;
188
+ }
189
+ async newConversation(input) {
190
+ await this.#closeAppToolBinding();
191
+ await this.session?.close().catch(() => undefined);
192
+ this.session = null;
193
+ this.busy = false;
194
+ this.error = null;
195
+ this.usage = null;
196
+ this.items = [];
197
+ this.#activeBinding = undefined;
198
+ this.#activeBindingId = undefined;
199
+ this.bindings = [];
200
+ this.location = null;
201
+ this.conversationStatus = null;
202
+ this.conversationPinned = false;
203
+ this.pickerEntries = [];
204
+ this.#approvalGrants = [];
205
+ if (this.repository && input) {
206
+ const created = await this.repository.create(input);
207
+ this.location = created.location;
208
+ this.conversationStatus = created.metadata.status;
209
+ this.conversationPinned = Boolean(created.metadata.pinned);
210
+ this.directoryContext = created.location.scopeDir;
211
+ this.#followedScope = created.location.scopeDir;
212
+ }
213
+ this.#onLocationChange?.(this.location);
214
+ }
215
+ async archiveCurrent(archived = true) {
216
+ if (!this.repository || !this.location)
217
+ return;
218
+ const snapshot = await this.repository.archive(this.location, archived);
219
+ this.conversationStatus = snapshot.metadata.status;
220
+ }
221
+ relocateScope(oldPath, newPath) {
222
+ if (this.location) {
223
+ const relocated = relocateConversationLocation(this.location, oldPath, newPath);
224
+ if (relocated) {
225
+ if (relocated.scopeDir !== this.location.scopeDir) {
226
+ void this.#closeAppToolBinding();
227
+ }
228
+ this.location = relocated;
229
+ this.#onLocationChange?.(relocated);
230
+ }
231
+ }
232
+ for (const context of this.#sessionContexts.values()) {
233
+ if (!context.location)
234
+ continue;
235
+ const next = relocateConversationLocation(context.location, oldPath, newPath);
236
+ if (next)
237
+ context.location = next;
238
+ }
239
+ }
240
+ async deleteCurrent() {
241
+ if (!this.repository || !this.location)
242
+ return;
243
+ const location = this.location;
244
+ await this.#closeAppToolBinding();
245
+ await this.session?.close().catch(() => undefined);
246
+ await this.repository.delete(location);
247
+ this.session = null;
248
+ this.busy = false;
249
+ this.error = null;
250
+ this.usage = null;
251
+ this.items = [];
252
+ this.#activeBinding = undefined;
253
+ this.#activeBindingId = undefined;
254
+ this.bindings = [];
255
+ this.location = null;
256
+ this.conversationStatus = null;
257
+ this.conversationPinned = false;
258
+ this.pickerEntries = [];
259
+ this.#approvalGrants = [];
260
+ this.#onLocationChange?.(null);
261
+ }
262
+ async followDirectoryScope(scopeDir, options = {}) {
263
+ if (!this.repository)
264
+ return;
265
+ const scope = normalizeConversationScope(scopeDir);
266
+ this.#desiredFollowScope = scope;
267
+ if (!options.force && (this.busy || this.conversationPinned))
268
+ return;
269
+ if (!options.force && this.#followedScope === scope)
270
+ return;
271
+ const generation = ++this.#followGeneration;
272
+ this.directoryContext = scope;
273
+ this.#followedScope = scope;
274
+ const matches = conversationsInScopeTree(await this.repository.listAll(), scope);
275
+ if (generation !== this.#followGeneration)
276
+ return;
277
+ if (matches.length === 0) {
278
+ this.pickerEntries = [];
279
+ if (this.location)
280
+ await this.#clearOpenConversation();
281
+ return;
282
+ }
283
+ if (matches.length === 1) {
284
+ this.pickerEntries = [];
285
+ const next = matches[0].location;
286
+ if (this.location?.scopeDir === next.scopeDir &&
287
+ this.location.conversationId === next.conversationId) {
288
+ return;
289
+ }
290
+ await this.openConversation(next);
291
+ return;
292
+ }
293
+ this.pickerEntries = matches;
294
+ if (this.location)
295
+ await this.#clearOpenConversation();
296
+ }
297
+ async setPinned(pinned) {
298
+ if (!this.repository || !this.location)
299
+ return;
300
+ const snapshot = await this.repository.writePinned(this.location, pinned);
301
+ this.conversationPinned = Boolean(snapshot.metadata.pinned);
302
+ if (!this.conversationPinned) {
303
+ await this.followDirectoryScope(this.#desiredFollowScope ?? this.directoryContext, { force: true });
304
+ }
305
+ }
306
+ async #clearOpenConversation() {
307
+ await this.#closeAppToolBinding();
308
+ await this.session?.close().catch(() => undefined);
309
+ this.session = null;
310
+ this.busy = false;
311
+ this.error = null;
312
+ this.usage = null;
313
+ this.items = [];
314
+ this.#activeBinding = undefined;
315
+ this.#activeBindingId = undefined;
316
+ this.bindings = [];
317
+ this.location = null;
318
+ this.conversationStatus = null;
319
+ this.conversationPinned = false;
320
+ this.#approvalGrants = [];
321
+ this.#onLocationChange?.(null);
322
+ }
323
+ async #restoreConversation() {
324
+ if (!this.repository || !this.location)
325
+ return;
326
+ const snapshot = await this.repository.read(this.location);
327
+ this.conversationStatus = snapshot.metadata.status;
328
+ this.conversationPinned = Boolean(snapshot.metadata.pinned);
329
+ this.directoryContext = snapshot.location.scopeDir;
330
+ this.#loadApprovalGrants(snapshot.metadata);
331
+ this.items = projectTranscriptToChatItems(snapshot.transcript);
332
+ const activeBinding = effectiveAgentBinding(snapshot.agents, snapshot.metadata.activeAgentBindingId);
333
+ this.#activeBinding = activeBinding;
334
+ this.#activeBindingId = activeBinding?.id;
335
+ if (activeBinding) {
336
+ await this.#prepareSkillSnapshot(activeBinding.id, snapshot.location.scopeDir);
337
+ }
338
+ this.bindings = reduceAgentBindings(snapshot.agents);
339
+ const latestUsage = [...snapshot.agents]
340
+ .reverse()
341
+ .find((record) => record.type === "usage.updated" &&
342
+ (!activeBinding || record.agentBindingId === activeBinding.id));
343
+ this.usage =
344
+ latestUsage?.type === "usage.updated" ? { ...latestUsage.usage } : null;
345
+ if (!activeBinding || activeBinding.runtime !== this.runtime.id) {
346
+ await this.#interruptUnresumableInteractions();
347
+ return;
348
+ }
349
+ this.#sessionRequest = {
350
+ ...this.request,
351
+ agent: activeBinding.agent,
352
+ model: activeBinding.model,
353
+ thinking: activeBinding.thinking,
354
+ workspace: this.workspace,
355
+ };
356
+ if (!activeBinding.nativeSessionId ||
357
+ !activeBinding.context ||
358
+ !this.runtime.capabilities().resume ||
359
+ !this.runtime.resume) {
360
+ await this.#interruptUnresumableInteractions();
361
+ return;
362
+ }
363
+ try {
364
+ const appToolSession = await this.#prepareAppToolSession(activeBinding.id, this.runtime, snapshot.location, snapshot.metadata.launchContext?.notePath);
365
+ this.session = await this.runtime.resume(activeBinding.nativeSessionId, {
366
+ ...this.#sessionRequest,
367
+ appToolSession,
368
+ });
369
+ void this.#consume(this.session, activeBinding.id);
370
+ }
371
+ catch (error) {
372
+ await this.#closeAppToolBinding(activeBinding.id);
373
+ this.session = null;
374
+ this.error = `Could not resume the previous agent session. Your local history is still available. ${error instanceof Error ? error.message : String(error)}`;
375
+ await this.#interruptUnresumableInteractions();
376
+ }
377
+ }
378
+ async #interruptUnresumableInteractions() {
379
+ const hasPending = this.items.some((item) => (item.type === "approval" || item.type === "question") &&
380
+ item.status === "pending");
381
+ if (!hasPending)
382
+ return;
383
+ this.items = interruptPendingInteractions(this.items);
384
+ await this.#persist(true, this.#activeBindingId);
385
+ }
386
+ async submit(prompt, request = {}) {
387
+ try {
388
+ await this.#syncSlashCatalog();
389
+ }
390
+ catch (error) {
391
+ this.error = error instanceof Error ? error.message : String(error);
392
+ this.items = [
393
+ ...this.items,
394
+ {
395
+ id: `error-${crypto.randomUUID()}`,
396
+ type: "error",
397
+ text: this.error,
398
+ createdAt: new Date().toISOString(),
399
+ },
400
+ ];
401
+ return;
402
+ }
403
+ const resolution = this.#slashRouter?.resolve(prompt, this.#activeBindingId);
404
+ if (resolution?.kind === "unknown" || resolution?.kind === "command") {
405
+ await this.#executeSlash(resolution, request);
406
+ return;
407
+ }
408
+ const text = resolution?.kind === "literal" ? resolution.text.trim() : prompt.trim();
409
+ if (!text || this.busy)
410
+ return;
411
+ this.error = null;
412
+ const userItem = {
413
+ id: this.repository
414
+ ? `user-${crypto.randomUUID()}`
415
+ : `user-${this.items.length + 1}`,
416
+ type: "message",
417
+ role: "user",
418
+ text,
419
+ createdAt: new Date().toISOString(),
420
+ };
421
+ this.items = [...this.items, userItem];
422
+ this.busy = true;
423
+ const turnId = ++this.#turnId;
424
+ const attachments = mergeAttachmentPaths(extractMentionPaths(text), readAttachmentPaths(request.metadata?.attachments));
425
+ const effectiveRequest = {
426
+ ...this.request,
427
+ ...request,
428
+ metadata: {
429
+ ...this.request.metadata,
430
+ ...request.metadata,
431
+ ...(attachments.length > 0 ? { attachments } : {}),
432
+ },
433
+ };
434
+ const previousRuntime = this.runtime;
435
+ const previousBinding = this.#activeBinding;
436
+ const previousBindingId = this.#activeBindingId;
437
+ const previousSessionRequest = this.#sessionRequest;
438
+ try {
439
+ if (this.repository)
440
+ await this.#ensureConversation();
441
+ if (this.#isAbandoned(turnId))
442
+ return;
443
+ await this.#prepareSession(effectiveRequest, turnId);
444
+ if (this.#isAbandoned(turnId))
445
+ return;
446
+ if (this.#activeBindingId) {
447
+ this.items = this.items.map((item) => item.id === userItem.id
448
+ ? { ...item, agentBindingId: this.#activeBindingId }
449
+ : item);
450
+ }
451
+ if (this.repository && this.location && this.session) {
452
+ const context = this.#sessionContexts.get(this.session) ?? {
453
+ location: { ...this.location },
454
+ pendingItems: [],
455
+ };
456
+ context.pendingItems.push(this.items.find((item) => item.id === userItem.id) ?? userItem);
457
+ this.#sessionContexts.set(this.session, context);
458
+ }
459
+ else {
460
+ await this.#persist();
461
+ }
462
+ if (this.#isAbandoned(turnId))
463
+ return;
464
+ if (!this.session) {
465
+ if (this.error)
466
+ return;
467
+ throw new Error("Agent session did not start.");
468
+ }
469
+ const recallAbort = new AbortController();
470
+ this.#activeRecallAbort?.abort();
471
+ this.#activeRecallAbort = recallAbort;
472
+ const recalledBlocks = await this.#memoryRecall
473
+ ?.recall(text, {
474
+ scopeDir: this.location?.scopeDir ?? this.directoryContext,
475
+ conversationId: this.location?.conversationId,
476
+ agentBindingId: this.#activeBindingId,
477
+ runId: `turn-${turnId}`,
478
+ }, recallAbort.signal)
479
+ .catch(() => []);
480
+ if (this.#activeRecallAbort === recallAbort) {
481
+ this.#activeRecallAbort = undefined;
482
+ }
483
+ if (this.#isAbandoned(turnId))
484
+ return;
485
+ const handoff = this.#pendingHandoffs.get(this.session);
486
+ const contextBlocks = [
487
+ ...(handoff ? [handoff.block] : []),
488
+ ...(recalledBlocks ?? []),
489
+ ];
490
+ await this.session.send(text, {
491
+ contextBlocks,
492
+ });
493
+ if (handoff)
494
+ this.#pendingHandoffs.delete(this.session);
495
+ if (!this.repository)
496
+ await this.#persist();
497
+ }
498
+ catch (error) {
499
+ if (this.#isAbandoned(turnId))
500
+ return;
501
+ const failedSession = this.session;
502
+ const failedBindingId = this.#activeBindingId;
503
+ const failedContext = failedSession
504
+ ? this.#sessionContexts.get(failedSession)
505
+ : undefined;
506
+ if (failedContext?.pendingBinding ||
507
+ failedContext?.pendingSwitch ||
508
+ failedContext?.pendingConfiguration) {
509
+ this.runtime = previousRuntime;
510
+ this.#activeBinding = previousBinding;
511
+ this.#activeBindingId = previousBindingId;
512
+ this.#sessionRequest = previousSessionRequest;
513
+ this.bindings = this.bindings.filter((binding) => binding.id !== failedContext.pendingBinding?.id);
514
+ if (failedContext.pendingConfiguration) {
515
+ this.bindings = this.bindings.map((binding) => binding.id === failedContext.pendingConfiguration?.previous.id
516
+ ? failedContext.pendingConfiguration.previous
517
+ : binding);
518
+ }
519
+ }
520
+ this.error = error instanceof Error ? error.message : String(error);
521
+ this.items = applyAgentEventToChatItems(this.items, {
522
+ type: "error",
523
+ error: error instanceof Error ? error : new Error(String(error)),
524
+ });
525
+ this.session = null;
526
+ this.busy = false;
527
+ await this.#closeAppToolBinding(failedBindingId);
528
+ await failedSession?.close().catch(() => undefined);
529
+ try {
530
+ if (this.repository)
531
+ await this.#ensureConversation();
532
+ if (this.repository && this.location) {
533
+ await this.#appendDurableItems(this.location, [userItem, this.items.at(-1)], this.#activeBindingId);
534
+ }
535
+ else {
536
+ await this.#persist();
537
+ }
538
+ }
539
+ catch (persistError) {
540
+ const persistMessage = persistError instanceof Error
541
+ ? persistError.message
542
+ : String(persistError);
543
+ this.error = `${this.error} Could not save this turn: ${persistMessage}`;
544
+ }
545
+ }
546
+ }
547
+ async respondToApproval(requestId, optionId) {
548
+ const pending = this.items.find((candidate) => candidate.type === "approval" && candidate.request.id === requestId);
549
+ if (pending?.type === "approval" && pending.request.origin === "app-tool") {
550
+ if (!this.#appToolBridge?.respondToApproval(requestId, optionId)) {
551
+ throw new Error(`Unknown application-tool approval: ${requestId}`);
552
+ }
553
+ this.items = markApprovalResponse(this.items, requestId, optionId);
554
+ const item = this.items.find((candidate) => candidate.type === "approval" && candidate.request.id === requestId);
555
+ if (this.repository && this.location && item) {
556
+ await this.#appendDurableItems(this.location, [item], this.#activeBindingId);
557
+ }
558
+ else {
559
+ await this.#persist();
560
+ }
561
+ return;
562
+ }
563
+ if (!this.session)
564
+ return;
565
+ if (pending?.type === "approval") {
566
+ await this.#rememberPersistentDecision(pending.request, optionId);
567
+ }
568
+ this.items = markApprovalResponse(this.items, requestId, optionId);
569
+ await this.session.respondToApproval(requestId, optionId);
570
+ const item = this.items.find((candidate) => candidate.type === "approval" && candidate.request.id === requestId);
571
+ if (this.repository && this.location && item) {
572
+ await this.#appendDurableItems(this.location, [item], this.#activeBindingId);
573
+ }
574
+ else {
575
+ await this.#persist();
576
+ }
577
+ }
578
+ async respondToQuestion(requestId, answers) {
579
+ if (!this.session?.respondToQuestion) {
580
+ throw new Error("The active runtime cannot answer agent questions.");
581
+ }
582
+ this.items = markQuestionResponse(this.items, requestId);
583
+ const item = this.items.find((candidate) => candidate.type === "question" && candidate.request.id === requestId);
584
+ if (this.repository && this.location && item) {
585
+ await this.#appendDurableItems(this.location, [item], this.#activeBindingId);
586
+ }
587
+ else {
588
+ await this.#persist();
589
+ }
590
+ await this.session.respondToQuestion(requestId, answers);
591
+ if (!this.repository)
592
+ await this.#persist();
593
+ }
594
+ async cancel() {
595
+ this.#turnId += 1;
596
+ this.#activeRecallAbort?.abort();
597
+ this.#activeRecallAbort = undefined;
598
+ const session = this.session;
599
+ if (session)
600
+ this.#cancelledSessions.add(session);
601
+ this.busy = false;
602
+ this.commandWorking = false;
603
+ this.items = interruptPendingInteractions(this.items);
604
+ void this.#confirmCancelledNotice(session);
605
+ await this.#persist(true);
606
+ }
607
+ async #confirmCancelledNotice(session) {
608
+ if (session) {
609
+ try {
610
+ await session.cancel?.();
611
+ }
612
+ catch {
613
+ return;
614
+ }
615
+ if (!this.#cancelledSessions.has(session))
616
+ return;
617
+ if (this.session !== session && this.session)
618
+ return;
619
+ }
620
+ if (this.items.some((item) => item.type === "status" && item.text === CANCELLED_NOTICE)) {
621
+ return;
622
+ }
623
+ const item = {
624
+ id: createChatItemId("status", this.items.length + 1),
625
+ type: "status",
626
+ text: CANCELLED_NOTICE,
627
+ createdAt: new Date().toISOString(),
628
+ agentBindingId: this.#activeBindingId,
629
+ };
630
+ this.items = [...this.items, item];
631
+ if (this.repository && this.location) {
632
+ await this.#appendDurableItems(this.location, [item], this.#activeBindingId);
633
+ return;
634
+ }
635
+ await this.#persist(false, this.#activeBindingId);
636
+ }
637
+ async cancelAndSwitch(request) {
638
+ if (this.busy)
639
+ await this.cancel();
640
+ this.error = null;
641
+ await this.#prepareSession({ ...this.request, ...request });
642
+ }
643
+ async close() {
644
+ this.#turnId += 1;
645
+ this.#activeRecallAbort?.abort();
646
+ this.#activeRecallAbort = undefined;
647
+ const interrupted = this.busy;
648
+ if (interrupted)
649
+ await this.session?.cancel?.().catch(() => undefined);
650
+ this.busy = false;
651
+ this.commandWorking = false;
652
+ if (interrupted)
653
+ this.items = interruptPendingInteractions(this.items);
654
+ await this.#persist(interrupted);
655
+ await this.#closeAppToolBinding();
656
+ await this.session?.close();
657
+ this.session = null;
658
+ this.busy = false;
659
+ for (const binding of this.bindings) {
660
+ this.#slashRouter?.catalog.clearNativeCommands(binding.id);
661
+ }
662
+ if (this.#activeBindingId) {
663
+ this.#slashRouter?.catalog.clearNativeCommands(this.#activeBindingId);
664
+ }
665
+ this.#skillSnapshots.clear();
666
+ this.#unsubscribeAppToolEvents?.();
667
+ }
668
+ async #consume(session, agentBindingId = this.#activeBindingId) {
669
+ const context = this.#sessionContexts.get(session) ?? {
670
+ location: this.location ? { ...this.location } : null,
671
+ pendingItems: [],
672
+ };
673
+ this.#sessionContexts.set(session, context);
674
+ let traceItems = [];
675
+ let latestUsage = null;
676
+ const persistCompletedTrace = async () => {
677
+ if (traceItems.length === 0 &&
678
+ !latestUsage &&
679
+ !context.pendingBinding &&
680
+ !context.pendingSwitch &&
681
+ !context.pendingConfiguration &&
682
+ context.pendingItems.length === 0) {
683
+ return;
684
+ }
685
+ await waitForNextTask();
686
+ if (this.repository && context.location) {
687
+ if (context.pendingBinding) {
688
+ await this.repository.appendAgentRecords(context.location, [
689
+ context.pendingBinding,
690
+ ]);
691
+ }
692
+ if (context.pendingActivationBindingId) {
693
+ await this.repository.activateBinding(context.location, context.pendingActivationBindingId, context.pendingSwitch);
694
+ }
695
+ else if (context.pendingSwitch) {
696
+ await this.repository.appendTranscript(context.location, [
697
+ context.pendingSwitch,
698
+ ]);
699
+ }
700
+ if (context.pendingConfiguration) {
701
+ await this.repository.appendAgentRecords(context.location, [
702
+ context.pendingConfiguration.agentRecord,
703
+ ]);
704
+ await this.repository.appendTranscript(context.location, [
705
+ context.pendingConfiguration.transcriptEntry,
706
+ ]);
707
+ }
708
+ await this.#appendDurableItems(context.location, [...context.pendingItems, ...traceItems], agentBindingId, false, false, agentBindingId);
709
+ if (latestUsage && agentBindingId) {
710
+ await this.#appendUsage(context.location, agentBindingId, latestUsage);
711
+ }
712
+ if (agentBindingId) {
713
+ const checkpoint = await this.repository.read(context.location);
714
+ const through = checkpoint.transcript.at(-1);
715
+ if (through) {
716
+ await this.repository.appendAgentRecords(context.location, [
717
+ {
718
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
719
+ id: `context-${crypto.randomUUID()}`,
720
+ type: "binding.context.updated",
721
+ createdAt: new Date().toISOString(),
722
+ agentBindingId,
723
+ throughEntryId: through.id,
724
+ throughEntryHash: await transcriptEntryHash(through),
725
+ cause: context.handoff ? "handoff" : "native-turn",
726
+ handoffId: context.handoff?.handoffId,
727
+ projectionMode: context.handoff?.mode,
728
+ omittedEntryCount: context.handoff?.omittedEntryCount,
729
+ },
730
+ ]);
731
+ }
732
+ }
733
+ void this.#handoffSummaries
734
+ ?.afterTerminal(context.location)
735
+ .catch(() => undefined);
736
+ }
737
+ else {
738
+ await this.#persist(false, agentBindingId);
739
+ }
740
+ context.pendingBinding = undefined;
741
+ context.pendingSwitch = undefined;
742
+ context.pendingActivationBindingId = undefined;
743
+ context.pendingConfiguration = undefined;
744
+ context.handoff = undefined;
745
+ context.pendingItems = [];
746
+ traceItems = [];
747
+ latestUsage = null;
748
+ };
749
+ const checkpointTrace = () => {
750
+ const checkpoint = persistCompletedTrace();
751
+ this.#terminalCheckpoint = checkpoint.catch(() => undefined);
752
+ return checkpoint;
753
+ };
754
+ try {
755
+ for await (const event of session.events()) {
756
+ if ((event.type === "tool.start" || event.type === "tool.end") &&
757
+ event.server === APP_TOOL_MCP_SERVER_NAME) {
758
+ continue;
759
+ }
760
+ if (event.type === "usage") {
761
+ latestUsage = { ...event.usage };
762
+ if (this.session === session)
763
+ this.usage = latestUsage;
764
+ continue;
765
+ }
766
+ if (event.type === "status" && !isVisibleAgentStatus(event.status)) {
767
+ continue;
768
+ }
769
+ if (event.type === "commands.update") {
770
+ if (agentBindingId) {
771
+ this.#slashRouter?.catalog.replaceNativeCommands(agentBindingId, event.commands);
772
+ }
773
+ continue;
774
+ }
775
+ if (event.type === "permission.request") {
776
+ const decision = persistentDecisionForRequest(this.#approvalGrants, event.request);
777
+ if (decision) {
778
+ try {
779
+ await waitForNextTask();
780
+ await session.respondToApproval(event.request.id, decision);
781
+ continue;
782
+ }
783
+ catch {
784
+ // Fall through so an unmatched runtime request still opens the drawer.
785
+ }
786
+ }
787
+ }
788
+ traceItems = applyAgentEventToChatItems(traceItems, event);
789
+ const activeSession = this.session === session;
790
+ const cancelled = this.#cancelledSessions.has(session);
791
+ if (activeSession && !cancelled) {
792
+ this.items = applyAgentEventToChatItems(this.items, event).map((item) => item.agentBindingId || !agentBindingId
793
+ ? item
794
+ : { ...item, agentBindingId });
795
+ }
796
+ if (event.type === "completed" || event.type === "error") {
797
+ if (event.type === "error" && activeSession) {
798
+ this.error = event.error.message;
799
+ this.session = null;
800
+ }
801
+ }
802
+ if (activeSession &&
803
+ (event.type === "completed" || event.type === "error")) {
804
+ this.busy = false;
805
+ }
806
+ if (event.type === "completed" || event.type === "error") {
807
+ await checkpointTrace();
808
+ }
809
+ if (event.type === "error") {
810
+ await session.close().catch(() => undefined);
811
+ await this.#closeAppToolBinding(agentBindingId);
812
+ break;
813
+ }
814
+ }
815
+ }
816
+ catch (error) {
817
+ const normalized = error instanceof Error ? error : new Error(String(error));
818
+ const errorEvent = {
819
+ type: "error",
820
+ error: normalized,
821
+ };
822
+ traceItems = applyAgentEventToChatItems(traceItems, errorEvent);
823
+ if (this.session === session) {
824
+ this.error = normalized.message;
825
+ this.items = applyAgentEventToChatItems(this.items, errorEvent);
826
+ this.busy = false;
827
+ this.session = null;
828
+ }
829
+ await checkpointTrace();
830
+ await session.close().catch(() => undefined);
831
+ await this.#closeAppToolBinding(agentBindingId);
832
+ }
833
+ finally {
834
+ if (this.session === session)
835
+ this.busy = false;
836
+ await checkpointTrace();
837
+ this.#sessionContexts.delete(session);
838
+ }
839
+ }
840
+ async refreshSkills() {
841
+ this.#refreshSkills = true;
842
+ this.#skills?.invalidate();
843
+ if (this.busy)
844
+ return;
845
+ this.busy = true;
846
+ try {
847
+ await this.#prepareSession(this.#sessionRequest);
848
+ }
849
+ finally {
850
+ this.busy = false;
851
+ this.#refreshSkills = false;
852
+ }
853
+ }
854
+ async #executeSlash(resolution, request) {
855
+ const slashRouter = this.#slashRouter;
856
+ if (!slashRouter || this.busy)
857
+ return;
858
+ this.error = null;
859
+ if (this.repository)
860
+ await this.#ensureConversation();
861
+ const discovery = this.#skillContext?.() ?? {
862
+ scopeDir: this.location?.scopeDir ?? "",
863
+ };
864
+ const result = await slashRouter.execute(resolution, {
865
+ agentBindingId: this.#activeBindingId,
866
+ discovery,
867
+ });
868
+ if (result.kind === "error") {
869
+ this.error = result.message;
870
+ this.items = [
871
+ ...this.items,
872
+ {
873
+ id: `command-error-${crypto.randomUUID()}`,
874
+ type: "error",
875
+ text: result.message,
876
+ createdAt: new Date().toISOString(),
877
+ },
878
+ ];
879
+ return;
880
+ }
881
+ if (result.kind === "local") {
882
+ if (result.notice === "new") {
883
+ await this.newConversation({
884
+ scopeDir: this.location?.scopeDir ?? "",
885
+ });
886
+ return;
887
+ }
888
+ if (result.notice === "refresh") {
889
+ await this.refreshSkills();
890
+ this.#appendLocalNotice("Agent skills refreshed.");
891
+ return;
892
+ }
893
+ if (result.notice === "agent") {
894
+ await this.#applyAgentCommand(result.arguments ?? "");
895
+ return;
896
+ }
897
+ if (result.notice === "help") {
898
+ await this.#withCommandProgress(async (turnId) => {
899
+ await this.#prepareSession(request, turnId);
900
+ if (this.#isAbandoned(turnId))
901
+ return;
902
+ this.#appendLocalNotice(formatSlashHelp(slashRouter.catalog.list(this.#activeBindingId), composerAgentLabel(this.request.agent, typeof this.request.metadata?.runtime === "string"
903
+ ? this.request.metadata.runtime
904
+ : undefined)));
905
+ await this.#persistCommandNotice();
906
+ });
907
+ return;
908
+ }
909
+ if (result.notice === "scope") {
910
+ await this.#applyScopeCommand(result.arguments ?? "");
911
+ return;
912
+ }
913
+ if (result.notice === "context") {
914
+ await this.#withCommandProgress(async (turnId) => {
915
+ await this.#prepareSession(request, turnId);
916
+ if (this.#isAbandoned(turnId))
917
+ return;
918
+ this.#appendLocalNotice(await this.#describeContext());
919
+ await this.#persistCommandNotice();
920
+ });
921
+ return;
922
+ }
923
+ if (result.notice === "model") {
924
+ await this.#withCommandProgress(async (turnId) => {
925
+ if (this.#isAbandoned(turnId))
926
+ return;
927
+ this.#appendLocalNotice("Change the model from the composer Model menu.");
928
+ await this.#persistCommandNotice();
929
+ });
930
+ return;
931
+ }
932
+ if (result.notice === "cancel") {
933
+ await this.#withCommandProgress(async (turnId) => {
934
+ if (this.#isAbandoned(turnId))
935
+ return;
936
+ this.#appendLocalNotice("Use Stop in the composer to cancel the active run.");
937
+ await this.#persistCommandNotice();
938
+ });
939
+ return;
940
+ }
941
+ if (result.notice === "skills" || result.notice === "tools") {
942
+ await this.#withCommandProgress(async (turnId) => {
943
+ await this.#prepareSession(request, turnId);
944
+ if (this.#isAbandoned(turnId))
945
+ return;
946
+ if (result.notice === "skills") {
947
+ const snapshot = await this.#effectiveSkillSnapshot();
948
+ const items = (snapshot?.skills ?? []).map((skill) => ({
949
+ name: skill.name,
950
+ description: skill.description,
951
+ kind: "skill",
952
+ path: skillInventoryPath(this.#skills?.getLoaded(skill.name)),
953
+ }));
954
+ this.#appendInventoryNotice("skills", items, "No skills are available.");
955
+ }
956
+ else {
957
+ this.#ensureLocalAppToolSession();
958
+ const items = (this.#appToolHost?.getSession(this.#activeBindingId ?? "")
959
+ ?.tools ?? []).map((tool) => ({
960
+ name: tool.name,
961
+ description: tool.description,
962
+ kind: "tool",
963
+ }));
964
+ this.#appendInventoryNotice("tools", items, "No application tools are available.");
965
+ }
966
+ await this.#persistCommandNotice();
967
+ });
968
+ return;
969
+ }
970
+ this.#appendLocalNotice(`/${result.notice}`);
971
+ return;
972
+ }
973
+ if (result.kind === "tool") {
974
+ await this.#withCommandProgress(async (turnId) => {
975
+ await this.#prepareSession(request, turnId);
976
+ if (this.#isAbandoned(turnId))
977
+ return;
978
+ this.#ensureLocalAppToolSession();
979
+ if (!this.#appToolHost || !this.#activeBindingId) {
980
+ this.error = "Application tools are unavailable for this command.";
981
+ this.items = [
982
+ ...this.items,
983
+ {
984
+ id: `command-error-${crypto.randomUUID()}`,
985
+ type: "error",
986
+ text: this.error,
987
+ createdAt: new Date().toISOString(),
988
+ },
989
+ ];
990
+ return;
991
+ }
992
+ const callId = `slash-tool-${crypto.randomUUID()}`;
993
+ const toolEvent = {
994
+ id: callId,
995
+ name: result.tool,
996
+ server: APP_TOOL_MCP_SERVER_NAME,
997
+ input: result.input,
998
+ };
999
+ await this.#applySlashToolEvent({
1000
+ type: "tool.start",
1001
+ ...toolEvent,
1002
+ });
1003
+ try {
1004
+ const toolResult = await this.#appToolHost.invoke(this.#activeBindingId, {
1005
+ runId: callId,
1006
+ toolCallId: callId,
1007
+ name: result.tool,
1008
+ input: result.input,
1009
+ });
1010
+ if (this.#isAbandoned(turnId))
1011
+ return;
1012
+ await this.#applySlashToolEvent({
1013
+ type: "tool.end",
1014
+ ...toolEvent,
1015
+ ...(toolResult.isError
1016
+ ? { error: toolResult }
1017
+ : { output: toolResult }),
1018
+ });
1019
+ }
1020
+ catch (error) {
1021
+ if (this.#isAbandoned(turnId))
1022
+ return;
1023
+ await this.#applySlashToolEvent({
1024
+ type: "tool.end",
1025
+ ...toolEvent,
1026
+ error: error instanceof Error ? error.message : String(error),
1027
+ });
1028
+ }
1029
+ });
1030
+ return;
1031
+ }
1032
+ if (result.kind === "skill") {
1033
+ await this.#submitWithActivation(result.activation, request);
1034
+ return;
1035
+ }
1036
+ if (result.kind === "prompt") {
1037
+ await this.submit(result.prompt, request);
1038
+ return;
1039
+ }
1040
+ if (result.kind === "native") {
1041
+ const text = `/${result.name}${result.arguments ? ` ${result.arguments}` : ""}`;
1042
+ await this.#recordCommandItem(result.name, "native-agent", result.arguments);
1043
+ this.busy = true;
1044
+ try {
1045
+ await this.#prepareSession(request);
1046
+ await this.session?.send(text);
1047
+ }
1048
+ finally {
1049
+ this.busy = false;
1050
+ }
1051
+ }
1052
+ }
1053
+ async #submitWithActivation(activation, request) {
1054
+ this.busy = true;
1055
+ try {
1056
+ await this.#prepareSession({
1057
+ ...request,
1058
+ skillActivations: [activation],
1059
+ });
1060
+ this.items = [
1061
+ ...this.items,
1062
+ {
1063
+ id: `skill-${crypto.randomUUID()}`,
1064
+ type: "skill-activation",
1065
+ skillId: activation.skillId,
1066
+ skillName: activation.skillName,
1067
+ version: activation.version,
1068
+ origin: activation.source,
1069
+ arguments: activation.arguments,
1070
+ text: `Skill ${activation.skillName} (${activation.version})`,
1071
+ createdAt: new Date().toISOString(),
1072
+ agentBindingId: this.#activeBindingId,
1073
+ },
1074
+ ];
1075
+ if (this.repository && this.location) {
1076
+ await this.#appendDurableItems(this.location, [this.items.at(-1)], this.#activeBindingId);
1077
+ }
1078
+ await this.session?.send(activation.arguments?.trim() || activation.skillName);
1079
+ }
1080
+ finally {
1081
+ this.busy = false;
1082
+ }
1083
+ }
1084
+ async #applyAgentCommand(raw) {
1085
+ const parsed = parseAgentCommand(raw);
1086
+ if (parsed === "unknown") {
1087
+ this.error = `Unknown agent: ${raw.trim()}`;
1088
+ this.items = [
1089
+ ...this.items,
1090
+ {
1091
+ id: `command-error-${crypto.randomUUID()}`,
1092
+ type: "error",
1093
+ text: this.error,
1094
+ createdAt: new Date().toISOString(),
1095
+ },
1096
+ ];
1097
+ return;
1098
+ }
1099
+ if (parsed === "status") {
1100
+ await this.#withCommandProgress(async (turnId) => {
1101
+ if (this.#isAbandoned(turnId))
1102
+ return;
1103
+ this.#appendLocalNotice(composerAgentLabel(this.request.agent, typeof this.request.metadata?.runtime === "string"
1104
+ ? this.request.metadata.runtime
1105
+ : undefined));
1106
+ await this.#persistCommandNotice();
1107
+ });
1108
+ return;
1109
+ }
1110
+ this.request = {
1111
+ ...this.request,
1112
+ agent: parsed.agent,
1113
+ metadata: {
1114
+ ...this.request.metadata,
1115
+ runtime: parsed.runtimePreference,
1116
+ },
1117
+ };
1118
+ this.#sessionRequest = { ...this.request };
1119
+ this.#onComposerDefaults?.({
1120
+ agent: parsed.agent,
1121
+ runtimePreference: parsed.runtimePreference,
1122
+ });
1123
+ if (this.session) {
1124
+ await this.cancelAndSwitch({
1125
+ agent: parsed.agent,
1126
+ metadata: this.request.metadata,
1127
+ });
1128
+ }
1129
+ this.#appendLocalNotice(`Agent: ${parsed.label}`);
1130
+ await this.#persistCommandNotice();
1131
+ }
1132
+ async #applyScopeCommand(raw) {
1133
+ const folder = raw.trim();
1134
+ if (!folder) {
1135
+ await this.#withCommandProgress(async (turnId) => {
1136
+ if (this.#isAbandoned(turnId))
1137
+ return;
1138
+ this.#appendLocalNotice(await this.#describeScope());
1139
+ await this.#persistCommandNotice();
1140
+ });
1141
+ return;
1142
+ }
1143
+ try {
1144
+ const scopeDir = normalizeConversationScope(folder);
1145
+ const input = this.#createConversation?.(scopeDir) ?? { scopeDir };
1146
+ await this.newConversation({
1147
+ ...input,
1148
+ scopeDir,
1149
+ });
1150
+ this.#appendLocalNotice(await this.#describeScope("explicit"));
1151
+ await this.#persistCommandNotice();
1152
+ }
1153
+ catch (error) {
1154
+ this.error = error instanceof Error ? error.message : String(error);
1155
+ this.items = [
1156
+ ...this.items,
1157
+ {
1158
+ id: `command-error-${crypto.randomUUID()}`,
1159
+ type: "error",
1160
+ text: this.error,
1161
+ createdAt: new Date().toISOString(),
1162
+ },
1163
+ ];
1164
+ }
1165
+ }
1166
+ async #describeScope(source) {
1167
+ const snapshot = this.repository && this.location
1168
+ ? await this.repository.read(this.location).catch(() => undefined)
1169
+ : undefined;
1170
+ const pending = this.#createConversation?.();
1171
+ const scopeDir = this.location?.scopeDir ?? pending?.scopeDir ?? "";
1172
+ return formatScopeNotice({
1173
+ scopeDir,
1174
+ launchNotePath: snapshot?.metadata.launchContext?.notePath ?? pending?.launchNotePath,
1175
+ workspace: snapshot?.metadata.workspace?.path ?? this.workspace,
1176
+ source: source ??
1177
+ (this.location ? "conversation" : scopeDir ? "folder" : "vault-root"),
1178
+ });
1179
+ }
1180
+ async #describeContext() {
1181
+ const snapshot = this.repository && this.location
1182
+ ? await this.repository.read(this.location).catch(() => undefined)
1183
+ : undefined;
1184
+ const pending = this.#createConversation?.();
1185
+ const binding = this.#activeBinding;
1186
+ const scopeDir = this.location?.scopeDir ?? pending?.scopeDir ?? "";
1187
+ const bootstrap = await buildAgentBootstrap({
1188
+ conversationId: this.location?.conversationId ?? snapshot?.metadata.id,
1189
+ scopeDir,
1190
+ launchNotePath: snapshot?.metadata.launchContext?.notePath ?? pending?.launchNotePath,
1191
+ workspaceLabel: portableWorkspaceLabel(snapshot?.metadata.workspace?.path ?? this.workspace),
1192
+ readText: this.#readVaultText,
1193
+ });
1194
+ return formatContextNotice({
1195
+ conversationId: this.location?.conversationId ?? snapshot?.metadata.id,
1196
+ scopeDir,
1197
+ launchNotePath: snapshot?.metadata.launchContext?.notePath ?? pending?.launchNotePath,
1198
+ workspace: bootstrap.workspace?.label,
1199
+ agent: composerAgentLabel(this.request.agent, typeof this.request.metadata?.runtime === "string"
1200
+ ? this.request.metadata.runtime
1201
+ : undefined),
1202
+ model: this.request.model?.model ?? binding?.model?.model,
1203
+ tools: this.#appToolHost
1204
+ ?.getSession(this.#activeBindingId ?? "")
1205
+ ?.tools.map((tool) => tool.name) ?? [],
1206
+ skills: (await this.#effectiveSkillSnapshot())?.skills.map((skill) => skill.name) ?? [],
1207
+ folderInstructionPaths: bootstrap.folderInstructions
1208
+ .filter((entry) => !entry.omitted)
1209
+ .map((entry) => entry.path),
1210
+ truncated: bootstrap.truncated,
1211
+ });
1212
+ }
1213
+ async #sessionBootstrapMetadata(scopeDir, launchNotePath, conversationId, tools, skills) {
1214
+ try {
1215
+ const { text } = await buildSessionBootstrap({
1216
+ conversationId,
1217
+ scopeDir,
1218
+ launchNotePath,
1219
+ workspaceLabel: portableWorkspaceLabel(this.workspace),
1220
+ tools,
1221
+ skills,
1222
+ readText: this.#readVaultText,
1223
+ });
1224
+ return { sessionBootstrap: text };
1225
+ }
1226
+ catch {
1227
+ return {};
1228
+ }
1229
+ }
1230
+ async #withCommandProgress(work) {
1231
+ const turnId = ++this.#turnId;
1232
+ this.busy = true;
1233
+ this.commandWorking = true;
1234
+ try {
1235
+ await work(turnId);
1236
+ }
1237
+ finally {
1238
+ this.commandWorking = false;
1239
+ if (this.#turnId === turnId)
1240
+ this.busy = false;
1241
+ }
1242
+ }
1243
+ #appendLocalNotice(text) {
1244
+ this.items = [
1245
+ ...this.items,
1246
+ {
1247
+ id: `notice-${crypto.randomUUID()}`,
1248
+ type: "status",
1249
+ text,
1250
+ layout: "report",
1251
+ createdAt: new Date().toISOString(),
1252
+ },
1253
+ ];
1254
+ }
1255
+ #appendInventoryNotice(kind, items, emptyText) {
1256
+ this.items = [
1257
+ ...this.items,
1258
+ {
1259
+ id: `notice-${crypto.randomUUID()}`,
1260
+ type: "status",
1261
+ text: items.map((item) => item.name).join(", ") || emptyText,
1262
+ layout: "inventory",
1263
+ inventory: { kind, items },
1264
+ createdAt: new Date().toISOString(),
1265
+ },
1266
+ ];
1267
+ }
1268
+ async #recordCommandItem(command, source, args) {
1269
+ const item = {
1270
+ id: `command-${crypto.randomUUID()}`,
1271
+ type: "command",
1272
+ command,
1273
+ origin: source,
1274
+ arguments: args,
1275
+ status: "completed",
1276
+ text: `/${command}${args ? ` ${args}` : ""}`,
1277
+ createdAt: new Date().toISOString(),
1278
+ agentBindingId: this.#activeBindingId,
1279
+ };
1280
+ this.items = [...this.items, item];
1281
+ if (this.repository && this.location) {
1282
+ await this.#appendDurableItems(this.location, [item], this.#activeBindingId);
1283
+ }
1284
+ }
1285
+ async #persistCommandNotice() {
1286
+ const item = this.items.at(-1);
1287
+ if (this.repository && this.location && item) {
1288
+ await this.#appendDurableItems(this.location, [item], this.#activeBindingId);
1289
+ }
1290
+ }
1291
+ async #applySlashToolEvent(event) {
1292
+ const bindingId = this.#activeBindingId;
1293
+ this.items = applyAgentEventToChatItems(this.items, event).map((item) => item.agentBindingId || !bindingId
1294
+ ? item
1295
+ : { ...item, agentBindingId: bindingId });
1296
+ const item = this.items.find((candidate) => candidate.id === event.id ||
1297
+ (candidate.type === "tool" && candidate.toolId === event.id));
1298
+ if (this.repository && this.location && item) {
1299
+ await this.#appendDurableItems(this.location, [item], bindingId);
1300
+ }
1301
+ else if (!this.repository) {
1302
+ await this.#persist(false, bindingId);
1303
+ }
1304
+ }
1305
+ async syncComposerCommands() {
1306
+ await this.#syncSlashCatalog();
1307
+ }
1308
+ async #syncSlashCatalog() {
1309
+ if (!this.#slashRouter || !this.#skills)
1310
+ return;
1311
+ const context = this.#discoveryContext();
1312
+ await this.#slashRouter.catalog.refreshFileCommands(context.scopeDir);
1313
+ const snapshot = await this.#effectiveSkillSnapshot();
1314
+ if (snapshot)
1315
+ this.#slashRouter.catalog.rebuildSkillCommands(snapshot);
1316
+ }
1317
+ #discoveryContext() {
1318
+ return (this.#skillContext?.() ?? {
1319
+ scopeDir: this.location?.scopeDir ?? "",
1320
+ });
1321
+ }
1322
+ #ensureLocalAppToolSession() {
1323
+ if (!this.#appToolHost || !this.#activeBindingId)
1324
+ return;
1325
+ if (this.#appToolHost.getSession(this.#activeBindingId))
1326
+ return;
1327
+ this.#appToolHost.createSession({
1328
+ conversationId: this.location?.conversationId ?? "local",
1329
+ agentBindingId: this.#activeBindingId,
1330
+ scopeDir: this.location?.scopeDir ?? "",
1331
+ runtimeSupportsAppTools: true,
1332
+ });
1333
+ }
1334
+ async #effectiveSkillSnapshot() {
1335
+ const scopeDir = this.location?.scopeDir ?? this.#discoveryContext().scopeDir;
1336
+ if (this.#activeBindingId) {
1337
+ return this.#prepareSkillSnapshot(this.#activeBindingId, scopeDir);
1338
+ }
1339
+ if (!this.#skills)
1340
+ return undefined;
1341
+ return this.#skills.snapshot(this.#discoveryContext());
1342
+ }
1343
+ async #prepareSkillSnapshot(bindingId, scopeDir) {
1344
+ if (!this.#skills)
1345
+ return undefined;
1346
+ const existing = this.#skillSnapshots.get(bindingId);
1347
+ await this.#slashRouter?.catalog.refreshFileCommands(scopeDir);
1348
+ if (existing && !this.#refreshSkills) {
1349
+ this.#slashRouter?.catalog.rebuildSkillCommands(existing);
1350
+ return existing;
1351
+ }
1352
+ const snapshot = await this.#skills.snapshot(this.#skillContext?.() ?? { scopeDir });
1353
+ this.#skillSnapshots.set(bindingId, snapshot);
1354
+ this.#slashRouter?.catalog.rebuildSkillCommands(snapshot);
1355
+ return snapshot;
1356
+ }
1357
+ #isAbandoned(turnId) {
1358
+ return turnId !== undefined && this.#turnId !== turnId;
1359
+ }
1360
+ #releaseUnreadableLocation(error) {
1361
+ this.error = error instanceof Error ? error.message : String(error);
1362
+ this.#approvalGrants = [];
1363
+ if (!this.location)
1364
+ return;
1365
+ this.location = null;
1366
+ this.conversationStatus = null;
1367
+ this.conversationPinned = false;
1368
+ this.#onLocationChange?.(null);
1369
+ }
1370
+ #loadApprovalGrants(metadata) {
1371
+ this.#approvalGrants = [...(metadata.approvalGrants ?? [])];
1372
+ }
1373
+ async #rememberPersistentDecision(request, optionId) {
1374
+ if (request.origin === "app-tool")
1375
+ return;
1376
+ const decision = persistentDecisionFromOption(optionId);
1377
+ const name = approvalGrantIdentity(request);
1378
+ if (!decision || !name)
1379
+ return;
1380
+ this.#approvalGrants = upsertApprovalGrant(this.#approvalGrants, name, decision);
1381
+ if (!this.repository || !this.location)
1382
+ return;
1383
+ await this.repository.writeApprovalGrants(this.location, this.#approvalGrants);
1384
+ }
1385
+ async #ensureConversation() {
1386
+ if (!this.repository)
1387
+ return;
1388
+ if (this.location) {
1389
+ try {
1390
+ const snapshot = await this.repository.read(this.location);
1391
+ this.#loadApprovalGrants(snapshot.metadata);
1392
+ return;
1393
+ }
1394
+ catch (error) {
1395
+ if (!(error instanceof ConversationUnavailableError))
1396
+ throw error;
1397
+ this.location = null;
1398
+ this.conversationStatus = null;
1399
+ this.#approvalGrants = [];
1400
+ this.#onLocationChange?.(null);
1401
+ }
1402
+ }
1403
+ const followedScope = this.#followedScope ?? this.directoryContext;
1404
+ const input = this.#followedScope !== undefined || this.directoryContext
1405
+ ? (this.#createConversation?.(followedScope) ?? {
1406
+ scopeDir: followedScope,
1407
+ })
1408
+ : (this.#createConversation?.() ?? { scopeDir: "" });
1409
+ const created = await this.repository.create(input);
1410
+ this.location = created.location;
1411
+ this.conversationStatus = created.metadata.status;
1412
+ this.conversationPinned = Boolean(created.metadata.pinned);
1413
+ this.directoryContext = created.location.scopeDir;
1414
+ this.#followedScope = created.location.scopeDir;
1415
+ this.pickerEntries = [];
1416
+ this.#approvalGrants = [];
1417
+ this.#onLocationChange?.(this.location);
1418
+ }
1419
+ #activateNewBinding(bindingId, session, request, runtime = this.runtime, handoff, replacesBindingId = this.#activeBindingId) {
1420
+ const previousBindingId = this.#activeBindingId;
1421
+ const binding = {
1422
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1423
+ id: bindingId,
1424
+ type: "binding.created",
1425
+ createdAt: new Date().toISOString(),
1426
+ runtime: runtime.id,
1427
+ agent: request.agent,
1428
+ model: request.model ? { ...request.model } : undefined,
1429
+ thinking: request.thinking,
1430
+ nativeSessionId: session.id,
1431
+ handoffThroughEntryId: handoff?.throughEntryId,
1432
+ replacesBindingId,
1433
+ };
1434
+ const switchRecord = previousBindingId
1435
+ ? {
1436
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1437
+ id: `switch-${crypto.randomUUID()}`,
1438
+ type: "agent.switch",
1439
+ createdAt: binding.createdAt,
1440
+ agentBindingId: binding.id,
1441
+ fromBindingId: previousBindingId,
1442
+ toBindingId: binding.id,
1443
+ handoffId: handoff?.handoffId,
1444
+ handoffMode: handoff?.mode,
1445
+ handoffThroughEntryId: handoff?.throughEntryId,
1446
+ omittedEntryCount: handoff?.omittedEntryCount,
1447
+ }
1448
+ : undefined;
1449
+ this.#activeBinding = binding;
1450
+ this.#activeBindingId = binding.id;
1451
+ this.bindings = [...this.bindings, binding];
1452
+ return { binding, switchRecord };
1453
+ }
1454
+ async #prepareAppToolSession(bindingId, runtime, location, launchNotePath) {
1455
+ if (!this.#appToolBridge)
1456
+ return undefined;
1457
+ try {
1458
+ const descriptor = await this.#appToolBridge.prepare({
1459
+ conversationId: location.conversationId,
1460
+ agentBindingId: bindingId,
1461
+ scopeDir: location.scopeDir,
1462
+ launchNotePath,
1463
+ runtimeSupportsAppTools: runtime.id !== "fake" && runtime.capabilities().mcpTools,
1464
+ });
1465
+ this.appToolsUnavailableReason =
1466
+ descriptor.status === "runtime-unavailable" && runtime.id === "fake"
1467
+ ? null
1468
+ : (descriptor.unavailableReason ?? null);
1469
+ return descriptor;
1470
+ }
1471
+ catch (error) {
1472
+ this.appToolsUnavailableReason = `Application tools are unavailable. ${error instanceof Error ? error.message : String(error)}`;
1473
+ return undefined;
1474
+ }
1475
+ }
1476
+ async #closeAppToolBinding(bindingId = this.#activeBindingId) {
1477
+ if (!bindingId)
1478
+ return;
1479
+ await this.#appToolBridge?.closeBinding(bindingId);
1480
+ if (!this.#appToolBridge) {
1481
+ this.#appToolHost?.closeBinding(bindingId);
1482
+ }
1483
+ }
1484
+ async #detachSession(session) {
1485
+ if (!session)
1486
+ return;
1487
+ if (session.detach)
1488
+ await session.detach().catch(() => undefined);
1489
+ else
1490
+ await session.close().catch(() => undefined);
1491
+ }
1492
+ async #consumeAppToolEvent({ bindingId, event, }) {
1493
+ if (this.#activeBindingId &&
1494
+ bindingId !== this.#activeBindingId &&
1495
+ !this.bindings.some((binding) => binding.id === bindingId)) {
1496
+ return;
1497
+ }
1498
+ this.items = applyAgentEventToChatItems(this.items, event).map((item) => item.agentBindingId ? item : { ...item, agentBindingId: bindingId });
1499
+ const itemId = event.type === "permission.request"
1500
+ ? `approval-${event.request.id}`
1501
+ : event.type === "tool.start" || event.type === "tool.end"
1502
+ ? event.id
1503
+ : undefined;
1504
+ const item = itemId
1505
+ ? this.items.find((candidate) => candidate.id === itemId ||
1506
+ (candidate.type === "tool" && candidate.toolId === itemId))
1507
+ : undefined;
1508
+ if (this.repository && this.location && item) {
1509
+ await this.#appendDurableItems(this.location, [item], bindingId);
1510
+ }
1511
+ else if (!this.repository) {
1512
+ await this.#persist(false, bindingId);
1513
+ }
1514
+ }
1515
+ #bindingMatchesIdentity(binding, request, runtime = this.runtime) {
1516
+ return binding.runtime === runtime.id && binding.agent === request.agent;
1517
+ }
1518
+ #bindingMatchesConfiguration(binding, request) {
1519
+ return (binding.model?.provider === request.model?.provider &&
1520
+ binding.model?.model === request.model?.model &&
1521
+ binding.thinking === request.thinking);
1522
+ }
1523
+ async #configureBinding(session, binding, request) {
1524
+ const modelChanged = binding.model?.provider !== request.model?.provider ||
1525
+ binding.model?.model !== request.model?.model;
1526
+ const thinkingChanged = binding.thinking !== request.thinking;
1527
+ if (!modelChanged && !thinkingChanged)
1528
+ return { binding };
1529
+ if (!session.configure)
1530
+ return undefined;
1531
+ try {
1532
+ const result = await session.configure({
1533
+ ...(modelChanged ? { model: request.model } : {}),
1534
+ ...(thinkingChanged ? { thinking: request.thinking } : {}),
1535
+ });
1536
+ if ((modelChanged &&
1537
+ result.model?.status !== "applied" &&
1538
+ result.model?.status !== "unchanged") ||
1539
+ (thinkingChanged &&
1540
+ result.thinking?.status !== "applied" &&
1541
+ result.thinking?.status !== "unchanged")) {
1542
+ return undefined;
1543
+ }
1544
+ }
1545
+ catch {
1546
+ return undefined;
1547
+ }
1548
+ const updated = {
1549
+ ...binding,
1550
+ ...(modelChanged
1551
+ ? { model: request.model ? { ...request.model } : undefined }
1552
+ : {}),
1553
+ ...(thinkingChanged ? { thinking: request.thinking } : {}),
1554
+ };
1555
+ const createdAt = new Date().toISOString();
1556
+ return {
1557
+ binding: updated,
1558
+ pendingConfiguration: {
1559
+ previous: binding,
1560
+ updated,
1561
+ agentRecord: {
1562
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1563
+ id: `config-${crypto.randomUUID()}`,
1564
+ type: "binding.config.updated",
1565
+ createdAt,
1566
+ agentBindingId: binding.id,
1567
+ ...(modelChanged && request.model
1568
+ ? { model: { ...request.model } }
1569
+ : {}),
1570
+ ...(thinkingChanged && request.thinking
1571
+ ? { thinking: request.thinking }
1572
+ : {}),
1573
+ },
1574
+ transcriptEntry: {
1575
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1576
+ id: `agent-config-${crypto.randomUUID()}`,
1577
+ type: "agent.config",
1578
+ createdAt,
1579
+ agentBindingId: binding.id,
1580
+ ...(modelChanged && request.model
1581
+ ? { model: { ...request.model } }
1582
+ : {}),
1583
+ ...(thinkingChanged && request.thinking
1584
+ ? { thinking: request.thinking }
1585
+ : {}),
1586
+ },
1587
+ },
1588
+ };
1589
+ }
1590
+ async #prepareSession(request, turnId) {
1591
+ if (this.#isAbandoned(turnId))
1592
+ return;
1593
+ await this.#terminalCheckpoint;
1594
+ if (this.#isAbandoned(turnId))
1595
+ return;
1596
+ const targetRuntime = this.#selectRuntime
1597
+ ? await this.#selectRuntime({
1598
+ ...request,
1599
+ prompt: "",
1600
+ mcpServers: request.mcpServers ?? this.mcpServers,
1601
+ })
1602
+ : this.runtime;
1603
+ if (this.#isAbandoned(turnId))
1604
+ return;
1605
+ const refreshRequiresReplacement = this.#refreshSkills ||
1606
+ Boolean(request.skillActivations && request.skillActivations.length > 0);
1607
+ let activeConfigurationFailed = false;
1608
+ if (this.session &&
1609
+ this.#activeBinding &&
1610
+ !refreshRequiresReplacement &&
1611
+ this.#bindingMatchesIdentity(this.#activeBinding, request, targetRuntime)) {
1612
+ const configured = await this.#configureBinding(this.session, this.#activeBinding, request);
1613
+ if (configured) {
1614
+ this.bindings = this.bindings.map((binding) => binding.id === configured.binding.id ? configured.binding : binding);
1615
+ this.#activeBinding = configured.binding;
1616
+ if (configured.pendingConfiguration) {
1617
+ const context = this.#sessionContexts.get(this.session) ?? {
1618
+ location: this.location ? { ...this.location } : null,
1619
+ pendingItems: [],
1620
+ };
1621
+ context.pendingConfiguration = configured.pendingConfiguration;
1622
+ this.#sessionContexts.set(this.session, context);
1623
+ }
1624
+ await this.#prepareSkillSnapshot(configured.binding.id, this.location?.scopeDir ?? this.#discoveryContext().scopeDir);
1625
+ this.#sessionRequest = request;
1626
+ return;
1627
+ }
1628
+ activeConfigurationFailed = true;
1629
+ }
1630
+ const previousSession = this.session;
1631
+ const previousBindingId = this.#activeBindingId;
1632
+ this.#sessionRequest = request;
1633
+ let snapshot;
1634
+ let compatibleBinding;
1635
+ let failedCompatibleBindingId;
1636
+ if (this.repository && this.location) {
1637
+ snapshot = await this.repository.read(this.location);
1638
+ compatibleBinding = [...reduceAgentBindings(snapshot.agents)]
1639
+ .reverse()
1640
+ .find((binding) => this.#bindingMatchesIdentity(binding, request, targetRuntime) &&
1641
+ binding.id !==
1642
+ (activeConfigurationFailed ? previousBindingId : undefined) &&
1643
+ Boolean(binding.context) &&
1644
+ Boolean(binding.nativeSessionId));
1645
+ }
1646
+ if (!refreshRequiresReplacement &&
1647
+ compatibleBinding?.nativeSessionId &&
1648
+ compatibleBinding.context &&
1649
+ snapshot &&
1650
+ targetRuntime.capabilities().resume &&
1651
+ targetRuntime.resume) {
1652
+ let resumed;
1653
+ try {
1654
+ const handoff = await buildConversationContextHandoff(snapshot.transcript, {
1655
+ conversationId: snapshot.metadata.id,
1656
+ targetBindingId: compatibleBinding.id,
1657
+ after: {
1658
+ entryId: compatibleBinding.context.throughEntryId,
1659
+ entryHash: compatibleBinding.context.throughEntryHash,
1660
+ },
1661
+ bindings: reduceAgentBindings(snapshot.agents),
1662
+ summaries: snapshot.agents.filter((record) => record.type === "handoff.summary.created"),
1663
+ });
1664
+ const appToolSession = await this.#prepareAppToolSession(compatibleBinding.id, targetRuntime, snapshot.location, snapshot.metadata.launchContext?.notePath);
1665
+ const skillSnapshot = await this.#prepareSkillSnapshot(compatibleBinding.id, snapshot.location.scopeDir);
1666
+ resumed = await targetRuntime.resume(compatibleBinding.nativeSessionId, {
1667
+ ...request,
1668
+ appToolSession,
1669
+ skillSnapshot,
1670
+ });
1671
+ const configured = await this.#configureBinding(resumed, compatibleBinding, request);
1672
+ if (!configured)
1673
+ throw new Error("Session configuration is unsupported.");
1674
+ if (this.#isAbandoned(turnId)) {
1675
+ await resumed.close().catch(() => undefined);
1676
+ await this.#closeAppToolBinding(compatibleBinding.id);
1677
+ return;
1678
+ }
1679
+ if (previousBindingId && previousBindingId !== compatibleBinding.id) {
1680
+ await this.#closeAppToolBinding(previousBindingId);
1681
+ await this.#detachSession(previousSession);
1682
+ }
1683
+ const switchRecord = previousBindingId !== compatibleBinding.id
1684
+ ? {
1685
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1686
+ id: `switch-${crypto.randomUUID()}`,
1687
+ type: "agent.switch",
1688
+ createdAt: new Date().toISOString(),
1689
+ agentBindingId: compatibleBinding.id,
1690
+ fromBindingId: previousBindingId,
1691
+ toBindingId: compatibleBinding.id,
1692
+ handoffId: handoff?.handoffId,
1693
+ handoffMode: handoff?.mode,
1694
+ handoffThroughEntryId: handoff?.throughEntryId,
1695
+ omittedEntryCount: handoff?.omittedEntryCount,
1696
+ }
1697
+ : undefined;
1698
+ this.runtime = targetRuntime;
1699
+ this.session = resumed;
1700
+ this.#activeBinding = configured.binding;
1701
+ this.#activeBindingId = configured.binding.id;
1702
+ this.bindings = this.bindings.map((binding) => binding.id === configured.binding.id ? configured.binding : binding);
1703
+ if (handoff)
1704
+ this.#pendingHandoffs.set(resumed, handoff);
1705
+ this.#sessionContexts.set(resumed, {
1706
+ location: { ...snapshot.location },
1707
+ ...(switchRecord
1708
+ ? {
1709
+ pendingSwitch: switchRecord,
1710
+ pendingActivationBindingId: configured.binding.id,
1711
+ }
1712
+ : {}),
1713
+ ...(configured.pendingConfiguration
1714
+ ? { pendingConfiguration: configured.pendingConfiguration }
1715
+ : {}),
1716
+ ...(handoff ? { handoff } : {}),
1717
+ pendingItems: [],
1718
+ });
1719
+ void this.#consume(resumed, configured.binding.id);
1720
+ return;
1721
+ }
1722
+ catch {
1723
+ await resumed?.close().catch(() => undefined);
1724
+ await this.#closeAppToolBinding(compatibleBinding.id);
1725
+ failedCompatibleBindingId = compatibleBinding.id;
1726
+ compatibleBinding = undefined;
1727
+ }
1728
+ }
1729
+ const bindingId = `binding-${crypto.randomUUID()}`;
1730
+ const handoff = snapshot && previousBindingId
1731
+ ? await buildConversationContextHandoff(snapshot.transcript, {
1732
+ conversationId: snapshot.metadata.id,
1733
+ targetBindingId: bindingId,
1734
+ bindings: reduceAgentBindings(snapshot.agents),
1735
+ summaries: snapshot.agents.filter((record) => record.type === "handoff.summary.created"),
1736
+ })
1737
+ : undefined;
1738
+ const skillSnapshot = await this.#prepareSkillSnapshot(bindingId, snapshot?.location.scopeDir ?? this.location?.scopeDir ?? "");
1739
+ const appToolLocation = snapshot?.location ?? this.location;
1740
+ const appToolSession = appToolLocation
1741
+ ? await this.#prepareAppToolSession(bindingId, targetRuntime, appToolLocation, snapshot?.metadata.launchContext?.notePath)
1742
+ : undefined;
1743
+ const preparedRequest = {
1744
+ ...request,
1745
+ skillSnapshot,
1746
+ metadata: {
1747
+ ...request.metadata,
1748
+ ...(skillSnapshot
1749
+ ? {
1750
+ availableSkillsManifest: buildAvailableSkillsManifest(skillSnapshot),
1751
+ }
1752
+ : {}),
1753
+ ...(appToolSession?.tools.length
1754
+ ? {
1755
+ availableAppTools: appToolSession.tools.map((tool) => tool.name),
1756
+ }
1757
+ : {}),
1758
+ ...(await this.#sessionBootstrapMetadata(snapshot?.location.scopeDir ?? this.location?.scopeDir ?? "", snapshot?.metadata.launchContext?.notePath ??
1759
+ this.#createConversation?.()?.launchNotePath, snapshot?.metadata.id ?? this.location?.conversationId, appToolSession?.tools ?? [], skillSnapshot?.skills ?? [])),
1760
+ },
1761
+ };
1762
+ if (this.#isAbandoned(turnId)) {
1763
+ await this.#closeAppToolBinding(bindingId);
1764
+ return;
1765
+ }
1766
+ let started;
1767
+ try {
1768
+ started = await targetRuntime.start({
1769
+ ...preparedRequest,
1770
+ prompt: "",
1771
+ mcpServers: preparedRequest.mcpServers ?? this.mcpServers,
1772
+ appToolSession,
1773
+ });
1774
+ }
1775
+ catch (error) {
1776
+ await this.#closeAppToolBinding(bindingId);
1777
+ throw error;
1778
+ }
1779
+ if (this.#isAbandoned(turnId)) {
1780
+ this.#cancelledSessions.add(started);
1781
+ void started.cancel?.().catch(() => undefined);
1782
+ await started.close().catch(() => undefined);
1783
+ await this.#closeAppToolBinding(bindingId);
1784
+ return;
1785
+ }
1786
+ if (previousBindingId) {
1787
+ await this.#closeAppToolBinding(previousBindingId);
1788
+ await this.#detachSession(previousSession);
1789
+ }
1790
+ const { binding, switchRecord } = this.#activateNewBinding(bindingId, started, request, targetRuntime, handoff, failedCompatibleBindingId ?? compatibleBinding?.id ?? previousBindingId);
1791
+ if (handoff)
1792
+ this.#pendingHandoffs.set(started, handoff);
1793
+ this.#sessionContexts.set(started, {
1794
+ location: this.location ? { ...this.location } : null,
1795
+ ...(this.repository ? { pendingBinding: binding } : {}),
1796
+ ...(this.repository && switchRecord
1797
+ ? { pendingSwitch: switchRecord }
1798
+ : {}),
1799
+ ...(handoff ? { handoff } : {}),
1800
+ pendingItems: [],
1801
+ });
1802
+ this.runtime = targetRuntime;
1803
+ this.session = started;
1804
+ this.#refreshSkills = false;
1805
+ void this.#consume(started, this.#activeBindingId);
1806
+ }
1807
+ async #appendDurableItems(location, items, agentBindingId = this.#activeBindingId, busy = false, interrupted = false, namespace) {
1808
+ if (!this.repository)
1809
+ return;
1810
+ const durableItems = namespace
1811
+ ? namespaceChatItems(items, namespace)
1812
+ : items;
1813
+ const entries = projectChatItemsToTranscript(durableItems, {
1814
+ agentBindingId,
1815
+ }).filter((entry) => !(busy && entry.type === "message" && entry.role === "assistant"));
1816
+ if (interrupted) {
1817
+ entries.push({
1818
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1819
+ id: `cancelled-${crypto.randomUUID()}`,
1820
+ type: "cancelled",
1821
+ text: "Agent turn interrupted",
1822
+ createdAt: new Date().toISOString(),
1823
+ agentBindingId,
1824
+ });
1825
+ for (const item of durableItems) {
1826
+ if ((item.type === "approval" || item.type === "question") &&
1827
+ item.status === "cancelled") {
1828
+ entries.push({
1829
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1830
+ id: `${item.id}:cancelled`,
1831
+ type: "cancelled",
1832
+ createdAt: new Date().toISOString(),
1833
+ requestId: item.request.id,
1834
+ interactionType: item.type,
1835
+ agentBindingId,
1836
+ });
1837
+ }
1838
+ }
1839
+ }
1840
+ if (entries.length > 0) {
1841
+ await this.repository.appendTranscript(location, entries);
1842
+ }
1843
+ }
1844
+ async #appendUsage(location, agentBindingId, usage) {
1845
+ if (!this.repository)
1846
+ return;
1847
+ await this.repository.appendAgentRecords(location, [
1848
+ {
1849
+ schemaVersion: CONVERSATION_SCHEMA_VERSION,
1850
+ id: `usage-${crypto.randomUUID()}`,
1851
+ type: "usage.updated",
1852
+ createdAt: new Date().toISOString(),
1853
+ agentBindingId,
1854
+ usage: { ...usage },
1855
+ },
1856
+ ]);
1857
+ }
1858
+ async #persist(interrupted = false, agentBindingId = this.#activeBindingId) {
1859
+ if (this.repository) {
1860
+ if (!this.location)
1861
+ return;
1862
+ const location = { ...this.location };
1863
+ const items = [...this.items];
1864
+ const busy = this.busy;
1865
+ this.#persistQueue = this.#persistQueue.then(async () => {
1866
+ if (!this.repository)
1867
+ return;
1868
+ await this.#appendDurableItems(location, items, agentBindingId, busy, interrupted);
1869
+ if (!busy && this.usage && this.#usageDirty && agentBindingId) {
1870
+ await this.#appendUsage(location, agentBindingId, this.usage);
1871
+ this.#usageDirty = false;
1872
+ }
1873
+ });
1874
+ await this.#persistQueue;
1875
+ return;
1876
+ }
1877
+ if (!this.store)
1878
+ return;
1879
+ this.#persistQueue = this.#persistQueue.then(async () => {
1880
+ const snapshot = snapshotStoredChatSession({
1881
+ id: this.sessionId,
1882
+ runtime: this.runtime.id,
1883
+ runtimeSessionId: this.session?.id ?? this.sessionId,
1884
+ workspace: this.workspace,
1885
+ agent: this.#sessionRequest.agent,
1886
+ model: this.#sessionRequest.model,
1887
+ thinking: this.#sessionRequest.thinking,
1888
+ usage: this.usage ?? undefined,
1889
+ items: this.items,
1890
+ createdAt: this.#createdAt,
1891
+ interrupted,
1892
+ });
1893
+ this.#createdAt = snapshot.createdAt;
1894
+ await this.store?.save(snapshot);
1895
+ });
1896
+ await this.#persistQueue;
1897
+ }
1898
+ }
1899
+ const CANCELLED_NOTICE = "Agent turn cancelled";
1900
+ function waitForNextTask() {
1901
+ return new Promise((resolve) => globalThis.setTimeout(resolve, 0));
1902
+ }
1903
+ function portableWorkspaceLabel(value) {
1904
+ const label = value?.trim();
1905
+ if (!label || hasHostFilesystemPath(label))
1906
+ return undefined;
1907
+ return label;
1908
+ }
1909
+ function readAttachmentPaths(value) {
1910
+ if (!Array.isArray(value))
1911
+ return [];
1912
+ return value.filter((item) => typeof item === "string");
1913
+ }
1914
+ function skillInventoryPath(skill) {
1915
+ if (!skill || skill.source === "programmatic")
1916
+ return undefined;
1917
+ if (skill.root.includes(":") || skill.root.startsWith("bundled/")) {
1918
+ return undefined;
1919
+ }
1920
+ return skill.root.endsWith("SKILL.md")
1921
+ ? skill.root
1922
+ : `${skill.root}/SKILL.md`;
1923
+ }
1924
+ function namespaceChatItems(items, namespace) {
1925
+ return items.map((item) => ({ ...item, id: `${namespace}:${item.id}` }));
1926
+ }