@chainingintention/pi-web-cn 1.202606.3

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +364 -0
  3. package/dist/cli.js +960 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/client/apple-touch-icon.png +0 -0
  6. package/dist/client/assets/CodeViewer-B4nxYc0g.js +4 -0
  7. package/dist/client/assets/TerminalPanel-htr2dU1I.js +122 -0
  8. package/dist/client/assets/index-BjUH4a8R.js +1994 -0
  9. package/dist/client/assets/vendor-editor-core-B4Sq6exx.js +12 -0
  10. package/dist/client/assets/vendor-editor-languages-DznYbTkJ.js +26 -0
  11. package/dist/client/assets/vendor-editor-legacy-B4QLsWF8.js +1 -0
  12. package/dist/client/assets/vendor-terminal-DDGTF8rc.css +1 -0
  13. package/dist/client/assets/vendor-terminal-DjQ08hXu.js +16 -0
  14. package/dist/client/favicon.svg +11 -0
  15. package/dist/client/index.html +60 -0
  16. package/dist/client/manifest.webmanifest +24 -0
  17. package/dist/client/pwa-icon-192.png +0 -0
  18. package/dist/client/pwa-icon-512.png +0 -0
  19. package/dist/config.js +154 -0
  20. package/dist/config.js.map +1 -0
  21. package/dist/pi-web-plugins/info/package.json +9 -0
  22. package/dist/pi-web-plugins/info/pi-web-plugin.js +51 -0
  23. package/dist/pi-web-plugins/updates/package.json +9 -0
  24. package/dist/pi-web-plugins/updates/pi-web-plugin.js +181 -0
  25. package/dist/pi-web-plugins/workspace-tasks/config.js +91 -0
  26. package/dist/pi-web-plugins/workspace-tasks/package.json +9 -0
  27. package/dist/pi-web-plugins/workspace-tasks/pi-web-plugin.js +48 -0
  28. package/dist/pi-web-plugins/workspace-tasks/taskRunner.js +12 -0
  29. package/dist/pi-web-plugins/workspace-tasks/tasksPanelElement.js +292 -0
  30. package/dist/pi-web-plugins/workspace-tasks/workspaceTasksClient.js +47 -0
  31. package/dist/piWebVersionReport.js +221 -0
  32. package/dist/piWebVersionReport.js.map +1 -0
  33. package/dist/plugin-api/unstable.d.ts +22 -0
  34. package/dist/plugin-api.d.ts +163 -0
  35. package/dist/server/activity/workspaceActivityRoutes.js +4 -0
  36. package/dist/server/activity/workspaceActivityRoutes.js.map +1 -0
  37. package/dist/server/activity/workspaceActivityService.js +98 -0
  38. package/dist/server/activity/workspaceActivityService.js.map +1 -0
  39. package/dist/server/app.js +115 -0
  40. package/dist/server/app.js.map +1 -0
  41. package/dist/server/configRoutes.js +123 -0
  42. package/dist/server/configRoutes.js.map +1 -0
  43. package/dist/server/diagnostics/nodePtySpawnHelper.js +135 -0
  44. package/dist/server/diagnostics/nodePtySpawnHelper.js.map +1 -0
  45. package/dist/server/git/gitEnv.js +15 -0
  46. package/dist/server/git/gitEnv.js.map +1 -0
  47. package/dist/server/git/gitService.js +119 -0
  48. package/dist/server/git/gitService.js.map +1 -0
  49. package/dist/server/gitRoutes.js +23 -0
  50. package/dist/server/gitRoutes.js.map +1 -0
  51. package/dist/server/index.js +7 -0
  52. package/dist/server/index.js.map +1 -0
  53. package/dist/server/machines/machineClient.js +134 -0
  54. package/dist/server/machines/machineClient.js.map +1 -0
  55. package/dist/server/machines/machineProxyRoutes.js +92 -0
  56. package/dist/server/machines/machineProxyRoutes.js.map +1 -0
  57. package/dist/server/machines/machineRoutes.js +50 -0
  58. package/dist/server/machines/machineRoutes.js.map +1 -0
  59. package/dist/server/machines/machineService.js +168 -0
  60. package/dist/server/machines/machineService.js.map +1 -0
  61. package/dist/server/machines/machineStore.js +128 -0
  62. package/dist/server/machines/machineStore.js.map +1 -0
  63. package/dist/server/piWebPluginService.js +235 -0
  64. package/dist/server/piWebPluginService.js.map +1 -0
  65. package/dist/server/piWebStatus.js +462 -0
  66. package/dist/server/piWebStatus.js.map +1 -0
  67. package/dist/server/projects/directorySuggestions.js +37 -0
  68. package/dist/server/projects/directorySuggestions.js.map +1 -0
  69. package/dist/server/projects/projectService.js +31 -0
  70. package/dist/server/projects/projectService.js.map +1 -0
  71. package/dist/server/realtime/sessionEventHub.js +39 -0
  72. package/dist/server/realtime/sessionEventHub.js.map +1 -0
  73. package/dist/server/sessiond/sessionProxyRoutes.js +60 -0
  74. package/dist/server/sessiond/sessionProxyRoutes.js.map +1 -0
  75. package/dist/server/sessiond.js +61 -0
  76. package/dist/server/sessiond.js.map +1 -0
  77. package/dist/server/sessions/authProviderOptions.js +56 -0
  78. package/dist/server/sessions/authProviderOptions.js.map +1 -0
  79. package/dist/server/sessions/authRoutes.js +59 -0
  80. package/dist/server/sessions/authRoutes.js.map +1 -0
  81. package/dist/server/sessions/authService.js +74 -0
  82. package/dist/server/sessions/authService.js.map +1 -0
  83. package/dist/server/sessions/builtinCommands.js +27 -0
  84. package/dist/server/sessions/builtinCommands.js.map +1 -0
  85. package/dist/server/sessions/editPreview.js +196 -0
  86. package/dist/server/sessions/editPreview.js.map +1 -0
  87. package/dist/server/sessions/messagePaging.js +43 -0
  88. package/dist/server/sessions/messagePaging.js.map +1 -0
  89. package/dist/server/sessions/oauthLoginFlowService.js +219 -0
  90. package/dist/server/sessions/oauthLoginFlowService.js.map +1 -0
  91. package/dist/server/sessions/piSessionService.js +1054 -0
  92. package/dist/server/sessions/piSessionService.js.map +1 -0
  93. package/dist/server/sessions/sessionArchiveStore.js +216 -0
  94. package/dist/server/sessions/sessionArchiveStore.js.map +1 -0
  95. package/dist/server/sessions/sessionArchiveTree.js +35 -0
  96. package/dist/server/sessions/sessionArchiveTree.js.map +1 -0
  97. package/dist/server/sessions/sessionCommandService.js +234 -0
  98. package/dist/server/sessions/sessionCommandService.js.map +1 -0
  99. package/dist/server/sessions/sessionNameGenerator.js +68 -0
  100. package/dist/server/sessions/sessionNameGenerator.js.map +1 -0
  101. package/dist/server/sessions/sessionRoutes.js +184 -0
  102. package/dist/server/sessions/sessionRoutes.js.map +1 -0
  103. package/dist/server/sessions/sessionRuntimeStore.js +2 -0
  104. package/dist/server/sessions/sessionRuntimeStore.js.map +1 -0
  105. package/dist/server/storage/projectStore.js +88 -0
  106. package/dist/server/storage/projectStore.js.map +1 -0
  107. package/dist/server/terminalProxyRoutes.js +130 -0
  108. package/dist/server/terminalProxyRoutes.js.map +1 -0
  109. package/dist/server/terminals/terminalRoutes.js +138 -0
  110. package/dist/server/terminals/terminalRoutes.js.map +1 -0
  111. package/dist/server/terminals/terminalService.js +293 -0
  112. package/dist/server/terminals/terminalService.js.map +1 -0
  113. package/dist/server/terminals/terminalSize.js +17 -0
  114. package/dist/server/terminals/terminalSize.js.map +1 -0
  115. package/dist/server/types.js +2 -0
  116. package/dist/server/types.js.map +1 -0
  117. package/dist/server/webSocketBridge.js +32 -0
  118. package/dist/server/webSocketBridge.js.map +1 -0
  119. package/dist/server/workspaceExplorerRoutes.js +42 -0
  120. package/dist/server/workspaceExplorerRoutes.js.map +1 -0
  121. package/dist/server/workspaces/fileContentService.js +70 -0
  122. package/dist/server/workspaces/fileContentService.js.map +1 -0
  123. package/dist/server/workspaces/fileSuggestions.js +148 -0
  124. package/dist/server/workspaces/fileSuggestions.js.map +1 -0
  125. package/dist/server/workspaces/fileTreeService.js +26 -0
  126. package/dist/server/workspaces/fileTreeService.js.map +1 -0
  127. package/dist/server/workspaces/gitWorktreeDiscovery.js +34 -0
  128. package/dist/server/workspaces/gitWorktreeDiscovery.js.map +1 -0
  129. package/dist/server/workspaces/imagePreviewService.js +40 -0
  130. package/dist/server/workspaces/imagePreviewService.js.map +1 -0
  131. package/dist/server/workspaces/pathSafety.js +45 -0
  132. package/dist/server/workspaces/pathSafety.js.map +1 -0
  133. package/dist/server/workspaces/workspaceContext.js +8 -0
  134. package/dist/server/workspaces/workspaceContext.js.map +1 -0
  135. package/dist/server/workspaces/workspaceService.js +39 -0
  136. package/dist/server/workspaces/workspaceService.js.map +1 -0
  137. package/dist/sessiond/config.js +9 -0
  138. package/dist/sessiond/config.js.map +1 -0
  139. package/dist/sessiond/sessionDaemonClient.js +65 -0
  140. package/dist/sessiond/sessionDaemonClient.js.map +1 -0
  141. package/dist/shared/activity.js +26 -0
  142. package/dist/shared/activity.js.map +1 -0
  143. package/dist/shared/apiTypes.d.ts +464 -0
  144. package/dist/shared/apiTypes.js +2 -0
  145. package/dist/shared/apiTypes.js.map +1 -0
  146. package/dist/shared/federatedRoutes.js +57 -0
  147. package/dist/shared/federatedRoutes.js.map +1 -0
  148. package/dist/shared/piWebStatusParsing.js +62 -0
  149. package/dist/shared/piWebStatusParsing.js.map +1 -0
  150. package/dist/shared/pluginIds.js +5 -0
  151. package/dist/shared/pluginIds.js.map +1 -0
  152. package/dist/shared/workspaceFiles.js +3 -0
  153. package/dist/shared/workspaceFiles.js.map +1 -0
  154. package/docs/assets/favicon.svg +11 -0
  155. package/docs/assets/pi-web-banner.png +0 -0
  156. package/docs/assets/pi-web-demo.gif +0 -0
  157. package/docs/assets/pi-web-demo.webm +0 -0
  158. package/docs/plugins.md +762 -0
  159. package/extensions/pi-web.ts +133 -0
  160. package/install.sh +5 -0
  161. package/package.json +127 -0
  162. package/plugin-api/unstable.d.ts +1 -0
  163. package/plugin-api.d.ts +1 -0
