wepscli 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 (130) hide show
  1. package/README.md +293 -0
  2. package/dist/WEPSCLI-shell/agent-runtime.js +824 -0
  3. package/dist/WEPSCLI-shell/agent-runtime.js.map +1 -0
  4. package/dist/WEPSCLI-shell/approval-overlay.js +275 -0
  5. package/dist/WEPSCLI-shell/approval-overlay.js.map +1 -0
  6. package/dist/WEPSCLI-shell/chat-components.js +760 -0
  7. package/dist/WEPSCLI-shell/chat-components.js.map +1 -0
  8. package/dist/WEPSCLI-shell/components.js +850 -0
  9. package/dist/WEPSCLI-shell/components.js.map +1 -0
  10. package/dist/WEPSCLI-shell/config-overlays.js +205 -0
  11. package/dist/WEPSCLI-shell/config-overlays.js.map +1 -0
  12. package/dist/WEPSCLI-shell/debug-log.js +16 -0
  13. package/dist/WEPSCLI-shell/debug-log.js.map +1 -0
  14. package/dist/WEPSCLI-shell/file-change-preview.js +261 -0
  15. package/dist/WEPSCLI-shell/file-change-preview.js.map +1 -0
  16. package/dist/WEPSCLI-shell/helpers.js +112 -0
  17. package/dist/WEPSCLI-shell/helpers.js.map +1 -0
  18. package/dist/WEPSCLI-shell/index.js +3 -0
  19. package/dist/WEPSCLI-shell/index.js.map +1 -0
  20. package/dist/WEPSCLI-shell/provider-add-flow.js +406 -0
  21. package/dist/WEPSCLI-shell/provider-add-flow.js.map +1 -0
  22. package/dist/WEPSCLI-shell/run-wepscli-shell.js +21 -0
  23. package/dist/WEPSCLI-shell/run-wepscli-shell.js.map +1 -0
  24. package/dist/WEPSCLI-shell/runtime-recovery.js +37 -0
  25. package/dist/WEPSCLI-shell/runtime-recovery.js.map +1 -0
  26. package/dist/WEPSCLI-shell/runtime-status.js +66 -0
  27. package/dist/WEPSCLI-shell/runtime-status.js.map +1 -0
  28. package/dist/WEPSCLI-shell/shell-app.js +1047 -0
  29. package/dist/WEPSCLI-shell/shell-app.js.map +1 -0
  30. package/dist/WEPSCLI-shell/shell-modes.js +77 -0
  31. package/dist/WEPSCLI-shell/shell-modes.js.map +1 -0
  32. package/dist/WEPSCLI-shell/slash-commands.js +135 -0
  33. package/dist/WEPSCLI-shell/slash-commands.js.map +1 -0
  34. package/dist/WEPSCLI-shell/theme.js +19 -0
  35. package/dist/WEPSCLI-shell/theme.js.map +1 -0
  36. package/dist/WEPSCLI-shell/tool-approval.js +85 -0
  37. package/dist/WEPSCLI-shell/tool-approval.js.map +1 -0
  38. package/dist/WEPSCLI-shell/tool-diff.js +76 -0
  39. package/dist/WEPSCLI-shell/tool-diff.js.map +1 -0
  40. package/dist/WEPSCLI-shell/tool-file-changes.js +268 -0
  41. package/dist/WEPSCLI-shell/tool-file-changes.js.map +1 -0
  42. package/dist/WEPSCLI-shell/tool-message-detail.js +138 -0
  43. package/dist/WEPSCLI-shell/tool-message-detail.js.map +1 -0
  44. package/dist/WEPSCLI-shell/tool-messages.js +145 -0
  45. package/dist/WEPSCLI-shell/tool-messages.js.map +1 -0
  46. package/dist/WEPSCLI-shell/transcript-panel.js +372 -0
  47. package/dist/WEPSCLI-shell/transcript-panel.js.map +1 -0
  48. package/dist/WEPSCLI-shell/transcript-state.js +62 -0
  49. package/dist/WEPSCLI-shell/transcript-state.js.map +1 -0
  50. package/dist/WEPSCLI-shell/types.js +1 -0
  51. package/dist/WEPSCLI-shell/types.js.map +1 -0
  52. package/dist/cli.js +11 -0
  53. package/dist/cli.js.map +1 -0
  54. package/dist/config.js +40 -0
  55. package/dist/config.js.map +1 -0
  56. package/dist/index.js +4 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/main.js +140 -0
  59. package/dist/main.js.map +1 -0
  60. package/dist/onboarding/action-screen.js +90 -0
  61. package/dist/onboarding/action-screen.js.map +1 -0
  62. package/dist/onboarding/framed-screen.js +35 -0
  63. package/dist/onboarding/framed-screen.js.map +1 -0
  64. package/dist/onboarding/onboarding-app.js +312 -0
  65. package/dist/onboarding/onboarding-app.js.map +1 -0
  66. package/dist/onboarding/run-onboarding.js +23 -0
  67. package/dist/onboarding/run-onboarding.js.map +1 -0
  68. package/dist/onboarding/select-screen.js +21 -0
  69. package/dist/onboarding/select-screen.js.map +1 -0
  70. package/dist/onboarding/summary-screen.js +23 -0
  71. package/dist/onboarding/summary-screen.js.map +1 -0
  72. package/dist/onboarding/text-input-screen.js +55 -0
  73. package/dist/onboarding/text-input-screen.js.map +1 -0
  74. package/dist/onboarding/theme.js +26 -0
  75. package/dist/onboarding/theme.js.map +1 -0
  76. package/dist/provider-profiles/api-key-store.js +51 -0
  77. package/dist/provider-profiles/api-key-store.js.map +1 -0
  78. package/dist/provider-profiles/defaults.js +18 -0
  79. package/dist/provider-profiles/defaults.js.map +1 -0
  80. package/dist/provider-profiles/fetch-models.js +53 -0
  81. package/dist/provider-profiles/fetch-models.js.map +1 -0
  82. package/dist/provider-profiles/index.js +6 -0
  83. package/dist/provider-profiles/index.js.map +1 -0
  84. package/dist/provider-profiles/provider-profile-service.js +223 -0
  85. package/dist/provider-profiles/provider-profile-service.js.map +1 -0
  86. package/dist/provider-profiles/providers-config-store.js +17 -0
  87. package/dist/provider-profiles/providers-config-store.js.map +1 -0
  88. package/dist/provider-profiles/types.js +1 -0
  89. package/dist/provider-profiles/types.js.map +1 -0
  90. package/dist/session-history/session-history-service.js +142 -0
  91. package/dist/session-history/session-history-service.js.map +1 -0
  92. package/dist/shell/animator.js +30 -0
  93. package/dist/shell/animator.js.map +1 -0
  94. package/dist/shell/clickables.js +101 -0
  95. package/dist/shell/clickables.js.map +1 -0
  96. package/dist/shell/dashboard-shell.js +292 -0
  97. package/dist/shell/dashboard-shell.js.map +1 -0
  98. package/dist/shell/index.js +5 -0
  99. package/dist/shell/index.js.map +1 -0
  100. package/dist/shell/keymap.js +14 -0
  101. package/dist/shell/keymap.js.map +1 -0
  102. package/dist/shell/mouse.js +39 -0
  103. package/dist/shell/mouse.js.map +1 -0
  104. package/dist/shell/render.js +122 -0
  105. package/dist/shell/render.js.map +1 -0
  106. package/dist/shell/run-shell.js +36 -0
  107. package/dist/shell/run-shell.js.map +1 -0
  108. package/dist/shell/theme.js +56 -0
  109. package/dist/shell/theme.js.map +1 -0
  110. package/dist/storage/locked-json-file.js +88 -0
  111. package/dist/storage/locked-json-file.js.map +1 -0
  112. package/dist/workbench/animator.js +30 -0
  113. package/dist/workbench/animator.js.map +1 -0
  114. package/dist/workbench/index.js +6 -0
  115. package/dist/workbench/index.js.map +1 -0
  116. package/dist/workbench/mouse.js +39 -0
  117. package/dist/workbench/mouse.js.map +1 -0
  118. package/dist/workbench/render.js +82 -0
  119. package/dist/workbench/render.js.map +1 -0
  120. package/dist/workbench/renderer.js +364 -0
  121. package/dist/workbench/renderer.js.map +1 -0
  122. package/dist/workbench/run-workbench.js +36 -0
  123. package/dist/workbench/run-workbench.js.map +1 -0
  124. package/dist/workbench/theme.js +63 -0
  125. package/dist/workbench/theme.js.map +1 -0
  126. package/dist/workbench/types.js +1 -0
  127. package/dist/workbench/types.js.map +1 -0
  128. package/dist/workbench/workbench-shell.js +649 -0
  129. package/dist/workbench/workbench-shell.js.map +1 -0
  130. package/package.json +65 -0
@@ -0,0 +1,824 @@
1
+ import { join } from "node:path";
2
+ import { getAgentDir } from "../config.js";
3
+ import { createCompactingRuntimeState, createErrorRuntimeState, createIdleRuntimeState, createInterruptedRuntimeState, createRetryingRuntimeState, createRunningRuntimeState } from "./runtime-status.js";
4
+ import { getRuntimeRecoveryHint } from "./runtime-recovery.js";
5
+ import { createToolApprovalRequest, toolApprovalDecisionReason } from "./tool-approval.js";
6
+ import { createToolMessageState, updateToolMessageState } from "./tool-messages.js";
7
+ let codingAgentRuntimePromise;
8
+ async function loadCodingAgentRuntime() {
9
+ if (!codingAgentRuntimePromise) {
10
+ codingAgentRuntimePromise = import("@mariozechner/pi-coding-agent").then(module => module);
11
+ }
12
+ return codingAgentRuntimePromise;
13
+ }
14
+ function formatTime(timestamp) {
15
+ return new Date(timestamp ?? Date.now()).toLocaleTimeString("en-US", {
16
+ hour: "2-digit",
17
+ minute: "2-digit"
18
+ });
19
+ }
20
+ function createMessageId(record, role) {
21
+ record.sequence += 1;
22
+ return `runtime:${role}:${record.sequence}`;
23
+ }
24
+ function guessReasoningSupport(modelId) {
25
+ const value = modelId.toLowerCase();
26
+ return ["gpt-5", "gpt-4.1", "o1", "o3", "o4", "claude-3-7", "claude-sonnet-4", "claude-opus-4", "gemini-2.5", "reasoner", "thinking"].some(token => value.includes(token));
27
+ }
28
+ function extractTextBlocks(content) {
29
+ return content.map(item => {
30
+ if (item.type === "text" && typeof item.text === "string") {
31
+ return item.text;
32
+ }
33
+ if (item.type === "image") {
34
+ return "[image]";
35
+ }
36
+ if (item.type === "thinking" && typeof item.thinking === "string") {
37
+ return item.thinking;
38
+ }
39
+ return "";
40
+ }).filter(Boolean).join("\n\n");
41
+ }
42
+ function extractUserText(message) {
43
+ if (typeof message.content === "string") {
44
+ return message.content;
45
+ }
46
+ return extractTextBlocks(message.content);
47
+ }
48
+ function extractAssistantVisibleText(message) {
49
+ const text = extractTextBlocks(message.content.filter(item => item.type === "text").map(item => ({
50
+ type: item.type,
51
+ text: item.text
52
+ })));
53
+ if (text) {
54
+ return text;
55
+ }
56
+ if (message.errorMessage) {
57
+ return message.errorMessage;
58
+ }
59
+ return "";
60
+ }
61
+ function extractAssistantReasoning(message) {
62
+ return extractTextBlocks(message.content.filter(item => item.type === "thinking").map(item => ({
63
+ type: item.type,
64
+ thinking: item.thinking
65
+ })));
66
+ }
67
+ function formatRuntimeError(error) {
68
+ return error instanceof Error ? error.message : String(error);
69
+ }
70
+ function formatCompactionResultMessage(result) {
71
+ if (typeof result.tokensBefore === "number" && Number.isFinite(result.tokensBefore)) {
72
+ return `Context compacted from ${result.tokensBefore.toLocaleString("en-US")} tokens.`;
73
+ }
74
+ return "Context compacted.";
75
+ }
76
+ function cancellationToolOutput(reason) {
77
+ return {
78
+ content: [{
79
+ type: "text",
80
+ text: reason
81
+ }]
82
+ };
83
+ }
84
+ function toModelDefinition(model) {
85
+ return {
86
+ id: model.id,
87
+ name: model.name,
88
+ reasoning: guessReasoningSupport(model.id),
89
+ input: ["text", "image"],
90
+ cost: {
91
+ input: 0,
92
+ output: 0,
93
+ cacheRead: 0,
94
+ cacheWrite: 0
95
+ },
96
+ contextWindow: 128_000,
97
+ maxTokens: 16_384
98
+ };
99
+ }
100
+ function getRuntimeSessionDir(cwd, agentDir) {
101
+ const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
102
+ return join(agentDir, "sessions", safePath);
103
+ }
104
+ function installBeforeToolCallHook(session, handler) {
105
+ if (typeof session.addBeforeToolCall === "function") {
106
+ return session.addBeforeToolCall(handler);
107
+ }
108
+ if (typeof session.agent?.setBeforeToolCall === "function") {
109
+ session.agent.setBeforeToolCall(handler);
110
+ return () => {
111
+ session.agent.setBeforeToolCall?.(undefined);
112
+ };
113
+ }
114
+ throw new Error("The installed pi-coding-agent runtime does not expose a before-tool-call hook API.");
115
+ }
116
+ export class WepsAgentRuntime {
117
+ records = new Map();
118
+ pending = new Map();
119
+ pendingApprovals = new Map();
120
+ currentMode = "agent";
121
+ approvalSequence = 0;
122
+ constructor(profileService, callbacks, options = {}) {
123
+ this.profileService = profileService;
124
+ this.callbacks = callbacks;
125
+ this.cwd = options.cwd ?? process.cwd();
126
+ this.agentDir = options.agentDir ?? getAgentDir();
127
+ }
128
+ async prompt(sessionId, text, selection, binding = {}) {
129
+ const record = await this.ensureSession(sessionId, selection, binding);
130
+ record.activePrompts += 1;
131
+ this.setRuntimeState(sessionId, record, createRunningRuntimeState());
132
+ try {
133
+ await this.applySelection(record, selection);
134
+ await record.session.prompt(text, record.session.isStreaming ? {
135
+ streamingBehavior: "steer"
136
+ } : undefined);
137
+ } catch (error) {
138
+ if (!this.isAbortLikeError(error)) {
139
+ const message = formatRuntimeError(error);
140
+ this.appendSystemMessage(sessionId, record, message);
141
+ this.applyRecoveryState(sessionId, record, message, "Request failed - ready");
142
+ }
143
+ } finally {
144
+ record.activePrompts = Math.max(0, record.activePrompts - 1);
145
+ this.resetRuntimeStateAfterActivity(sessionId, record);
146
+ }
147
+ }
148
+ async loadSession(sessionId, selection, binding = {}) {
149
+ await this.ensureSession(sessionId, selection, binding);
150
+ }
151
+ async abort(sessionId) {
152
+ const record = this.records.get(sessionId);
153
+ if (!record) {
154
+ return false;
155
+ }
156
+ const hasPendingApproval = [...this.pendingApprovals.values()].some(pending => pending.sessionId === sessionId);
157
+ if (!hasPendingApproval && record.activePrompts === 0) {
158
+ return false;
159
+ }
160
+ record.session.abortCompaction?.();
161
+ await record.session.abort().catch(() => {});
162
+ this.cleanupAfterInterruption(sessionId, record, "Cancelled before completion.");
163
+ this.appendSystemMessage(sessionId, record, "Request interrupted. You can continue with a new prompt.");
164
+ this.setRuntimeState(sessionId, record, createInterruptedRuntimeState("Interrupted - ready", "The current request was cancelled."));
165
+ record.activePrompts = 0;
166
+ return true;
167
+ }
168
+ async compact(sessionId, selection, binding = {}, customInstructions) {
169
+ const record = await this.ensureSession(sessionId, selection, binding);
170
+ const compactSession = record.session;
171
+ if (typeof compactSession.compact !== "function") {
172
+ this.appendSystemMessage(sessionId, record, "Manual compaction is not available in the current runtime.");
173
+ this.setRuntimeState(sessionId, record, createErrorRuntimeState("Compaction unavailable - ready"));
174
+ return false;
175
+ }
176
+ record.activePrompts += 1;
177
+ this.setRuntimeState(sessionId, record, createCompactingRuntimeState("Compacting context"));
178
+ try {
179
+ await this.applySelection(record, selection);
180
+ const result = await compactSession.compact(customInstructions);
181
+ this.appendSystemMessage(sessionId, record, formatCompactionResultMessage(result ?? {}));
182
+ this.setRuntimeState(sessionId, record, createIdleRuntimeState());
183
+ return true;
184
+ } catch (error) {
185
+ if (this.isAbortLikeError(error)) {
186
+ this.appendSystemMessage(sessionId, record, "Compaction cancelled.");
187
+ this.setRuntimeState(sessionId, record, createInterruptedRuntimeState("Compaction cancelled - ready", "You can continue with a new prompt."));
188
+ return false;
189
+ }
190
+ const message = formatRuntimeError(error);
191
+ this.appendSystemMessage(sessionId, record, `Compaction failed: ${message}`);
192
+ this.applyRecoveryState(sessionId, record, message, "Compaction failed - ready");
193
+ return false;
194
+ } finally {
195
+ record.activePrompts = Math.max(0, record.activePrompts - 1);
196
+ this.resetRuntimeStateAfterActivity(sessionId, record);
197
+ }
198
+ }
199
+ async syncSelection(sessionId, selection) {
200
+ const record = this.records.get(sessionId);
201
+ if (!record) {
202
+ return;
203
+ }
204
+ try {
205
+ await this.applySelection(record, selection);
206
+ } catch (error) {
207
+ const message = formatRuntimeError(error);
208
+ this.appendSystemMessage(sessionId, record, message);
209
+ this.applyRecoveryState(sessionId, record, message, "Selection failed - ready");
210
+ }
211
+ }
212
+ setMode(mode) {
213
+ this.currentMode = mode;
214
+ }
215
+ dispose() {
216
+ for (const [requestId, pendingApproval] of this.pendingApprovals.entries()) {
217
+ pendingApproval.resolve("cancel");
218
+ this.callbacks.closeApproval(pendingApproval.sessionId, requestId);
219
+ this.pendingApprovals.delete(requestId);
220
+ }
221
+ for (const [sessionId, record] of this.records.entries()) {
222
+ record.unsubscribe();
223
+ record.removeBeforeToolCallHook();
224
+ void record.session.abort().catch(() => {});
225
+ record.session.dispose();
226
+ this.records.delete(sessionId);
227
+ }
228
+ }
229
+ async ensureSession(sessionId, selection, binding = {}) {
230
+ const existing = this.records.get(sessionId);
231
+ if (existing) {
232
+ return existing;
233
+ }
234
+ const pending = this.pending.get(sessionId);
235
+ if (pending) {
236
+ return pending;
237
+ }
238
+ const promise = this.createSessionRecord(sessionId, selection, binding).then(record => {
239
+ this.records.set(sessionId, record);
240
+ return record;
241
+ }).finally(() => {
242
+ this.pending.delete(sessionId);
243
+ });
244
+ this.pending.set(sessionId, promise);
245
+ return promise;
246
+ }
247
+ async createSessionRecord(sessionId, selection, binding) {
248
+ const codingAgent = await loadCodingAgentRuntime();
249
+ const resolved = this.resolveSelection(selection);
250
+ const authStorage = codingAgent.AuthStorage.inMemory();
251
+ const modelRegistry = new codingAgent.ModelRegistry(authStorage, join(this.agentDir, "runtime-models.json"));
252
+ const settingsManager = codingAgent.SettingsManager.inMemory();
253
+ const sessionDir = getRuntimeSessionDir(this.cwd, this.agentDir);
254
+ const sessionManager = binding.runtimeSessionFile ? codingAgent.SessionManager.open(binding.runtimeSessionFile, sessionDir) : codingAgent.SessionManager.create(this.cwd, sessionDir);
255
+ this.registerProfileProvider(modelRegistry, authStorage, resolved.profile, resolved.modelId, resolved.apiKey);
256
+ const model = modelRegistry.find(resolved.profile.id, resolved.modelId);
257
+ if (!model) {
258
+ throw new Error(`Unable to register model ${resolved.modelId} for ${resolved.profile.label}`);
259
+ }
260
+ const {
261
+ session,
262
+ modelFallbackMessage
263
+ } = await codingAgent.createAgentSession({
264
+ cwd: this.cwd,
265
+ agentDir: this.agentDir,
266
+ authStorage,
267
+ modelRegistry,
268
+ settingsManager,
269
+ sessionManager,
270
+ model
271
+ });
272
+ await session.bindExtensions({
273
+ commandContextActions: {
274
+ waitForIdle: () => session.agent.waitForIdle(),
275
+ newSession: async () => ({
276
+ cancelled: true
277
+ }),
278
+ fork: async () => ({
279
+ cancelled: true
280
+ }),
281
+ navigateTree: async () => ({
282
+ cancelled: true
283
+ }),
284
+ switchSession: async () => ({
285
+ cancelled: true
286
+ }),
287
+ reload: async () => {}
288
+ },
289
+ onError: error => {
290
+ const record = this.records.get(sessionId);
291
+ if (record) {
292
+ this.appendSystemMessage(sessionId, record, `Extension error (${error.extensionPath}): ${error.error}`);
293
+ }
294
+ }
295
+ });
296
+ const record = {
297
+ session,
298
+ unsubscribe: () => {},
299
+ removeBeforeToolCallHook: () => {},
300
+ modelRegistry,
301
+ authStorage,
302
+ activeProfileId: resolved.profile.id,
303
+ activeModelId: resolved.modelId,
304
+ streamingAssistantMessageId: undefined,
305
+ streamingReasoningMessageId: undefined,
306
+ toolMessageIds: new Map(),
307
+ toolStates: new Map(),
308
+ activePrompts: 0,
309
+ runtimeState: createIdleRuntimeState(),
310
+ sequence: 0
311
+ };
312
+ record.removeBeforeToolCallHook = installBeforeToolCallHook(session, async ({
313
+ toolCall,
314
+ args
315
+ }, signal) => this.handleApprovalRequest(sessionId, record, toolCall.id, toolCall.name, args, signal));
316
+ record.unsubscribe = session.subscribe(event => {
317
+ this.handleSessionEvent(sessionId, record, event);
318
+ });
319
+ const runtimeSessionFile = session.sessionManager.getSessionFile();
320
+ if (runtimeSessionFile && runtimeSessionFile !== binding.runtimeSessionFile) {
321
+ this.callbacks.updateSessionBinding(sessionId, {
322
+ runtimeSessionFile
323
+ });
324
+ }
325
+ const restoredMessages = this.rehydrateTranscript(record);
326
+ if (restoredMessages.length > 0) {
327
+ this.callbacks.replaceMessages(sessionId, restoredMessages);
328
+ }
329
+ if (modelFallbackMessage) {
330
+ this.appendSystemMessage(sessionId, record, modelFallbackMessage);
331
+ }
332
+ this.callbacks.updateRuntimeState(sessionId, record.runtimeState);
333
+ return record;
334
+ }
335
+ rehydrateTranscript(record) {
336
+ const restored = [];
337
+ for (const message of record.session.state.messages) {
338
+ if (message.role === "user") {
339
+ const text = extractUserText(message);
340
+ if (!text) continue;
341
+ restored.push({
342
+ id: createMessageId(record, "user"),
343
+ role: "user",
344
+ content: text,
345
+ time: formatTime(message.timestamp)
346
+ });
347
+ continue;
348
+ }
349
+ if (message.role === "assistant") {
350
+ const reasoning = extractAssistantReasoning(message);
351
+ if (reasoning) {
352
+ restored.push({
353
+ id: createMessageId(record, "assistant"),
354
+ role: "assistant",
355
+ content: reasoning,
356
+ time: formatTime(message.timestamp),
357
+ kind: "reasoning",
358
+ collapsible: true,
359
+ expanded: false
360
+ });
361
+ }
362
+ const content = extractAssistantVisibleText(message);
363
+ if (content) {
364
+ restored.push({
365
+ id: createMessageId(record, "assistant"),
366
+ role: "assistant",
367
+ content,
368
+ time: formatTime(message.timestamp)
369
+ });
370
+ }
371
+ continue;
372
+ }
373
+ if (message.role === "toolResult") {
374
+ const tool = updateToolMessageState(createToolMessageState(message.toolCallId, message.toolName, undefined), {
375
+ status: message.isError ? "failed" : "completed",
376
+ output: message
377
+ });
378
+ restored.push({
379
+ id: createMessageId(record, "system"),
380
+ role: "system",
381
+ content: "",
382
+ time: formatTime(message.timestamp),
383
+ kind: "tool",
384
+ collapsible: true,
385
+ expanded: false,
386
+ tool
387
+ });
388
+ }
389
+ }
390
+ return restored;
391
+ }
392
+ isAbortLikeError(error) {
393
+ const message = formatRuntimeError(error).toLowerCase();
394
+ return message.includes("aborted") || message.includes("cancelled") || message.includes("canceled");
395
+ }
396
+ setRuntimeState(sessionId, record, state) {
397
+ record.runtimeState = state;
398
+ this.callbacks.updateRuntimeState(sessionId, state);
399
+ }
400
+ resetRuntimeStateAfterActivity(sessionId, record) {
401
+ if (record.activePrompts > 0) {
402
+ return;
403
+ }
404
+ if (record.runtimeState.phase === "running" || record.runtimeState.phase === "retrying" || record.runtimeState.phase === "compacting") {
405
+ this.setRuntimeState(sessionId, record, createIdleRuntimeState());
406
+ }
407
+ }
408
+ cleanupAfterInterruption(sessionId, record, reason) {
409
+ for (const [requestId, pendingApproval] of [...this.pendingApprovals.entries()]) {
410
+ if (pendingApproval.sessionId !== sessionId) {
411
+ continue;
412
+ }
413
+ this.resolveApproval(requestId, "cancel");
414
+ }
415
+ for (const [toolCallId, tool] of record.toolStates.entries()) {
416
+ if (tool.status === "completed" || tool.status === "failed") {
417
+ continue;
418
+ }
419
+ const nextTool = updateToolMessageState(tool, {
420
+ status: "failed",
421
+ output: cancellationToolOutput(reason)
422
+ });
423
+ record.toolStates.set(toolCallId, nextTool);
424
+ const messageId = record.toolMessageIds.get(toolCallId);
425
+ if (messageId) {
426
+ this.callbacks.patchMessage(sessionId, messageId, {
427
+ tool: nextTool,
428
+ time: formatTime()
429
+ });
430
+ }
431
+ }
432
+ record.toolMessageIds.clear();
433
+ record.toolStates.clear();
434
+ record.streamingAssistantMessageId = undefined;
435
+ record.streamingReasoningMessageId = undefined;
436
+ }
437
+ resolveSelection(selection) {
438
+ const profileId = selection.profileId ?? this.profileService.getActiveSelection().profileId;
439
+ if (!profileId) {
440
+ throw new Error("No provider profile selected");
441
+ }
442
+ const profile = this.profileService.getProfile(profileId);
443
+ if (!profile) {
444
+ throw new Error(`Unknown provider profile: ${profileId}`);
445
+ }
446
+ const modelId = selection.modelId ?? profile.models[0]?.id;
447
+ if (!modelId) {
448
+ throw new Error(`No model configured for ${profile.label}`);
449
+ }
450
+ const apiKey = this.profileService.getApiKey(profile.id);
451
+ if (!apiKey) {
452
+ throw new Error(`No API key configured for ${profile.label}`);
453
+ }
454
+ return {
455
+ profile,
456
+ modelId,
457
+ apiKey
458
+ };
459
+ }
460
+ registerProfileProvider(modelRegistry, authStorage, profile, selectedModelId, apiKey) {
461
+ authStorage.setRuntimeApiKey(profile.id, apiKey);
462
+ const models = [...profile.models];
463
+ if (!models.some(model => model.id === selectedModelId)) {
464
+ models.push({
465
+ id: selectedModelId,
466
+ name: selectedModelId,
467
+ family: profile.family
468
+ });
469
+ }
470
+ modelRegistry.registerProvider(profile.id, {
471
+ baseUrl: profile.baseUrl,
472
+ apiKey,
473
+ api: profile.apiDialect,
474
+ models: models.map(model => toModelDefinition(model))
475
+ });
476
+ }
477
+ async applySelection(record, selection) {
478
+ const resolved = this.resolveSelection(selection);
479
+ if (record.activeProfileId === resolved.profile.id && record.activeModelId === resolved.modelId) {
480
+ return;
481
+ }
482
+ this.registerProfileProvider(record.modelRegistry, record.authStorage, resolved.profile, resolved.modelId, resolved.apiKey);
483
+ const model = record.modelRegistry.find(resolved.profile.id, resolved.modelId);
484
+ if (!model) {
485
+ throw new Error(`Model ${resolved.modelId} is unavailable for ${resolved.profile.label}`);
486
+ }
487
+ await record.session.setModel(model);
488
+ record.activeProfileId = resolved.profile.id;
489
+ record.activeModelId = resolved.modelId;
490
+ }
491
+ appendSystemMessage(sessionId, record, content) {
492
+ this.callbacks.appendMessage(sessionId, {
493
+ id: createMessageId(record, "system"),
494
+ role: "system",
495
+ content,
496
+ time: formatTime(),
497
+ kind: "status"
498
+ });
499
+ }
500
+ applyRecoveryState(sessionId, record, message, fallbackLabel) {
501
+ const hint = getRuntimeRecoveryHint(message);
502
+ this.setRuntimeState(sessionId, record, createErrorRuntimeState(hint?.label ?? fallbackLabel, message));
503
+ if (hint?.nextStep) {
504
+ this.appendSystemMessage(sessionId, record, hint.nextStep);
505
+ }
506
+ }
507
+ appendToolMessage(sessionId, record, tool) {
508
+ const id = createMessageId(record, "system");
509
+ this.callbacks.appendMessage(sessionId, {
510
+ id,
511
+ role: "system",
512
+ content: "",
513
+ time: formatTime(),
514
+ kind: "tool",
515
+ collapsible: true,
516
+ expanded: false,
517
+ tool
518
+ });
519
+ return id;
520
+ }
521
+ appendReasoningMessage(sessionId, record, content) {
522
+ this.callbacks.appendMessage(sessionId, {
523
+ id: createMessageId(record, "assistant"),
524
+ role: "assistant",
525
+ content,
526
+ time: formatTime(),
527
+ kind: "reasoning",
528
+ collapsible: true,
529
+ expanded: false
530
+ });
531
+ }
532
+ insertReasoningMessageBefore(sessionId, record, beforeMessageId, content) {
533
+ const id = createMessageId(record, "assistant");
534
+ this.callbacks.insertMessageBefore(sessionId, beforeMessageId, {
535
+ id,
536
+ role: "assistant",
537
+ content,
538
+ time: formatTime(),
539
+ kind: "reasoning",
540
+ collapsible: true,
541
+ expanded: false
542
+ });
543
+ return id;
544
+ }
545
+ resolveApproval(requestId, decision) {
546
+ const pendingApproval = this.pendingApprovals.get(requestId);
547
+ if (!pendingApproval) {
548
+ return;
549
+ }
550
+ this.pendingApprovals.delete(requestId);
551
+ this.callbacks.closeApproval(pendingApproval.sessionId, requestId);
552
+ pendingApproval.resolve(decision);
553
+ }
554
+ createApprovalId() {
555
+ this.approvalSequence += 1;
556
+ return `approval:${this.approvalSequence}`;
557
+ }
558
+ patchToolState(sessionId, record, toolCallId, toolName, update) {
559
+ const messageId = record.toolMessageIds.get(toolCallId);
560
+ const currentTool = record.toolStates.get(toolCallId) ?? createToolMessageState(toolCallId, toolName, update.args);
561
+ const nextTool = updateToolMessageState(currentTool, update);
562
+ record.toolStates.set(toolCallId, nextTool);
563
+ if (messageId) {
564
+ this.callbacks.patchMessage(sessionId, messageId, {
565
+ tool: nextTool
566
+ });
567
+ }
568
+ }
569
+ async handleApprovalRequest(sessionId, record, toolCallId, toolName, args, signal) {
570
+ const request = createToolApprovalRequest(this.createApprovalId(), sessionId, toolCallId, toolName, args);
571
+ if (!request) {
572
+ return undefined;
573
+ }
574
+ if (this.currentMode === "read-only") {
575
+ const reason = `${toolName} was blocked because read-only mode is active.`;
576
+ this.appendSystemMessage(sessionId, record, reason);
577
+ this.patchToolState(sessionId, record, toolCallId, toolName, {
578
+ status: "failed",
579
+ args,
580
+ output: cancellationToolOutput(reason)
581
+ });
582
+ return {
583
+ block: true,
584
+ reason
585
+ };
586
+ }
587
+ if (this.currentMode === "auto-approve") {
588
+ this.appendSystemMessage(sessionId, record, `Auto-approved ${toolName} in auto-approve mode.`);
589
+ return undefined;
590
+ }
591
+ this.patchToolState(sessionId, record, toolCallId, toolName, {
592
+ status: "awaiting_approval",
593
+ args
594
+ });
595
+ this.setRuntimeState(sessionId, record, createRunningRuntimeState(`Awaiting approval for ${toolName}`));
596
+ this.callbacks.openApproval(sessionId, request);
597
+ const decision = await new Promise(resolve => {
598
+ this.pendingApprovals.set(request.id, {
599
+ sessionId,
600
+ resolve
601
+ });
602
+ if (!signal) {
603
+ return;
604
+ }
605
+ const onAbort = () => {
606
+ if (!this.pendingApprovals.has(request.id)) {
607
+ return;
608
+ }
609
+ this.pendingApprovals.delete(request.id);
610
+ this.callbacks.closeApproval(sessionId, request.id);
611
+ resolve("cancel");
612
+ };
613
+ if (signal.aborted) {
614
+ onAbort();
615
+ return;
616
+ }
617
+ signal.addEventListener("abort", onAbort, {
618
+ once: true
619
+ });
620
+ });
621
+ if (decision === "allow") {
622
+ this.patchToolState(sessionId, record, toolCallId, toolName, {
623
+ status: "running",
624
+ args
625
+ });
626
+ this.setRuntimeState(sessionId, record, createRunningRuntimeState());
627
+ return undefined;
628
+ }
629
+ this.patchToolState(sessionId, record, toolCallId, toolName, {
630
+ status: "failed",
631
+ args,
632
+ output: cancellationToolOutput(toolApprovalDecisionReason(decision, request))
633
+ });
634
+ return {
635
+ block: true,
636
+ reason: toolApprovalDecisionReason(decision, request)
637
+ };
638
+ }
639
+ handleSessionEvent(sessionId, record, event) {
640
+ switch (event.type) {
641
+ case "message_start":
642
+ if (event.message.role === "user") {
643
+ const text = extractUserText(event.message);
644
+ if (text) {
645
+ this.callbacks.appendMessage(sessionId, {
646
+ id: createMessageId(record, "user"),
647
+ role: "user",
648
+ content: text,
649
+ time: formatTime(event.message.timestamp)
650
+ });
651
+ }
652
+ return;
653
+ }
654
+ if (event.message.role === "assistant") {
655
+ record.streamingAssistantMessageId = undefined;
656
+ record.streamingReasoningMessageId = undefined;
657
+ }
658
+ return;
659
+ case "message_update":
660
+ if (event.assistantMessageEvent.type === "thinking_delta") {
661
+ if (!record.streamingReasoningMessageId) {
662
+ record.streamingReasoningMessageId = record.streamingAssistantMessageId ? this.insertReasoningMessageBefore(sessionId, record, record.streamingAssistantMessageId, event.assistantMessageEvent.delta) : (() => {
663
+ const id = createMessageId(record, "assistant");
664
+ this.callbacks.appendMessage(sessionId, {
665
+ id,
666
+ role: "assistant",
667
+ content: event.assistantMessageEvent.delta,
668
+ time: formatTime(),
669
+ kind: "reasoning",
670
+ collapsible: true,
671
+ expanded: false
672
+ });
673
+ return id;
674
+ })();
675
+ return;
676
+ }
677
+ this.callbacks.patchMessage(sessionId, record.streamingReasoningMessageId, {
678
+ appendContent: event.assistantMessageEvent.delta
679
+ });
680
+ return;
681
+ }
682
+ if (event.assistantMessageEvent.type === "text_delta" && record.streamingAssistantMessageId) {
683
+ this.callbacks.patchMessage(sessionId, record.streamingAssistantMessageId, {
684
+ appendContent: event.assistantMessageEvent.delta
685
+ });
686
+ return;
687
+ }
688
+ if (event.assistantMessageEvent.type === "text_delta") {
689
+ const messageId = createMessageId(record, "assistant");
690
+ record.streamingAssistantMessageId = messageId;
691
+ this.callbacks.appendMessage(sessionId, {
692
+ id: messageId,
693
+ role: "assistant",
694
+ content: event.assistantMessageEvent.delta,
695
+ time: formatTime()
696
+ });
697
+ }
698
+ return;
699
+ case "message_end":
700
+ if (event.message.role === "assistant") {
701
+ const finalMessage = event.message;
702
+ const content = extractAssistantVisibleText(finalMessage);
703
+ const reasoning = extractAssistantReasoning(finalMessage);
704
+ const messageId = record.streamingAssistantMessageId;
705
+ const reasoningMessageId = record.streamingReasoningMessageId;
706
+ record.streamingAssistantMessageId = undefined;
707
+ record.streamingReasoningMessageId = undefined;
708
+ if (messageId) {
709
+ this.callbacks.patchMessage(sessionId, messageId, {
710
+ content,
711
+ time: formatTime(finalMessage.timestamp)
712
+ });
713
+ } else if (content) {
714
+ this.callbacks.appendMessage(sessionId, {
715
+ id: createMessageId(record, "assistant"),
716
+ role: "assistant",
717
+ content,
718
+ time: formatTime(finalMessage.timestamp)
719
+ });
720
+ }
721
+ if (reasoning) {
722
+ if (reasoningMessageId) {
723
+ this.callbacks.patchMessage(sessionId, reasoningMessageId, {
724
+ content: reasoning,
725
+ time: formatTime(finalMessage.timestamp)
726
+ });
727
+ } else if (messageId) {
728
+ this.insertReasoningMessageBefore(sessionId, record, messageId, reasoning);
729
+ } else {
730
+ this.appendReasoningMessage(sessionId, record, reasoning);
731
+ }
732
+ }
733
+ if (finalMessage.stopReason === "aborted") {
734
+ this.setRuntimeState(sessionId, record, createInterruptedRuntimeState("Interrupted - ready", "The current request was cancelled."));
735
+ } else if (finalMessage.stopReason === "error" && finalMessage.errorMessage) {
736
+ this.applyRecoveryState(sessionId, record, finalMessage.errorMessage, "Request failed - ready");
737
+ }
738
+ }
739
+ return;
740
+ case "tool_execution_start":
741
+ {
742
+ const tool = createToolMessageState(event.toolCallId, event.toolName, event.args);
743
+ const messageId = this.appendToolMessage(sessionId, record, tool);
744
+ record.toolMessageIds.set(event.toolCallId, messageId);
745
+ record.toolStates.set(event.toolCallId, tool);
746
+ return;
747
+ }
748
+ case "tool_execution_update":
749
+ {
750
+ const messageId = record.toolMessageIds.get(event.toolCallId);
751
+ const tool = record.toolStates.get(event.toolCallId);
752
+ if (!messageId || !tool) {
753
+ return;
754
+ }
755
+ const nextTool = updateToolMessageState(tool, {
756
+ args: event.args,
757
+ output: event.partialResult
758
+ });
759
+ record.toolStates.set(event.toolCallId, nextTool);
760
+ this.callbacks.patchMessage(sessionId, messageId, {
761
+ tool: nextTool
762
+ });
763
+ return;
764
+ }
765
+ case "tool_execution_end":
766
+ {
767
+ const messageId = record.toolMessageIds.get(event.toolCallId);
768
+ const tool = record.toolStates.get(event.toolCallId);
769
+ record.toolMessageIds.delete(event.toolCallId);
770
+ record.toolStates.delete(event.toolCallId);
771
+ const nextTool = updateToolMessageState(tool ?? createToolMessageState(event.toolCallId, event.toolName, undefined), {
772
+ status: event.isError ? "failed" : "completed",
773
+ output: event.result
774
+ });
775
+ if (messageId) {
776
+ this.callbacks.patchMessage(sessionId, messageId, {
777
+ tool: nextTool,
778
+ time: formatTime()
779
+ });
780
+ return;
781
+ }
782
+ this.appendToolMessage(sessionId, record, nextTool);
783
+ return;
784
+ }
785
+ case "auto_compaction_start":
786
+ this.setRuntimeState(sessionId, record, createCompactingRuntimeState(event.reason === "overflow" ? "Compacting after overflow" : "Compacting context"));
787
+ this.appendSystemMessage(sessionId, record, "Compacting context...");
788
+ return;
789
+ case "auto_compaction_end":
790
+ if (event.errorMessage) {
791
+ this.applyRecoveryState(sessionId, record, event.errorMessage, "Compaction failed - ready");
792
+ } else if (event.aborted) {
793
+ this.setRuntimeState(sessionId, record, createInterruptedRuntimeState("Compaction cancelled - ready", "You can continue with a new prompt."));
794
+ } else if (event.willRetry) {
795
+ this.setRuntimeState(sessionId, record, createRetryingRuntimeState("Retrying after compaction"));
796
+ } else if (record.activePrompts > 0) {
797
+ this.setRuntimeState(sessionId, record, createRunningRuntimeState());
798
+ }
799
+ this.appendSystemMessage(sessionId, record, event.errorMessage ? `Compaction failed: ${event.errorMessage}` : event.aborted ? "Compaction aborted." : event.result ? "Context compacted." : "Compaction skipped.");
800
+ return;
801
+ case "auto_retry_start":
802
+ this.setRuntimeState(sessionId, record, createRetryingRuntimeState(`Retrying (${event.attempt}/${event.maxAttempts})`, event.errorMessage));
803
+ this.appendSystemMessage(sessionId, record, `Retrying request (${event.attempt}/${event.maxAttempts}) after error: ${event.errorMessage}`);
804
+ return;
805
+ case "auto_retry_end":
806
+ if (!event.success) {
807
+ const finalError = event.finalError ?? "Retry cancelled";
808
+ this.setRuntimeState(sessionId, record, finalError.toLowerCase().includes("cancel") ? createInterruptedRuntimeState("Retry cancelled - ready", finalError) : createErrorRuntimeState("Retry failed - ready", finalError));
809
+ this.appendSystemMessage(sessionId, record, `Retry failed: ${finalError}`);
810
+ if (!finalError.toLowerCase().includes("cancel")) {
811
+ const hint = getRuntimeRecoveryHint(finalError);
812
+ if (hint?.nextStep) {
813
+ this.appendSystemMessage(sessionId, record, hint.nextStep);
814
+ }
815
+ }
816
+ } else if (record.activePrompts > 0) {
817
+ this.setRuntimeState(sessionId, record, createRunningRuntimeState());
818
+ }
819
+ return;
820
+ default:
821
+ return;
822
+ }
823
+ }
824
+ }