@@ -0,0 +1,1054 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { AuthStorage, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createEditToolDefinition, defineTool, getAgentDir, ModelRegistry, SessionManager, } from "@earendil-works/pi-coding-agent";
3
+ import { pageMessagesAtSafeBoundary } from "./messagePaging.js";
4
+ import { BUILTIN_COMMANDS } from "./builtinCommands.js";
5
+ import { SessionCommandService } from "./sessionCommandService.js";
6
+ import { SessionArchiveStore } from "./sessionArchiveStore.js";
7
+ import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree } from "./sessionArchiveTree.js";
8
+ import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
9
+ import { computeEditPreview } from "./editPreview.js";
10
+ function noop() {
11
+ // Intentionally empty default unsubscribe callback.
12
+ }
13
+ function authLossWarningKey(sessionId, provider, modelId) {
14
+ return `${sessionId}:${provider}/${modelId}`;
15
+ }
16
+ function defaultCreateAgentRuntime(createRuntime, options) {
17
+ if (!(options.sessionManager instanceof SessionManager))
18
+ throw new Error("Default runtime creation requires an SDK SessionManager");
19
+ return createAgentSessionRuntime(createRuntime, { ...options, sessionManager: options.sessionManager });
20
+ }
21
+ function createDefaultRuntimeFactory(authStorage, modelRegistry) {
22
+ return async ({ cwd, agentDir, sessionManager, sessionStartEvent }) => {
23
+ const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry });
24
+ const customTools = [createPiWebEditToolDefinition(cwd)];
25
+ const options = sessionStartEvent === undefined
26
+ ? { services, sessionManager, customTools }
27
+ : { services, sessionManager, sessionStartEvent, customTools };
28
+ const result = await createAgentSessionFromServices(options);
29
+ return { ...result, services, diagnostics: services.diagnostics };
30
+ };
31
+ }
32
+ function createPiWebEditToolDefinition(cwd) {
33
+ const editTool = createEditToolDefinition(cwd);
34
+ return defineTool({
35
+ name: editTool.name,
36
+ label: editTool.label,
37
+ description: editTool.description,
38
+ ...(editTool.promptSnippet === undefined ? {} : { promptSnippet: editTool.promptSnippet }),
39
+ ...(editTool.promptGuidelines === undefined ? {} : { promptGuidelines: editTool.promptGuidelines }),
40
+ parameters: editTool.parameters,
41
+ ...(editTool.renderShell === undefined ? {} : { renderShell: editTool.renderShell }),
42
+ ...(editTool.prepareArguments === undefined ? {} : { prepareArguments: editTool.prepareArguments }),
43
+ ...(editTool.executionMode === undefined ? {} : { executionMode: editTool.executionMode }),
44
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
45
+ const preview = await computeEditPreview(params.path, params.edits, cwd);
46
+ if (signal?.aborted !== true) {
47
+ onUpdate?.({ content: [{ type: "text", text: "Edit preview computed." }], details: { preview } });
48
+ }
49
+ return editTool.execute(toolCallId, params, signal, onUpdate, ctx);
50
+ },
51
+ });
52
+ }
53
+ export class PiSessionService {
54
+ constructor(events, deps = {}) {
55
+ this.events = events;
56
+ this.active = new Map();
57
+ this.activities = new Map();
58
+ this.compactionPromptQueues = new Map();
59
+ this.compactionDrainTimers = new Map();
60
+ this.authLossWarnings = new Set();
61
+ this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
62
+ this.agentDir = deps.agentDir ?? getAgentDir();
63
+ this.sessionManager = deps.sessionManager ?? SessionManager;
64
+ this.modelRegistry = deps.modelRegistry ?? ModelRegistry.create(AuthStorage.create());
65
+ this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(this.modelRegistry.authStorage, this.modelRegistry);
66
+ this.createAgentRuntime = deps.createAgentRuntime ?? defaultCreateAgentRuntime;
67
+ this.workspaceActivity = deps.workspaceActivity;
68
+ this.heartbeat = setInterval(() => { this.publishHeartbeats(); }, deps.heartbeatIntervalMs ?? 2000);
69
+ this.commandService = new SessionCommandService((sessionId) => this.getActive(sessionId), (sessionId, text) => this.prompt(sessionId, text), events, {
70
+ onCompactionStart: (session) => {
71
+ this.publishActivity(session, "compacting", "active");
72
+ this.publishStatus(session);
73
+ },
74
+ onCompactionEnd: (session, result, detail) => {
75
+ this.publishActivity(session, result === "success" ? "compaction complete" : "compaction failed", result === "success" ? "idle" : "error", detail);
76
+ this.publishStatus(session);
77
+ },
78
+ }, { listSessionNames: (cwd) => this.listSessionNames(cwd) });
79
+ }
80
+ activeCount() {
81
+ return this.active.size;
82
+ }
83
+ async dispose() {
84
+ clearInterval(this.heartbeat);
85
+ this.clearCompactionDrainTimers();
86
+ const activeSessions = Array.from(new Set(this.active.values()));
87
+ this.active.clear();
88
+ this.activities.clear();
89
+ this.compactionPromptQueues.clear();
90
+ this.authLossWarnings.clear();
91
+ await Promise.all(activeSessions.map(async (active) => {
92
+ active.unsubscribe();
93
+ this.workspaceActivity?.removeSession(active.runtime.session.sessionId, active.runtime.session.sessionManager.getCwd());
94
+ await active.runtime.session.abort();
95
+ await active.runtime.dispose();
96
+ }));
97
+ }
98
+ async list(cwd) {
99
+ const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
100
+ const sessionsById = new Map(sessions.map((session) => [session.id, session]));
101
+ const archivedForCwd = await Promise.all(archivedRecords
102
+ .filter((record) => record.cwd === cwd)
103
+ .map((record) => this.ensureArchivedSessionMoved(record, sessionsById.get(record.sessionId))));
104
+ const archivedById = new Map(archivedForCwd.map((record) => [record.sessionId, record]));
105
+ const unarchivedSessions = sessions.filter((session) => !archivedById.has(session.id)).map(clientSessionFromListEntry);
106
+ this.workspaceActivity?.reconcileSessionActivity(cwd, this.reconcilableSessionIds(cwd, unarchivedSessions.map((session) => session.id), archivedById));
107
+ const archivedSessions = archivedForCwd
108
+ .sort(compareArchivedRecords)
109
+ .map((record) => clientSessionFromArchivedRecord(record, sessionsById.get(record.sessionId)))
110
+ .filter(isDefined);
111
+ return [...unarchivedSessions, ...archivedSessions];
112
+ }
113
+ async start(cwd) {
114
+ const active = await this.create(this.sessionManager.create(cwd), cwd);
115
+ const { session } = active.runtime;
116
+ return {
117
+ id: session.sessionId,
118
+ path: session.sessionFile ?? "",
119
+ cwd,
120
+ created: new Date().toISOString(),
121
+ modified: new Date().toISOString(),
122
+ messageCount: session.messages.length,
123
+ firstMessage: "",
124
+ };
125
+ }
126
+ async messages(sessionId, page) {
127
+ const session = await this.getOrOpen(sessionId);
128
+ return pageMessagesAtSafeBoundary(historyMessages(session), page);
129
+ }
130
+ async status(sessionId) {
131
+ return this.statusFromSession(await this.getOrOpen(sessionId));
132
+ }
133
+ async availableModels(sessionId) {
134
+ const session = await this.getOrOpen(sessionId);
135
+ session.modelRegistry.refresh();
136
+ const models = session.scopedModels.length > 0
137
+ ? session.scopedModels.map((scoped) => scoped.model)
138
+ : session.modelRegistry.getAvailable();
139
+ return models.map(modelToClientModel);
140
+ }
141
+ async setModel(sessionId, provider, modelId) {
142
+ await this.assertWritable(sessionId);
143
+ const session = await this.getOrOpen(sessionId);
144
+ session.modelRegistry.refresh();
145
+ const candidates = session.scopedModels.length > 0
146
+ ? session.scopedModels.map((scoped) => scoped.model)
147
+ : session.modelRegistry.getAvailable();
148
+ const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
149
+ ?? session.modelRegistry.find(provider, modelId);
150
+ if (model === undefined)
151
+ throw new Error(`Model not found: ${provider}/${modelId}`);
152
+ await session.setModel(model);
153
+ this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
154
+ this.publishStatus(session);
155
+ return this.statusFromSession(session);
156
+ }
157
+ async cycleModel(sessionId, direction) {
158
+ await this.assertWritable(sessionId);
159
+ const session = await this.getOrOpen(sessionId);
160
+ const result = await session.cycleModel(direction);
161
+ if (result === undefined)
162
+ throw new Error(session.scopedModels.length > 0 ? "Only one model in scope" : "Only one model available");
163
+ this.publishActivity(session, `model: ${result.model.id}`, "idle", result.model.provider);
164
+ this.publishStatus(session);
165
+ return this.statusFromSession(session);
166
+ }
167
+ async availableThinkingLevels(sessionId) {
168
+ const session = await this.getOrOpen(sessionId);
169
+ return session.getAvailableThinkingLevels();
170
+ }
171
+ async setThinkingLevel(sessionId, level) {
172
+ await this.assertWritable(sessionId);
173
+ const session = await this.getOrOpen(sessionId);
174
+ session.setThinkingLevel(level);
175
+ this.publishActivity(session, `thinking: ${session.thinkingLevel}`, "idle");
176
+ this.publishStatus(session);
177
+ return this.statusFromSession(session);
178
+ }
179
+ async cycleThinkingLevel(sessionId) {
180
+ await this.assertWritable(sessionId);
181
+ const session = await this.getOrOpen(sessionId);
182
+ const level = session.cycleThinkingLevel();
183
+ if (level === undefined)
184
+ throw new Error("Current model does not support thinking");
185
+ this.publishActivity(session, `thinking: ${level}`, "idle");
186
+ this.publishStatus(session);
187
+ return this.statusFromSession(session);
188
+ }
189
+ async commands(sessionId) {
190
+ const session = await this.getOrOpen(sessionId);
191
+ const commands = [...BUILTIN_COMMANDS];
192
+ for (const command of session.extensionRunner.getRegisteredCommands()) {
193
+ commands.push({ name: command.invocationName, ...(command.description === undefined ? {} : { description: command.description }), source: "extension" });
194
+ }
195
+ for (const template of session.promptTemplates) {
196
+ commands.push({ name: template.name, ...(template.description === undefined ? {} : { description: template.description }), source: "prompt" });
197
+ }
198
+ for (const skill of session.resourceLoader.getSkills().skills) {
199
+ commands.push({ name: `skill:${skill.name}`, ...(skill.description === undefined ? {} : { description: skill.description }), source: "skill" });
200
+ }
201
+ return commands.sort((a, b) => a.name.localeCompare(b.name));
202
+ }
203
+ async prompt(sessionId, text, streamingBehavior) {
204
+ await this.assertWritable(sessionId);
205
+ const session = await this.getOrOpen(sessionId);
206
+ this.maybeGenerateSessionName(session, text);
207
+ const isQueued = session.isStreaming || session.isCompacting;
208
+ const behavior = isQueued ? streamingBehavior ?? "followUp" : undefined;
209
+ if (isQueued && this.hasQueuedMessageText(session, text)) {
210
+ this.publishActivity(session, "duplicate queued message ignored", "active");
211
+ this.publishStatus(session);
212
+ return;
213
+ }
214
+ if (session.isCompacting) {
215
+ this.enqueuePromptDuringCompaction(session, text, behavior ?? "followUp");
216
+ return;
217
+ }
218
+ void this.submitPrompt(session, text, behavior);
219
+ }
220
+ submitPrompt(session, text, behavior) {
221
+ this.publishActivity(session, behavior === "steer" ? "steering queued" : behavior === "followUp" ? "message queued" : "prompt accepted", "active");
222
+ if (behavior === undefined)
223
+ this.events.publish(session.sessionId, { type: "message.append", message: userTextMessage(text) });
224
+ const promptPromise = session.prompt(text, behavior === undefined ? undefined : { streamingBehavior: behavior }).catch((error) => {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ this.publishActivity(session, "error", "error", message);
227
+ this.events.publish(session.sessionId, { type: "session.error", message });
228
+ });
229
+ void promptPromise;
230
+ return promptPromise;
231
+ }
232
+ enqueuePromptDuringCompaction(session, text, kind) {
233
+ const queue = this.compactionPromptQueues.get(session.sessionId) ?? [];
234
+ queue.push({ kind, text });
235
+ this.compactionPromptQueues.set(session.sessionId, queue);
236
+ this.publishActivity(session, "message queued during compaction", "active");
237
+ this.publishStatus(session);
238
+ }
239
+ async shell(sessionId, text) {
240
+ await this.assertWritable(sessionId);
241
+ const active = await this.getActive(sessionId);
242
+ const { session } = active.runtime;
243
+ const isExcluded = text.startsWith("!!");
244
+ const command = (isExcluded ? text.slice(2) : text.slice(1)).trim();
245
+ if (!command)
246
+ throw new Error("Usage: !<shell command>");
247
+ if (session.isBashRunning)
248
+ throw new Error("A bash command is already running");
249
+ this.publishActivity(session, "running bash", "active", command);
250
+ this.events.publish(session.sessionId, { type: "shell.start", command, excludeFromContext: isExcluded });
251
+ void session.executeBash(command, (chunk) => {
252
+ this.events.publish(session.sessionId, { type: "shell.chunk", chunk });
253
+ this.publishActivity(session, "running bash", "active", command);
254
+ this.publishStatus(session);
255
+ }, { excludeFromContext: isExcluded }).then((result) => {
256
+ this.events.publish(session.sessionId, {
257
+ type: "shell.end",
258
+ output: result.output,
259
+ ...(result.exitCode === undefined ? {} : { exitCode: result.exitCode }),
260
+ cancelled: result.cancelled,
261
+ truncated: result.truncated,
262
+ ...(result.fullOutputPath === undefined ? {} : { fullOutputPath: result.fullOutputPath }),
263
+ });
264
+ this.publishActivity(session, "bash complete", result.exitCode === 0 ? "idle" : "error", command);
265
+ this.publishStatus(session);
266
+ }).catch((error) => {
267
+ const message = error instanceof Error ? error.message : String(error);
268
+ this.events.publish(session.sessionId, { type: "shell.end", output: message, isError: true });
269
+ this.events.publish(session.sessionId, { type: "session.error", message });
270
+ this.publishActivity(session, "bash failed", "error", message);
271
+ this.publishStatus(session);
272
+ });
273
+ }
274
+ async runCommand(sessionId, text) {
275
+ await this.assertWritable(sessionId);
276
+ return this.commandService.run(sessionId, text);
277
+ }
278
+ async respondToCommand(sessionId, requestId, value) {
279
+ await this.assertWritable(sessionId);
280
+ return this.commandService.respond(sessionId, requestId, value);
281
+ }
282
+ async archive(sessionId) {
283
+ const session = await this.getOrOpen(sessionId);
284
+ if (this.hasActiveWork(session))
285
+ throw new Error("Stop current session activity before archiving");
286
+ const archiveInput = await this.archiveInputForSession(session);
287
+ await this.closeActive(session.sessionId);
288
+ await this.archiveStore.archive(archiveInput);
289
+ }
290
+ async archiveTree(sessionId) {
291
+ const session = await this.getOrOpen(sessionId);
292
+ const catalog = await this.workspaceArchiveCandidates(session.sessionManager.getCwd());
293
+ const root = findArchiveCandidateByIdOrPrefix(catalog, session.sessionId) ?? archiveCandidateFromActiveSession(session, false);
294
+ const plan = planSessionArchiveTree(root, catalog);
295
+ const busy = plan.targets.map((target) => target.activeSession).find((target) => target !== undefined && this.hasActiveWork(target));
296
+ if (busy !== undefined)
297
+ throw new Error(`Stop current session activity before archiving ${sessionDisplayName(busy)}`);
298
+ const archiveInputs = plan.unarchivedTargets.map((target) => archiveInputFromCandidate(target));
299
+ for (const input of archiveInputs)
300
+ await this.closeActive(input.sessionId);
301
+ for (const input of archiveInputs)
302
+ await this.archiveStore.archive(input);
303
+ return {
304
+ archived: true,
305
+ sessionIds: archiveInputs.map((input) => input.sessionId),
306
+ archivedCount: archiveInputs.length,
307
+ skippedAlreadyArchivedCount: plan.skippedAlreadyArchivedCount,
308
+ };
309
+ }
310
+ async restore(sessionId) {
311
+ await this.closeActive(sessionId);
312
+ await this.archiveStore.restore(sessionId);
313
+ }
314
+ async detachParent(sessionId) {
315
+ const session = await this.getOrOpen(sessionId);
316
+ const sessionFile = session.sessionFile;
317
+ if (sessionFile === undefined || sessionFile === "")
318
+ throw new Error("Session is not persisted");
319
+ await clearParentSession(sessionFile);
320
+ }
321
+ async abort(sessionId) {
322
+ const active = this.active.get(sessionId);
323
+ if (!active)
324
+ return;
325
+ this.clearCompactionPromptQueue(sessionId);
326
+ clearSessionQueue(active.runtime.session);
327
+ await active.runtime.session.abort();
328
+ this.publishActivity(active.runtime.session, "stopped", "idle");
329
+ this.publishStatus(active.runtime.session);
330
+ }
331
+ stop(sessionId) {
332
+ void this.closeActive(sessionId).catch(() => {
333
+ // Best-effort shutdown; callers that need errors await closeActive directly.
334
+ });
335
+ }
336
+ reconcilableSessionIds(cwd, listedSessionIds, archivedById) {
337
+ const sessionIds = new Set(listedSessionIds);
338
+ for (const active of new Set(this.active.values())) {
339
+ const session = active.runtime.session;
340
+ if (session.sessionManager.getCwd() === cwd && !archivedById.has(session.sessionId))
341
+ sessionIds.add(session.sessionId);
342
+ }
343
+ return [...sessionIds];
344
+ }
345
+ async ensureArchivedSessionMoved(record, session) {
346
+ if (session === undefined || this.active.has(record.sessionId))
347
+ return record;
348
+ try {
349
+ return await this.archiveStore.archive(archiveInputFromListEntry(session));
350
+ }
351
+ catch {
352
+ return record;
353
+ }
354
+ }
355
+ async archiveInputForSession(session) {
356
+ const cwd = session.sessionManager.getCwd();
357
+ const sessionFile = session.sessionFile;
358
+ if (sessionFile === undefined || sessionFile === "")
359
+ throw new Error("Session is not persisted");
360
+ const listed = (await this.sessionManager.list(cwd)).find((candidate) => candidate.id === session.sessionId);
361
+ if (listed !== undefined)
362
+ return archiveInputFromListEntry(listed);
363
+ return archiveInputFromActiveSession(session);
364
+ }
365
+ async workspaceArchiveCandidates(cwd) {
366
+ const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
367
+ const candidates = new Map();
368
+ const archivedById = new Map();
369
+ for (const record of archivedRecords) {
370
+ if (record.cwd === cwd)
371
+ archivedById.set(record.sessionId, record);
372
+ }
373
+ for (const session of sessions) {
374
+ const archived = archivedById.get(session.id);
375
+ if (archived === undefined)
376
+ candidates.set(session.id, archiveCandidateFromListEntry(session));
377
+ else {
378
+ const candidate = archiveCandidateFromArchivedRecord(archived, session);
379
+ if (candidate !== undefined)
380
+ candidates.set(candidate.id, candidate);
381
+ }
382
+ }
383
+ for (const record of archivedById.values()) {
384
+ if (candidates.has(record.sessionId))
385
+ continue;
386
+ const candidate = archiveCandidateFromArchivedRecord(record, undefined);
387
+ if (candidate !== undefined)
388
+ candidates.set(candidate.id, candidate);
389
+ }
390
+ for (const active of new Set(this.active.values())) {
391
+ const session = active.runtime.session;
392
+ if (session.sessionManager.getCwd() !== cwd || archivedById.has(session.sessionId))
393
+ continue;
394
+ const existing = candidates.get(session.sessionId);
395
+ candidates.set(session.sessionId, { ...(existing ?? archiveCandidateFromActiveSession(session, false)), activeSession: session });
396
+ }
397
+ return [...candidates.values()];
398
+ }
399
+ async listSessionNames(cwd) {
400
+ const [sessions, archivedRecords] = await Promise.all([this.sessionManager.list(cwd), this.archiveStore.list()]);
401
+ const names = new Set();
402
+ for (const session of sessions)
403
+ addSessionName(names, session.name);
404
+ for (const record of archivedRecords) {
405
+ if (record.cwd === cwd)
406
+ addSessionName(names, record.name);
407
+ }
408
+ for (const active of new Set(this.active.values())) {
409
+ const session = active.runtime.session;
410
+ if (session.sessionManager.getCwd() === cwd)
411
+ addSessionName(names, session.sessionName);
412
+ }
413
+ return [...names];
414
+ }
415
+ async closeActive(sessionId) {
416
+ const active = this.active.get(sessionId);
417
+ if (!active)
418
+ return;
419
+ this.active.delete(sessionId);
420
+ this.activities.delete(sessionId);
421
+ this.workspaceActivity?.removeSession(sessionId, active.runtime.session.sessionManager.getCwd());
422
+ this.clearAuthLossWarningsForSession(sessionId);
423
+ this.clearCompactionPromptQueue(sessionId);
424
+ clearSessionQueue(active.runtime.session);
425
+ active.unsubscribe();
426
+ try {
427
+ await active.runtime.session.abort();
428
+ }
429
+ finally {
430
+ await active.runtime.dispose();
431
+ }
432
+ }
433
+ async assertWritable(sessionId) {
434
+ if (await this.archiveStore.isArchived(sessionId))
435
+ throw new Error("Archived sessions are read-only. Restore the session to continue.");
436
+ }
437
+ async getOrOpen(sessionId) {
438
+ return (await this.getActive(sessionId)).runtime.session;
439
+ }
440
+ async getActive(sessionId) {
441
+ const active = this.active.get(sessionId);
442
+ if (active)
443
+ return active;
444
+ const archived = await this.archiveStore.get(sessionId);
445
+ if (archived?.archivePath !== undefined)
446
+ return this.create(this.sessionManager.open(archived.archivePath), archived.cwd);
447
+ const match = (await this.sessionManager.listAll()).find((s) => s.id === sessionId || s.id.startsWith(sessionId));
448
+ if (!match)
449
+ throw new Error("Session not found");
450
+ return this.create(this.sessionManager.open(match.path), match.cwd);
451
+ }
452
+ async create(sessionManager, cwd) {
453
+ const runtime = await this.createAgentRuntime(this.createRuntime, { cwd, agentDir: this.agentDir, sessionManager });
454
+ const active = { runtime, unsubscribe: noop };
455
+ this.bindRuntime(active);
456
+ runtime.setRebindSession(() => {
457
+ this.bindRuntime(active);
458
+ return Promise.resolve();
459
+ });
460
+ this.active.set(runtime.session.sessionId, active);
461
+ this.publishStatus(runtime.session);
462
+ return active;
463
+ }
464
+ bindRuntime(active) {
465
+ active.unsubscribe();
466
+ const { session } = active.runtime;
467
+ for (const [sessionId, candidate] of this.active.entries()) {
468
+ if (candidate === active) {
469
+ this.active.delete(sessionId);
470
+ if (sessionId !== session.sessionId)
471
+ this.clearCompactionPromptQueue(sessionId);
472
+ }
473
+ }
474
+ active.unsubscribe = session.subscribe((event) => {
475
+ this.events.publish(session.sessionId, toClientEvent(event));
476
+ this.publishActivityForEvent(session, event);
477
+ const eventType = getString(event, "type");
478
+ if (eventType === "compaction_end")
479
+ this.scheduleCompactionQueueDrain(session.sessionId);
480
+ if (eventType === "agent_start" || eventType === "agent_end")
481
+ this.scheduleCompactionQueueDrain(session.sessionId);
482
+ this.publishStatus(session);
483
+ });
484
+ this.active.set(session.sessionId, active);
485
+ }
486
+ scheduleCompactionQueueDrain(sessionId, delayMs = 0) {
487
+ if (!this.compactionPromptQueues.has(sessionId) || this.compactionDrainTimers.has(sessionId))
488
+ return;
489
+ const timer = setTimeout(() => {
490
+ this.compactionDrainTimers.delete(sessionId);
491
+ this.drainCompactionPromptQueue(sessionId);
492
+ }, delayMs);
493
+ this.compactionDrainTimers.set(sessionId, timer);
494
+ }
495
+ drainCompactionPromptQueue(sessionId) {
496
+ const active = this.active.get(sessionId);
497
+ if (active === undefined)
498
+ return;
499
+ const { session } = active.runtime;
500
+ if (session.isCompacting) {
501
+ this.scheduleCompactionQueueDrain(sessionId, 100);
502
+ return;
503
+ }
504
+ if (session.isStreaming) {
505
+ const queued = this.takeCompactionPromptQueue(sessionId);
506
+ if (queued.length === 0)
507
+ return;
508
+ this.publishStatus(session);
509
+ for (const prompt of queued)
510
+ void this.submitPrompt(session, prompt.text, prompt.kind);
511
+ return;
512
+ }
513
+ const prompt = this.shiftCompactionPrompt(sessionId);
514
+ if (prompt === undefined)
515
+ return;
516
+ this.publishStatus(session);
517
+ const submitted = this.submitPrompt(session, prompt.text, undefined);
518
+ void submitted.finally(() => { this.scheduleCompactionQueueDrain(sessionId); });
519
+ }
520
+ takeCompactionPromptQueue(sessionId) {
521
+ const queued = this.compactionPromptQueues.get(sessionId) ?? [];
522
+ this.compactionPromptQueues.delete(sessionId);
523
+ return queued;
524
+ }
525
+ shiftCompactionPrompt(sessionId) {
526
+ const queue = this.compactionPromptQueues.get(sessionId);
527
+ const prompt = queue?.shift();
528
+ if (queue === undefined || queue.length === 0)
529
+ this.compactionPromptQueues.delete(sessionId);
530
+ return prompt;
531
+ }
532
+ clearCompactionPromptQueue(sessionId) {
533
+ this.compactionPromptQueues.delete(sessionId);
534
+ const timer = this.compactionDrainTimers.get(sessionId);
535
+ if (timer !== undefined) {
536
+ clearTimeout(timer);
537
+ this.compactionDrainTimers.delete(sessionId);
538
+ }
539
+ }
540
+ clearCompactionDrainTimers() {
541
+ for (const timer of this.compactionDrainTimers.values())
542
+ clearTimeout(timer);
543
+ this.compactionDrainTimers.clear();
544
+ }
545
+ maybeGenerateSessionName(session, firstMessage) {
546
+ if (session.sessionName !== undefined || session.messages.length !== 0 || session.isStreaming || session.isCompacting)
547
+ return;
548
+ const model = session.model;
549
+ if (model === undefined)
550
+ return;
551
+ void generateShortSessionName(this.modelRegistry, model, firstMessage).then((name) => {
552
+ this.applyGeneratedSessionName(session, name ?? fallbackSessionName(firstMessage));
553
+ }).catch(() => {
554
+ this.applyGeneratedSessionName(session, fallbackSessionName(firstMessage));
555
+ });
556
+ }
557
+ applyGeneratedSessionName(session, name) {
558
+ if (name === undefined || session.sessionName !== undefined)
559
+ return;
560
+ session.setSessionName(name);
561
+ this.publishSessionName(session);
562
+ }
563
+ applyAuthChange(change = {}) {
564
+ this.modelRegistry.refresh();
565
+ for (const active of this.active.values()) {
566
+ const { session } = active.runtime;
567
+ session.modelRegistry.refresh();
568
+ this.syncCurrentModelAuthWarning(session, change.removedProviderId);
569
+ this.publishStatus(session);
570
+ }
571
+ }
572
+ syncCurrentModelAuthWarning(session, removedProviderId) {
573
+ const model = session.model;
574
+ if (model === undefined)
575
+ return;
576
+ if (model.provider === "unknown" && model.id === "unknown")
577
+ return;
578
+ const warningKey = authLossWarningKey(session.sessionId, model.provider, model.id);
579
+ const registered = session.modelRegistry.find(model.provider, model.id);
580
+ if (registered === undefined)
581
+ return;
582
+ if (session.modelRegistry.hasConfiguredAuth(registered)) {
583
+ this.authLossWarnings.delete(warningKey);
584
+ return;
585
+ }
586
+ if (removedProviderId === undefined || model.provider !== removedProviderId || this.authLossWarnings.has(warningKey))
587
+ return;
588
+ this.authLossWarnings.add(warningKey);
589
+ this.events.publish(session.sessionId, {
590
+ type: "command.output",
591
+ level: "error",
592
+ message: `Authentication for ${model.provider}/${model.id} was removed. Use /model to select another model.`,
593
+ });
594
+ }
595
+ clearAuthLossWarningsForSession(sessionId) {
596
+ const prefix = `${sessionId}:`;
597
+ for (const key of this.authLossWarnings) {
598
+ if (key.startsWith(prefix))
599
+ this.authLossWarnings.delete(key);
600
+ }
601
+ }
602
+ publishSessionName(session) {
603
+ const event = session.sessionName === undefined
604
+ ? { type: "session.name", sessionId: session.sessionId }
605
+ : { type: "session.name", sessionId: session.sessionId, name: session.sessionName };
606
+ this.events.publish(session.sessionId, event);
607
+ this.events.publishGlobal(event);
608
+ }
609
+ publishHeartbeats() {
610
+ for (const active of this.active.values()) {
611
+ const { session } = active.runtime;
612
+ const activity = this.activities.get(session.sessionId);
613
+ if (!this.hasActiveWork(session)) {
614
+ if (activity?.phase === "active")
615
+ this.publishStatus(session);
616
+ continue;
617
+ }
618
+ this.publishStatus(session);
619
+ if (activity?.phase === "active")
620
+ this.publishActivity(session, activity.label, "active", activity.detail);
621
+ else
622
+ this.publishActivity(session, this.activityLabelFromStatus(session), "active");
623
+ }
624
+ }
625
+ activityLabelFromStatus(session) {
626
+ if (session.isCompacting)
627
+ return "compacting";
628
+ if (session.isBashRunning)
629
+ return "running bash";
630
+ if (session.isStreaming)
631
+ return "agent running";
632
+ if (this.pendingMessageCount(session) > 0)
633
+ return "queued";
634
+ return "active";
635
+ }
636
+ hasActiveWork(session) {
637
+ return sessionHasActiveWork(session, this.compactionQueuedMessages(session.sessionId).length);
638
+ }
639
+ publishActivityForEvent(session, event) {
640
+ const eventType = getString(event, "type");
641
+ if (eventType === undefined)
642
+ return;
643
+ if (eventType === "agent_start") {
644
+ this.publishActivity(session, "agent running", "active");
645
+ return;
646
+ }
647
+ if (eventType === "agent_end") {
648
+ this.publishActivity(session, "idle", "idle");
649
+ setTimeout(() => {
650
+ this.publishActivity(session, "idle", "idle");
651
+ this.publishStatus(session);
652
+ }, 250);
653
+ return;
654
+ }
655
+ if (eventType === "turn_end") {
656
+ this.publishActivity(session, "turn complete", "idle");
657
+ return;
658
+ }
659
+ if (eventType === "message_start") {
660
+ this.publishActivity(session, "message started", "active");
661
+ return;
662
+ }
663
+ if (eventType === "message_end") {
664
+ this.publishActivity(session, "message complete", "idle");
665
+ return;
666
+ }
667
+ if (eventType === "message_update") {
668
+ this.publishActivity(session, "receiving response", "active");
669
+ return;
670
+ }
671
+ if (eventType === "tool_execution_start") {
672
+ this.publishActivity(session, "running tool", "active", getString(event, "toolName"));
673
+ return;
674
+ }
675
+ if (eventType === "tool_execution_end") {
676
+ const isError = getBoolean(event, "isError") === true;
677
+ this.publishActivity(session, isError ? "tool failed" : "tool complete", isError ? "error" : "idle", getString(event, "toolName"));
678
+ return;
679
+ }
680
+ if (eventType === "bash_execution_start") {
681
+ this.publishActivity(session, "running bash", "active");
682
+ return;
683
+ }
684
+ if (eventType === "bash_execution_end") {
685
+ this.publishActivity(session, "bash complete", "idle");
686
+ return;
687
+ }
688
+ if (this.hasActiveWork(session))
689
+ this.publishActivity(session, eventType.replaceAll("_", " "), "active");
690
+ }
691
+ publishActivity(session, label, phase, detail) {
692
+ const at = new Date().toISOString();
693
+ const stored = detail === undefined ? { phase, label, at } : { phase, label, detail, at };
694
+ this.activities.set(session.sessionId, stored);
695
+ const activity = detail === undefined ? { sessionId: session.sessionId, phase, label, at } : { sessionId: session.sessionId, phase, label, detail, at };
696
+ this.workspaceActivity?.applySessionActivity(session.sessionManager.getCwd(), activity);
697
+ this.events.publish(session.sessionId, { type: "activity.update", activity });
698
+ this.events.publishGlobal({ type: "activity.update", activity });
699
+ }
700
+ publishStatus(session) {
701
+ const status = this.statusFromSession(session);
702
+ this.clearStaleActiveActivity(session);
703
+ this.workspaceActivity?.applySessionStatus(session.sessionManager.getCwd(), status);
704
+ this.events.publish(session.sessionId, { type: "status.update", status });
705
+ this.events.publishGlobal({ type: "status.update", status });
706
+ }
707
+ clearStaleActiveActivity(session) {
708
+ const current = this.activities.get(session.sessionId);
709
+ if (current?.phase !== "active" || this.hasActiveWork(session))
710
+ return;
711
+ const at = new Date().toISOString();
712
+ const stored = { phase: "idle", label: "idle", at };
713
+ this.activities.set(session.sessionId, stored);
714
+ const activity = { sessionId: session.sessionId, ...stored };
715
+ this.events.publish(session.sessionId, { type: "activity.update", activity });
716
+ this.events.publishGlobal({ type: "activity.update", activity });
717
+ }
718
+ statusFromSession(session) {
719
+ const stats = session.getSessionStats();
720
+ const model = session.model === undefined ? undefined : modelToClientModel(session.model);
721
+ const contextUsage = session.getContextUsage();
722
+ return {
723
+ sessionId: session.sessionId,
724
+ ...(model === undefined ? {} : { model }),
725
+ thinkingLevel: session.thinkingLevel,
726
+ isStreaming: session.isStreaming,
727
+ isCompacting: session.isCompacting,
728
+ isBashRunning: session.isBashRunning,
729
+ pendingMessageCount: this.pendingMessageCount(session),
730
+ queuedMessages: queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)),
731
+ messageCount: session.messages.length,
732
+ tokens: stats.tokens,
733
+ cost: stats.cost,
734
+ ...(contextUsage === undefined ? {} : { contextUsage }),
735
+ };
736
+ }
737
+ pendingMessageCount(session) {
738
+ return session.pendingMessageCount + this.compactionQueuedMessages(session.sessionId).length;
739
+ }
740
+ compactionQueuedMessages(sessionId) {
741
+ return this.compactionPromptQueues.get(sessionId) ?? [];
742
+ }
743
+ hasQueuedMessageText(session, text) {
744
+ return queuedMessagesFromSession(session, this.compactionQueuedMessages(session.sessionId)).some((message) => message.text === text);
745
+ }
746
+ }
747
+ function modelToClientModel(model) {
748
+ if (model === undefined)
749
+ return {};
750
+ const name = getString(model, "name");
751
+ const reasoning = getProperty(model, "reasoning");
752
+ return {
753
+ provider: model.provider,
754
+ id: model.id,
755
+ ...(name === undefined ? {} : { name }),
756
+ contextWindow: model.contextWindow,
757
+ ...(reasoning === undefined ? {} : { reasoning }),
758
+ };
759
+ }
760
+ function clientSessionFromListEntry(session) {
761
+ return {
762
+ id: session.id,
763
+ path: session.path,
764
+ cwd: session.cwd,
765
+ ...(session.name === undefined ? {} : { name: session.name }),
766
+ created: session.created.toISOString(),
767
+ modified: session.modified.toISOString(),
768
+ messageCount: session.messageCount,
769
+ firstMessage: session.firstMessage,
770
+ ...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
771
+ };
772
+ }
773
+ function archiveInputFromListEntry(session) {
774
+ return {
775
+ sessionId: session.id,
776
+ cwd: session.cwd,
777
+ path: session.path,
778
+ created: session.created.toISOString(),
779
+ modified: session.modified.toISOString(),
780
+ messageCount: session.messageCount,
781
+ firstMessage: session.firstMessage,
782
+ ...(session.name === undefined ? {} : { name: session.name }),
783
+ ...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
784
+ };
785
+ }
786
+ function archiveInputFromActiveSession(session) {
787
+ const sessionFile = session.sessionFile;
788
+ if (sessionFile === undefined || sessionFile === "")
789
+ throw new Error("Session is not persisted");
790
+ const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
791
+ return {
792
+ sessionId: session.sessionId,
793
+ cwd: session.sessionManager.getCwd(),
794
+ path: sessionFile,
795
+ created: new Date().toISOString(),
796
+ modified: new Date().toISOString(),
797
+ messageCount: session.messages.length,
798
+ firstMessage: "",
799
+ ...(session.sessionName === undefined ? {} : { name: session.sessionName }),
800
+ ...(parentSessionPath === undefined ? {} : { parentSessionPath }),
801
+ };
802
+ }
803
+ function archiveCandidateFromListEntry(session) {
804
+ return {
805
+ id: session.id,
806
+ path: session.path,
807
+ cwd: session.cwd,
808
+ archived: false,
809
+ listEntry: session,
810
+ ...(session.parentSessionPath === undefined ? {} : { parentSessionPath: session.parentSessionPath }),
811
+ };
812
+ }
813
+ function archiveCandidateFromArchivedRecord(record, fallback) {
814
+ const path = record.originalPath ?? fallback?.path;
815
+ if (path === undefined)
816
+ return undefined;
817
+ const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
818
+ return {
819
+ id: record.sessionId,
820
+ path,
821
+ cwd: record.cwd,
822
+ archived: true,
823
+ ...(fallback === undefined ? {} : { listEntry: fallback }),
824
+ ...(parentSessionPath === undefined ? {} : { parentSessionPath }),
825
+ };
826
+ }
827
+ function archiveCandidateFromActiveSession(session, archived) {
828
+ const sessionFile = session.sessionFile;
829
+ if (sessionFile === undefined || sessionFile === "")
830
+ throw new Error("Session is not persisted");
831
+ const parentSessionPath = session.sessionManager.getHeader?.()?.parentSession;
832
+ return {
833
+ id: session.sessionId,
834
+ path: sessionFile,
835
+ cwd: session.sessionManager.getCwd(),
836
+ archived,
837
+ activeSession: session,
838
+ ...(parentSessionPath === undefined ? {} : { parentSessionPath }),
839
+ };
840
+ }
841
+ function archiveInputFromCandidate(candidate) {
842
+ if (candidate.listEntry !== undefined)
843
+ return archiveInputFromListEntry(candidate.listEntry);
844
+ if (candidate.activeSession !== undefined)
845
+ return archiveInputFromActiveSession(candidate.activeSession);
846
+ throw new Error(`Session is not available for archiving: ${candidate.id}`);
847
+ }
848
+ function sessionHasActiveWork(session, extraQueuedMessageCount = 0) {
849
+ return session.isStreaming || session.isCompacting || session.isBashRunning || session.pendingMessageCount + extraQueuedMessageCount > 0;
850
+ }
851
+ function sessionDisplayName(session) {
852
+ return session.sessionName ?? session.sessionId;
853
+ }
854
+ function clientSessionFromArchivedRecord(record, fallback) {
855
+ const path = record.originalPath ?? fallback?.path;
856
+ const created = record.created ?? fallback?.created.toISOString();
857
+ const modified = record.modified ?? fallback?.modified.toISOString();
858
+ const messageCount = record.messageCount ?? fallback?.messageCount;
859
+ const firstMessage = record.firstMessage ?? fallback?.firstMessage;
860
+ if (path === undefined || created === undefined || modified === undefined || messageCount === undefined || firstMessage === undefined)
861
+ return undefined;
862
+ const name = record.name ?? fallback?.name;
863
+ const parentSessionPath = record.parentSessionPath ?? fallback?.parentSessionPath;
864
+ return {
865
+ id: record.sessionId,
866
+ path,
867
+ cwd: record.cwd,
868
+ ...(name === undefined ? {} : { name }),
869
+ created,
870
+ modified,
871
+ messageCount,
872
+ firstMessage,
873
+ ...(parentSessionPath === undefined ? {} : { parentSessionPath }),
874
+ archived: true,
875
+ archivedAt: record.archivedAt,
876
+ };
877
+ }
878
+ function addSessionName(names, name) {
879
+ const trimmed = name?.replace(/\s+/g, " ").trim();
880
+ if (trimmed !== undefined && trimmed !== "")
881
+ names.add(trimmed);
882
+ }
883
+ function compareArchivedRecords(a, b) {
884
+ return archivedTimestamp(b) - archivedTimestamp(a);
885
+ }
886
+ function archivedTimestamp(record) {
887
+ const time = Date.parse(record.archivedAt);
888
+ return Number.isNaN(time) ? 0 : time;
889
+ }
890
+ function isDefined(value) {
891
+ return value !== undefined;
892
+ }
893
+ async function clearParentSession(sessionFile) {
894
+ const content = await readFile(sessionFile, "utf8");
895
+ const newlineIndex = content.indexOf("\n");
896
+ const firstLine = newlineIndex === -1 ? content : content.slice(0, newlineIndex);
897
+ const rest = newlineIndex === -1 ? "" : content.slice(newlineIndex);
898
+ const header = JSON.parse(firstLine);
899
+ if (!isRecord(header) || header["type"] !== "session")
900
+ throw new Error("Invalid session file header");
901
+ if (header["parentSession"] === undefined)
902
+ return;
903
+ delete header["parentSession"];
904
+ await writeFile(sessionFile, `${JSON.stringify(header)}${rest}`, "utf8");
905
+ }
906
+ function clearSessionQueue(session) {
907
+ session.clearQueue();
908
+ }
909
+ function queuedMessagesFromSession(session, extraQueuedMessages = []) {
910
+ return [
911
+ ...session.getSteeringMessages().map((text) => ({ kind: "steer", text })),
912
+ ...session.getFollowUpMessages().map((text) => ({ kind: "followUp", text })),
913
+ ...extraQueuedMessages,
914
+ ];
915
+ }
916
+ function userTextMessage(text) {
917
+ return { role: "user", content: text };
918
+ }
919
+ function stringValue(value) {
920
+ return typeof value === "string" ? value : "";
921
+ }
922
+ function historyMessages(session) {
923
+ const messages = [];
924
+ for (const entry of session.sessionManager.getBranch()) {
925
+ if (!isRecord(entry))
926
+ continue;
927
+ if (entry["type"] === "message")
928
+ messages.push(entry["message"]);
929
+ else if (entry["type"] === "custom_message" && entry["display"] === true)
930
+ messages.push({ role: "custom", content: entry["content"], customType: entry["customType"], details: entry["details"] });
931
+ else if (entry["type"] === "compaction")
932
+ messages.push({ role: "system", source: "compaction", content: `Compacted history:\n\n${stringValue(entry["summary"])}` });
933
+ else if (entry["type"] === "branch_summary")
934
+ messages.push({ role: "system", source: "branch_summary", content: `Branch summary:\n\n${stringValue(entry["summary"])}` });
935
+ }
936
+ return messages;
937
+ }
938
+ function toClientEvent(event) {
939
+ const eventType = getString(event, "type");
940
+ const assistantMessageEvent = getProperty(event, "assistantMessageEvent");
941
+ if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "text_delta") {
942
+ return { type: "assistant.delta", text: getString(assistantMessageEvent, "delta") ?? "" };
943
+ }
944
+ if (eventType === "message_update" && getString(assistantMessageEvent, "type") === "thinking_delta") {
945
+ return { type: "assistant.thinking.delta", text: getString(assistantMessageEvent, "delta") ?? "" };
946
+ }
947
+ if (eventType === "tool_execution_start") {
948
+ const args = getProperty(event, "args");
949
+ return { type: "tool.start", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", summary: summarizeToolArgs(args), args };
950
+ }
951
+ if (eventType === "tool_execution_update") {
952
+ const partialResult = getProperty(event, "partialResult");
953
+ return { type: "tool.update", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", text: stringifyToolResult(partialResult), content: toolResultContent(partialResult), details: toolResultDetails(partialResult) };
954
+ }
955
+ if (eventType === "tool_execution_end") {
956
+ const result = getProperty(event, "result");
957
+ return { type: "tool.end", toolName: getString(event, "toolName") ?? "", toolCallId: getString(event, "toolCallId") ?? "", text: stringifyToolResult(result), content: toolResultContent(result), details: toolResultDetails(result), isError: getBoolean(event, "isError") === true };
958
+ }
959
+ if (eventType === "agent_start")
960
+ return { type: "agent.start" };
961
+ if (eventType === "agent_end")
962
+ return { type: "agent.end" };
963
+ if (eventType === "message_end") {
964
+ const message = getProperty(event, "message");
965
+ return message === undefined ? { type: "message.end" } : { type: "message.end", message };
966
+ }
967
+ return { type: "pi.event", eventType: eventType ?? "unknown" };
968
+ }
969
+ function summarizeToolArgs(args) {
970
+ if (!isRecord(args))
971
+ return stringifyPrimitive(args);
972
+ const command = getString(args, "command");
973
+ if (command !== undefined)
974
+ return command;
975
+ const path = getString(args, "path");
976
+ if (path !== undefined)
977
+ return path;
978
+ if (typeof args["oldText"] === "string" && typeof args["newText"] === "string")
979
+ return "edit text replacement";
980
+ const edits = args["edits"];
981
+ if (Array.isArray(edits))
982
+ return `${String(edits.length)} edit${edits.length === 1 ? "" : "s"}`;
983
+ const entries = Object.entries(args).filter(([, value]) => value != null).slice(0, 3);
984
+ return entries.map(([key, value]) => `${key}: ${shortToolValue(value)}`).join(" · ");
985
+ }
986
+ function shortToolValue(value) {
987
+ if (typeof value === "string")
988
+ return value.length > 80 ? `${value.slice(0, 77)}…` : value;
989
+ if (typeof value === "number" || typeof value === "boolean")
990
+ return String(value);
991
+ if (Array.isArray(value))
992
+ return `${String(value.length)} item${value.length === 1 ? "" : "s"}`;
993
+ if (typeof value === "object" && value !== null)
994
+ return "object";
995
+ return "";
996
+ }
997
+ function toolResultContent(result) {
998
+ if (isRecord(result)) {
999
+ const content = getProperty(result, "content");
1000
+ if (content !== undefined)
1001
+ return content;
1002
+ const text = getString(result, "text") ?? getString(result, "output");
1003
+ if (text !== undefined)
1004
+ return [{ type: "text", text }];
1005
+ }
1006
+ if (typeof result === "string")
1007
+ return [{ type: "text", text: result }];
1008
+ return result;
1009
+ }
1010
+ function toolResultDetails(result) {
1011
+ return isRecord(result) ? getProperty(result, "details") : undefined;
1012
+ }
1013
+ function stringifyToolResult(result) {
1014
+ if (typeof result === "string")
1015
+ return result;
1016
+ if (Array.isArray(result))
1017
+ return result.map(stringifyToolResult).filter((text) => text !== "").join("\n");
1018
+ if (isRecord(result)) {
1019
+ if (getString(result, "type") === "image")
1020
+ return "[image]";
1021
+ const text = getString(result, "text") ?? getString(result, "content") ?? getString(result, "output");
1022
+ if (text !== undefined)
1023
+ return text;
1024
+ const content = getProperty(result, "content");
1025
+ if (Array.isArray(content))
1026
+ return stringifyToolResult(content);
1027
+ return JSON.stringify(result, null, 2);
1028
+ }
1029
+ return stringifyPrimitive(result);
1030
+ }
1031
+ function isRecord(value) {
1032
+ return typeof value === "object" && value !== null;
1033
+ }
1034
+ function getProperty(value, key) {
1035
+ return isRecord(value) ? value[key] : undefined;
1036
+ }
1037
+ function getString(value, key) {
1038
+ const property = getProperty(value, key);
1039
+ return typeof property === "string" ? property : undefined;
1040
+ }
1041
+ function getBoolean(value, key) {
1042
+ const property = getProperty(value, key);
1043
+ return typeof property === "boolean" ? property : undefined;
1044
+ }
1045
+ function stringifyPrimitive(value) {
1046
+ if (value == null)
1047
+ return "";
1048
+ if (typeof value === "string")
1049
+ return value;
1050
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint")
1051
+ return String(value);
1052
+ return "";
1053
+ }
1054
+ //# sourceMappingURL=piSessionService.js.map