@dreb/coding-agent 2.34.3 → 2.35.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 (80) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/cli/args.d.ts +3 -0
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +3 -0
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/core/agent-session.d.ts +30 -2
  8. package/dist/core/agent-session.d.ts.map +1 -1
  9. package/dist/core/agent-session.js +172 -130
  10. package/dist/core/agent-session.js.map +1 -1
  11. package/dist/core/export-html/template.js +1 -1
  12. package/dist/core/footer-data-provider.d.ts.map +1 -1
  13. package/dist/core/footer-data-provider.js +5 -102
  14. package/dist/core/footer-data-provider.js.map +1 -1
  15. package/dist/core/git-branch.d.ts +19 -0
  16. package/dist/core/git-branch.d.ts.map +1 -0
  17. package/dist/core/git-branch.js +109 -0
  18. package/dist/core/git-branch.js.map +1 -0
  19. package/dist/core/sdk.d.ts.map +1 -1
  20. package/dist/core/sdk.js +1 -1
  21. package/dist/core/sdk.js.map +1 -1
  22. package/dist/core/session-manager.d.ts +21 -0
  23. package/dist/core/session-manager.d.ts.map +1 -1
  24. package/dist/core/session-manager.js +117 -42
  25. package/dist/core/session-manager.js.map +1 -1
  26. package/dist/core/settings-manager.d.ts +6 -0
  27. package/dist/core/settings-manager.d.ts.map +1 -1
  28. package/dist/core/settings-manager.js +8 -0
  29. package/dist/core/settings-manager.js.map +1 -1
  30. package/dist/{modes/interactive → core}/tab-title.d.ts +12 -3
  31. package/dist/core/tab-title.d.ts.map +1 -0
  32. package/dist/{modes/interactive → core}/tab-title.js +58 -16
  33. package/dist/core/tab-title.js.map +1 -0
  34. package/dist/core/tools/search.d.ts.map +1 -1
  35. package/dist/core/tools/search.js +4 -1
  36. package/dist/core/tools/search.js.map +1 -1
  37. package/dist/core/tools/subagent.d.ts +63 -2
  38. package/dist/core/tools/subagent.d.ts.map +1 -1
  39. package/dist/core/tools/subagent.js +459 -73
  40. package/dist/core/tools/subagent.js.map +1 -1
  41. package/dist/core/tools/web.d.ts +1 -1
  42. package/dist/core/tools/web.d.ts.map +1 -1
  43. package/dist/core/tools/web.js +60 -16
  44. package/dist/core/tools/web.js.map +1 -1
  45. package/dist/main.d.ts +10 -0
  46. package/dist/main.d.ts.map +1 -1
  47. package/dist/main.js +54 -4
  48. package/dist/main.js.map +1 -1
  49. package/dist/modes/index.d.ts +2 -2
  50. package/dist/modes/index.d.ts.map +1 -1
  51. package/dist/modes/index.js +1 -1
  52. package/dist/modes/index.js.map +1 -1
  53. package/dist/modes/interactive/components/session-selector.d.ts +1 -1
  54. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  55. package/dist/modes/interactive/components/session-selector.js +4 -40
  56. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  57. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/dist/modes/interactive/interactive-mode.js +2 -2
  59. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/dist/modes/rpc/index.d.ts +2 -2
  61. package/dist/modes/rpc/index.d.ts.map +1 -1
  62. package/dist/modes/rpc/index.js.map +1 -1
  63. package/dist/modes/rpc/rpc-client.d.ts +114 -6
  64. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  65. package/dist/modes/rpc/rpc-client.js +140 -2
  66. package/dist/modes/rpc/rpc-client.js.map +1 -1
  67. package/dist/modes/rpc/rpc-mode.d.ts +97 -0
  68. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  69. package/dist/modes/rpc/rpc-mode.js +656 -27
  70. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  71. package/dist/modes/rpc/rpc-types.d.ts +312 -1
  72. package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  73. package/dist/modes/rpc/rpc-types.js.map +1 -1
  74. package/docs/dashboard.md +152 -0
  75. package/docs/development.md +8 -5
  76. package/docs/rpc.md +652 -5
  77. package/examples/sdk/12-full-control.ts +1 -1
  78. package/package.json +1 -1
  79. package/dist/modes/interactive/tab-title.d.ts.map +0 -1
  80. package/dist/modes/interactive/tab-title.js.map +0 -1
@@ -11,16 +11,544 @@
11
11
  * - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
12
12
  */
13
13
  import * as crypto from "node:crypto";
14
+ import { basename } from "node:path";
15
+ import { isValidThinkingLevel, VALID_THINKING_LEVELS } from "../../cli/args.js";
14
16
  import { VERSION } from "../../config.js";
17
+ import { DailyCostTracker } from "../../core/daily-cost-tracker.js";
18
+ import { getGitBranch } from "../../core/git-branch.js";
15
19
  import { parseModelPattern } from "../../core/model-resolver.js";
16
20
  import { takeOverStdout, writeRawStdout } from "../../core/output-guard.js";
17
21
  import { SessionManager } from "../../core/session-manager.js";
22
+ import { TabTitleGenerator } from "../../core/tab-title.js";
23
+ import { discoverAgentTypes, getBackgroundAgents, rehydrateBackgroundAgentsFromDisk, } from "../../core/tools/subagent.js";
18
24
  import { theme } from "../interactive/theme/theme.js";
19
25
  import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
26
+ /**
27
+ * Map a core {@link SessionInfo} to the RPC DTO, converting Date fields to ISO strings.
28
+ * Shared by the `list_sessions` and `list_all_sessions` handlers so their shapes cannot drift.
29
+ */
30
+ export function toRpcSessionInfo(s) {
31
+ return {
32
+ path: s.path,
33
+ id: s.id,
34
+ cwd: s.cwd,
35
+ name: s.name,
36
+ created: s.created.toISOString(),
37
+ modified: s.modified.toISOString(),
38
+ messageCount: s.messageCount,
39
+ firstMessage: s.firstMessage,
40
+ };
41
+ }
20
42
  export function getPerformanceStatsData(session) {
21
43
  const tracker = session.getPerformanceTracker();
22
44
  return { models: tracker.getAllRollingAverages() };
23
45
  }
46
+ export function getScopedModelsForRpc(session) {
47
+ return session.scopedModels.map(({ model, thinkingLevel }) => ({
48
+ provider: model.provider,
49
+ id: model.id,
50
+ name: model.name,
51
+ reasoning: model.reasoning,
52
+ thinkingLevel,
53
+ }));
54
+ }
55
+ export function getResourcesForRpc(session) {
56
+ const extensionsResult = session.resourceLoader.getExtensions();
57
+ return {
58
+ contextFiles: session.resourceLoader.getAgentsFiles().agentsFiles.map((file) => ({ path: file.path })),
59
+ skills: session.getFilteredSkills().map((skill) => ({ name: skill.name, description: skill.description })),
60
+ extensions: extensionsResult.extensions.map((extension) => ({
61
+ path: extension.path,
62
+ name: extension.sourceInfo.source || basename(extension.path),
63
+ })),
64
+ promptTemplates: session.promptTemplates.map((template) => ({
65
+ name: template.name,
66
+ description: template.description,
67
+ })),
68
+ systemPromptPresent: session.resourceLoader.getSystemPrompt() !== undefined ||
69
+ session.resourceLoader.getAppendSystemPrompt().length > 0,
70
+ };
71
+ }
72
+ export function getPendingMessagesForRpc(session) {
73
+ return {
74
+ steering: [...session.getSteeringMessages()],
75
+ followUp: [...session.getFollowUpMessages()],
76
+ steeringMessages: session.getSteeringMessagePayloads().map((message) => ({
77
+ text: message.text,
78
+ images: message.images ? [...message.images] : undefined,
79
+ })),
80
+ followUpMessages: session.getFollowUpMessagePayloads().map((message) => ({
81
+ text: message.text,
82
+ images: message.images ? [...message.images] : undefined,
83
+ })),
84
+ };
85
+ }
86
+ export function getStateForRpc(session, modelFallbackMessage) {
87
+ return {
88
+ model: session.model,
89
+ scopedModels: getScopedModelsForRpc(session),
90
+ usingSubscription: session.model ? session.modelRegistry.isUsingOAuth(session.model) : false,
91
+ thinkingLevel: session.thinkingLevel,
92
+ isStreaming: session.isStreaming,
93
+ isCompacting: session.isCompacting,
94
+ steeringMode: session.steeringMode,
95
+ followUpMode: session.followUpMode,
96
+ sessionFile: session.sessionFile,
97
+ sessionId: session.sessionId,
98
+ sessionName: session.sessionName,
99
+ autoCompactionEnabled: session.autoCompactionEnabled,
100
+ messageCount: session.messages.length,
101
+ pendingMessageCount: session.pendingMessageCount,
102
+ contextUsage: session.getContextUsage(),
103
+ modelFallbackMessage,
104
+ };
105
+ }
106
+ /**
107
+ * Handle the `delete_session` RPC command: wires the active session into the core
108
+ * {@link SessionManager.deleteSession} guard and maps the result to a discriminated
109
+ * union the handler serializes. Extracted (like {@link getPerformanceStatsData}) so the guard
110
+ * wiring is unit-testable without a live RPC session. Note: the authoritative active-session
111
+ * guard lives in core — this passes the active path through; it does not re-implement it.
112
+ * Uses the same unrestricted path-based addressing as `switch_session` (no containment guard —
113
+ * see PR #315 discussion).
114
+ */
115
+ export async function deleteSessionForRpc(sessionManager, sessionPath) {
116
+ const activePath = sessionManager.getSessionFile();
117
+ const result = await SessionManager.deleteSession(sessionPath, {
118
+ activeSessionPath: activePath,
119
+ });
120
+ if (!result.ok) {
121
+ return { ok: false, error: result.error ?? "Unknown deletion error" };
122
+ }
123
+ return { ok: true, method: result.method };
124
+ }
125
+ /** Handle the `list_all_sessions` RPC command: list every project's sessions as RPC DTOs. */
126
+ export async function listAllSessionsForRpc() {
127
+ const sessions = await SessionManager.listAll();
128
+ return sessions.map(toRpcSessionInfo);
129
+ }
130
+ /**
131
+ * Map a {@link BackgroundAgentInfo} registry entry to the RPC DTO, converting the
132
+ * epoch-ms timestamp to an ISO string. Shared shape guard for `list_background_agents`.
133
+ */
134
+ export function toRpcBackgroundAgentInfo(a) {
135
+ return {
136
+ agentId: a.agentId,
137
+ agentType: a.agentType,
138
+ taskSummary: a.taskSummary,
139
+ startedAt: new Date(a.startedAt).toISOString(),
140
+ status: a.status,
141
+ sessionDir: a.sessionDir,
142
+ sessionFile: a.sessionFile,
143
+ cwd: a.cwd,
144
+ };
145
+ }
146
+ /** Discover available subagent types for RPC clients, sorted by display name. */
147
+ export function listAgentTypesForRpc(cwd) {
148
+ return [...discoverAgentTypes(cwd).values()]
149
+ .map(({ name, description }) => ({ name, description }))
150
+ .sort((a, b) => a.name.localeCompare(b.name));
151
+ }
152
+ /**
153
+ * Handle the `get_settings` RPC command: snapshot the persistent default settings.
154
+ *
155
+ * Reads the SettingsManager's merged (global + project) view — these are the values that
156
+ * seed fresh runtimes, NOT the live session state (`get_state` reports that). Extracted
157
+ * (like {@link deleteSessionForRpc}) so it is unit-testable without a live RPC session.
158
+ */
159
+ export function getSettingsForRpc(settingsManager) {
160
+ return {
161
+ defaultProvider: settingsManager.getDefaultProvider(),
162
+ defaultModel: settingsManager.getDefaultModel(),
163
+ defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),
164
+ steeringMode: settingsManager.getSteeringMode(),
165
+ followUpMode: settingsManager.getFollowUpMode(),
166
+ compactionEnabled: settingsManager.getCompactionEnabled(),
167
+ retryEnabled: settingsManager.getRetryEnabled(),
168
+ imageAutoResize: settingsManager.getImageAutoResize(),
169
+ blockImages: settingsManager.getBlockImages(),
170
+ enableSkillCommands: settingsManager.getEnableSkillCommands(),
171
+ autoLoadNestedContext: settingsManager.getAutoLoadNestedContext(),
172
+ transport: settingsManager.getTransport(),
173
+ hideThinkingBlock: settingsManager.getHideThinkingBlock(),
174
+ agentModels: settingsManager.getAgentModels(),
175
+ };
176
+ }
177
+ const SETTINGS_UPDATE_KEYS = [
178
+ "defaultProvider",
179
+ "defaultModel",
180
+ "defaultThinkingLevel",
181
+ "steeringMode",
182
+ "followUpMode",
183
+ "compactionEnabled",
184
+ "retryEnabled",
185
+ "imageAutoResize",
186
+ "blockImages",
187
+ "enableSkillCommands",
188
+ "autoLoadNestedContext",
189
+ "transport",
190
+ "hideThinkingBlock",
191
+ "agentModels",
192
+ ];
193
+ const QUEUE_MODES = ["all", "one-at-a-time"];
194
+ const TRANSPORT_SETTINGS = ["sse", "websocket", "auto"];
195
+ function isPlainObject(value) {
196
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
197
+ return false;
198
+ }
199
+ const proto = Object.getPrototypeOf(value);
200
+ return proto === Object.prototype || proto === null;
201
+ }
202
+ function validateAgentModels(value) {
203
+ if (!isPlainObject(value)) {
204
+ return {
205
+ ok: false,
206
+ error: "Invalid agentModels: must be a plain object mapping agent names to model fallback arrays",
207
+ };
208
+ }
209
+ for (const [agentName, models] of Object.entries(value)) {
210
+ if (!Array.isArray(models)) {
211
+ return {
212
+ ok: false,
213
+ error: `Invalid agentModels[${JSON.stringify(agentName)}]: expected an array of non-empty strings`,
214
+ };
215
+ }
216
+ const invalidModel = models.find((model) => typeof model !== "string" || model.trim().length === 0);
217
+ if (invalidModel !== undefined) {
218
+ return {
219
+ ok: false,
220
+ error: `Invalid agentModels[${JSON.stringify(agentName)}]: expected an array of non-empty strings`,
221
+ };
222
+ }
223
+ }
224
+ return { ok: true };
225
+ }
226
+ /**
227
+ * Simple promise-based mutex that serializes settings writes. RPC commands are dispatched
228
+ * concurrently (`void handleInputLine(line)`) so two `set_settings` commands can overlap.
229
+ * Without serialization, concurrent commands race on SettingsManager's shared error bucket
230
+ * (`drainErrors()` clears the array for everyone): the first to drain takes all errors
231
+ * (including the second's), and the second reports false success despite its write failing.
232
+ *
233
+ * This lock ensures only one apply+flush+drain block runs at a time — each `set_settings`
234
+ * gets an error window isolated from other `set_settings` commands and from stale errors.
235
+ *
236
+ * Known limitation: the lock does NOT cover the per-runtime commands (`set_model`,
237
+ * `set_steering_mode`, etc.), which persist through the same SettingsManager write queue
238
+ * and record failures into the same shared error bucket. Because `flush()` awaits the
239
+ * entire shared queue, a concurrent runtime setter's write failure can land in this
240
+ * operation's post-flush drain window and be reported as a `set_settings` failure (loud
241
+ * but mis-attributed — never a silent success). Per-operation error isolation in
242
+ * SettingsManager is the proper fix and is tracked in
243
+ * https://github.com/aebrer/dreb/issues/319.
244
+ */
245
+ let settingsWriteQueue = Promise.resolve();
246
+ function settingsWriteLock(fn) {
247
+ const prev = settingsWriteQueue;
248
+ const next = prev.then(fn, fn);
249
+ settingsWriteQueue = next.catch(() => { });
250
+ return next;
251
+ }
252
+ /**
253
+ * Handle the `set_settings` RPC command: validate and persist default settings.
254
+ *
255
+ * Writes persistent defaults via SettingsManager only — never touches live session state
256
+ * (the existing per-runtime commands do that). The whole payload is validated before
257
+ * anything is applied: on any invalid field, nothing changes.
258
+ *
259
+ * Persistence is verified loudly: if the settings file failed to load at startup,
260
+ * SettingsManager.save() silently no-ops — this handler reports that as an error instead
261
+ * of returning success while nothing was written. Write failures surface the same way.
262
+ *
263
+ * The apply+flush+drain block is serialized via {@link settingsWriteLock} so that
264
+ * concurrent `set_settings` commands (dispatched concurrently by the RPC input loop)
265
+ * cannot race on the shared error bucket. Pre-existing stale errors (from other commands
266
+ * like `set_model` that record write failures but never drain) are discarded before
267
+ * applying. See the {@link settingsWriteLock} doc for the remaining attribution caveat
268
+ * with runtime setters that write concurrently during the flush window.
269
+ */
270
+ export async function setSettingsForRpc(settingsManager, modelRegistry, update) {
271
+ // --- Validate everything first; apply nothing on any failure ---
272
+ if (update === undefined || update === null || typeof update !== "object" || Array.isArray(update)) {
273
+ return { ok: false, error: "set_settings requires a settings object" };
274
+ }
275
+ const unknownKeys = Object.keys(update).filter((key) => !SETTINGS_UPDATE_KEYS.includes(key));
276
+ if (unknownKeys.length > 0) {
277
+ return {
278
+ ok: false,
279
+ error: `Unknown settings key(s): ${unknownKeys.join(", ")}. Valid keys: ${SETTINGS_UPDATE_KEYS.join(", ")}`,
280
+ };
281
+ }
282
+ const hasAnySetting = SETTINGS_UPDATE_KEYS.some((key) => update[key] !== undefined);
283
+ if (!hasAnySetting) {
284
+ return { ok: false, error: "set_settings requires at least one setting to change" };
285
+ }
286
+ if (update.defaultThinkingLevel !== undefined) {
287
+ if (typeof update.defaultThinkingLevel !== "string" || !isValidThinkingLevel(update.defaultThinkingLevel)) {
288
+ return {
289
+ ok: false,
290
+ error: `Invalid defaultThinkingLevel: ${JSON.stringify(update.defaultThinkingLevel)}. Valid values: ${VALID_THINKING_LEVELS.join(", ")}`,
291
+ };
292
+ }
293
+ }
294
+ for (const key of ["steeringMode", "followUpMode"]) {
295
+ const value = update[key];
296
+ if (value !== undefined && !QUEUE_MODES.includes(value)) {
297
+ return {
298
+ ok: false,
299
+ error: `Invalid ${key}: ${JSON.stringify(value)}. Valid values: ${QUEUE_MODES.join(", ")}`,
300
+ };
301
+ }
302
+ }
303
+ for (const key of [
304
+ "compactionEnabled",
305
+ "retryEnabled",
306
+ "imageAutoResize",
307
+ "blockImages",
308
+ "enableSkillCommands",
309
+ "autoLoadNestedContext",
310
+ "hideThinkingBlock",
311
+ ]) {
312
+ const value = update[key];
313
+ if (value !== undefined && typeof value !== "boolean") {
314
+ return { ok: false, error: `Invalid ${key}: ${JSON.stringify(value)}. Must be a boolean` };
315
+ }
316
+ }
317
+ if (update.transport !== undefined) {
318
+ if (typeof update.transport !== "string" ||
319
+ !TRANSPORT_SETTINGS.includes(update.transport)) {
320
+ return {
321
+ ok: false,
322
+ error: `Invalid transport: ${JSON.stringify(update.transport)}. Valid values: ${TRANSPORT_SETTINGS.join(", ")}`,
323
+ };
324
+ }
325
+ }
326
+ if (update.agentModels !== undefined) {
327
+ const validation = validateAgentModels(update.agentModels);
328
+ if (!validation.ok) {
329
+ return validation;
330
+ }
331
+ }
332
+ const hasProvider = update.defaultProvider !== undefined;
333
+ const hasModel = update.defaultModel !== undefined;
334
+ if (hasProvider !== hasModel) {
335
+ return {
336
+ ok: false,
337
+ error: "defaultProvider and defaultModel must be set together",
338
+ };
339
+ }
340
+ if (hasProvider && hasModel) {
341
+ if (typeof update.defaultProvider !== "string" || typeof update.defaultModel !== "string") {
342
+ return { ok: false, error: "defaultProvider and defaultModel must be strings" };
343
+ }
344
+ const models = await modelRegistry.getAvailable();
345
+ const match = models.find((m) => m.provider === update.defaultProvider && m.id === update.defaultModel);
346
+ if (!match) {
347
+ return {
348
+ ok: false,
349
+ error: `Model not found: ${update.defaultProvider}/${update.defaultModel}`,
350
+ };
351
+ }
352
+ }
353
+ // Serialize the apply+flush+drain block so concurrent set_settings commands
354
+ // cannot race on the shared error bucket. See settingsWriteLock doc.
355
+ return settingsWriteLock(async () => {
356
+ // Discard stale errors left by other operations (set_model, set_steering_mode, etc.)
357
+ // that record write failures into SettingsManager's shared error bucket but never
358
+ // drain it. Without this, we'd mis-attribute their failures to this operation.
359
+ settingsManager.drainErrors();
360
+ // Persisting would silently no-op if the settings file failed to load — fail loudly
361
+ // instead. Checked INSIDE the lock (after the stale-error discard) so a concurrent
362
+ // reload() that flips the load-error state cannot slip between check and apply and
363
+ // turn this write into a silent no-op reported as success.
364
+ if (settingsManager.hasGlobalSettingsLoadError()) {
365
+ return {
366
+ ok: false,
367
+ error: "Cannot write settings: the global settings file failed to load (fix or remove the corrupt settings.json first)",
368
+ };
369
+ }
370
+ // --- Apply (validated) ---
371
+ if (update.defaultProvider !== undefined && update.defaultModel !== undefined) {
372
+ settingsManager.setDefaultModelAndProvider(update.defaultProvider, update.defaultModel);
373
+ }
374
+ if (update.defaultThinkingLevel !== undefined) {
375
+ settingsManager.setDefaultThinkingLevel(update.defaultThinkingLevel);
376
+ }
377
+ if (update.steeringMode !== undefined) {
378
+ settingsManager.setSteeringMode(update.steeringMode);
379
+ }
380
+ if (update.followUpMode !== undefined) {
381
+ settingsManager.setFollowUpMode(update.followUpMode);
382
+ }
383
+ if (update.compactionEnabled !== undefined) {
384
+ settingsManager.setCompactionEnabled(update.compactionEnabled);
385
+ }
386
+ if (update.retryEnabled !== undefined) {
387
+ settingsManager.setRetryEnabled(update.retryEnabled);
388
+ }
389
+ if (update.imageAutoResize !== undefined) {
390
+ settingsManager.setImageAutoResize(update.imageAutoResize);
391
+ }
392
+ if (update.blockImages !== undefined) {
393
+ settingsManager.setBlockImages(update.blockImages);
394
+ }
395
+ if (update.enableSkillCommands !== undefined) {
396
+ settingsManager.setEnableSkillCommands(update.enableSkillCommands);
397
+ }
398
+ if (update.autoLoadNestedContext !== undefined) {
399
+ settingsManager.setAutoLoadNestedContext(update.autoLoadNestedContext);
400
+ }
401
+ if (update.transport !== undefined) {
402
+ settingsManager.setTransport(update.transport);
403
+ }
404
+ if (update.hideThinkingBlock !== undefined) {
405
+ settingsManager.setHideThinkingBlock(update.hideThinkingBlock);
406
+ }
407
+ const warnings = [];
408
+ if (update.agentModels !== undefined) {
409
+ for (const [agentName, models] of Object.entries(update.agentModels)) {
410
+ if (settingsManager.hasProjectAgentModelOverride(agentName)) {
411
+ warnings.push(`A project-level agentModels override for ${JSON.stringify(agentName)} (.dreb/settings.json) ` +
412
+ "takes precedence — this change to global settings will have no effect. " +
413
+ "Edit the project settings file to change it.");
414
+ }
415
+ if (models.length > 0) {
416
+ settingsManager.setAgentModelsForAgent(agentName, models);
417
+ }
418
+ else {
419
+ settingsManager.removeAgentModelsForAgent(agentName);
420
+ }
421
+ }
422
+ }
423
+ // Ensure durability and surface write errors instead of losing them.
424
+ await settingsManager.flush();
425
+ const writeErrors = settingsManager.drainErrors();
426
+ if (writeErrors.length > 0) {
427
+ const detail = writeErrors.map((e) => `${e.scope}: ${e.error.message}`).join("; ");
428
+ return { ok: false, error: `Failed to persist settings: ${detail}` };
429
+ }
430
+ return warnings.length > 0
431
+ ? { ok: true, settings: getSettingsForRpc(settingsManager), warnings }
432
+ : { ok: true, settings: getSettingsForRpc(settingsManager) };
433
+ });
434
+ }
435
+ function normalizePreview(text) {
436
+ return text.replace(/\s+/g, " ").trim().slice(0, 200);
437
+ }
438
+ function extractTextContent(content) {
439
+ if (typeof content === "string")
440
+ return content;
441
+ if (!Array.isArray(content))
442
+ return "";
443
+ let text = "";
444
+ for (const part of content) {
445
+ if (typeof part === "object" &&
446
+ part !== null &&
447
+ "type" in part &&
448
+ part.type === "text" &&
449
+ "text" in part &&
450
+ typeof part.text === "string") {
451
+ text += part.text;
452
+ }
453
+ }
454
+ return text;
455
+ }
456
+ function getRpcEntryPreview(node) {
457
+ const entry = node.entry;
458
+ switch (entry.type) {
459
+ case "message": {
460
+ const msg = entry.message;
461
+ const role = msg.role;
462
+ if (role === "user") {
463
+ return normalizePreview(extractTextContent(msg.content));
464
+ }
465
+ if (role === "assistant") {
466
+ const textContent = normalizePreview(extractTextContent(msg.content));
467
+ if (textContent)
468
+ return textContent;
469
+ if (msg.stopReason === "aborted")
470
+ return "(aborted)";
471
+ if (msg.errorMessage)
472
+ return normalizePreview(msg.errorMessage);
473
+ return "(no content)";
474
+ }
475
+ if (role === "toolResult") {
476
+ return normalizePreview(`[${msg.toolName ?? "tool"}]`);
477
+ }
478
+ if (role === "bashExecution") {
479
+ return normalizePreview(`[bash]: ${msg.command ?? ""}`);
480
+ }
481
+ return normalizePreview(`[${role}]`);
482
+ }
483
+ case "custom_message":
484
+ return normalizePreview(`[${entry.customType}]: ${extractTextContent(entry.content)}`);
485
+ case "compaction":
486
+ return normalizePreview(`[compaction: ${Math.round(entry.tokensBefore / 1000)}k tokens]`);
487
+ case "branch_summary":
488
+ return normalizePreview(`[branch summary]: ${entry.summary}`);
489
+ case "model_change":
490
+ return normalizePreview(`[model: ${entry.modelId}]`);
491
+ case "thinking_level_change":
492
+ return normalizePreview(`[thinking: ${entry.thinkingLevel}]`);
493
+ case "custom":
494
+ return normalizePreview(`[custom: ${entry.customType}]`);
495
+ case "label":
496
+ return normalizePreview(`[label: ${entry.label ?? "(cleared)"}]`);
497
+ case "session_info":
498
+ return normalizePreview(`[title: ${entry.name || "empty"}]`);
499
+ default: {
500
+ // Compile-time exhaustiveness guard: a new SessionEntry type forces an update here.
501
+ const _exhaustive = entry;
502
+ // Runtime: unknown types from forward-compat/corrupt session files get a placeholder.
503
+ return normalizePreview(`[${entry.type}]`);
504
+ }
505
+ }
506
+ }
507
+ function toRpcTreeNode(node) {
508
+ const role = node.entry.type === "message" ? String(node.entry.message.role) : undefined;
509
+ return {
510
+ id: node.entry.id,
511
+ parentId: node.entry.parentId,
512
+ type: node.entry.type,
513
+ ...(role !== undefined ? { role } : {}),
514
+ preview: getRpcEntryPreview(node),
515
+ timestamp: node.entry.timestamp,
516
+ ...(node.label !== undefined ? { label: node.label } : {}),
517
+ children: [],
518
+ };
519
+ }
520
+ /**
521
+ * Map core session tree nodes to stable RPC DTOs without leaking raw entries/messages.
522
+ * Uses an explicit stack so deep linear session trees do not overflow the JS call stack.
523
+ */
524
+ export function toRpcTreeNodes(nodes) {
525
+ const roots = [];
526
+ const stack = [];
527
+ for (let i = nodes.length - 1; i >= 0; i--) {
528
+ stack.push({ source: nodes[i], targetSiblings: roots });
529
+ }
530
+ while (stack.length > 0) {
531
+ const { source, targetSiblings } = stack.pop();
532
+ const dto = toRpcTreeNode(source);
533
+ targetSiblings.push(dto);
534
+ for (let i = source.children.length - 1; i >= 0; i--) {
535
+ stack.push({ source: source.children[i], targetSiblings: dto.children });
536
+ }
537
+ }
538
+ return roots;
539
+ }
540
+ /** Return the current session tree and active leaf as RPC DTOs. */
541
+ export function getTreeForRpc(sessionManager) {
542
+ return { roots: toRpcTreeNodes(sessionManager.getTree()), leafId: sessionManager.getLeafId() };
543
+ }
544
+ /** Navigate the active session tree, returning only the stable RPC result fields. */
545
+ export async function navigateTreeForRpc(session, targetId, options) {
546
+ const result = await session.navigateTree(targetId, options ?? {});
547
+ return {
548
+ cancelled: result.cancelled,
549
+ ...(result.editorText !== undefined ? { editorText: result.editorText } : {}),
550
+ };
551
+ }
24
552
  /**
25
553
  * Run in RPC mode.
26
554
  * Listens for JSON commands on stdin, outputs events and responses on stdout.
@@ -39,10 +567,18 @@ export async function runRpcMode(session, modelFallbackMessage) {
39
567
  const error = (id, command, message) => {
40
568
  return { id, type: "response", command, success: false, error: message };
41
569
  };
570
+ if (session.sessionFile && session.messages.length > 0) {
571
+ const rehydratedCount = rehydrateBackgroundAgentsFromDisk(session.sessionFile);
572
+ if (rehydratedCount > 0) {
573
+ console.error(`[rpc] Rehydrated ${rehydratedCount} background subagent${rehydratedCount === 1 ? "" : "s"} from disk`);
574
+ }
575
+ }
42
576
  // Pending extension UI requests waiting for response
43
577
  const pendingExtensionRequests = new Map();
44
578
  // Shutdown request flag
45
579
  let shutdownRequested = false;
580
+ let dailyCostTracker;
581
+ let dailyCostTrackerPrimed = false;
46
582
  /** Helper for dialog methods with signal/timeout support */
47
583
  function createDialogPromise(opts, defaultValue, request, parseResponse) {
48
584
  if (opts?.signal?.aborted)
@@ -244,8 +780,51 @@ export async function runRpcMode(session, modelFallbackMessage) {
244
780
  output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
245
781
  },
246
782
  });
783
+ const getDailyCost = async () => {
784
+ dailyCostTracker ??= new DailyCostTracker();
785
+ if (!dailyCostTrackerPrimed) {
786
+ await dailyCostTracker.refresh();
787
+ dailyCostTrackerPrimed = true;
788
+ }
789
+ return dailyCostTracker.getDailyCost();
790
+ };
791
+ const cwd = session.sessionManager.getCwd();
792
+ const tabTitleSettings = session.settingsManager.getTabTitleSettings();
793
+ const tabTitleGenerator = !session.sessionName && tabTitleSettings?.enabled !== false
794
+ ? new TabTitleGenerator(tabTitleSettings, {
795
+ setTitle: () => { },
796
+ setSessionName: (name) => {
797
+ if (!session.sessionName) {
798
+ session.setSessionName(name);
799
+ }
800
+ },
801
+ getMessages: () => session.messages,
802
+ getModel: () => session.model,
803
+ getModelRegistry: () => session.modelRegistry,
804
+ getProvider: () => session.model?.provider,
805
+ getAgentModelsOverride: (name) => session.settingsManager.getAgentModelsForAgent(name),
806
+ getBranch: () => getGitBranch(cwd),
807
+ getRepo: () => basename(cwd),
808
+ getCwd: () => cwd,
809
+ onError: (err) => {
810
+ console.error(`[rpc] Tab title auto-generation failed: ${err instanceof Error ? err.message : String(err)}`);
811
+ },
812
+ })
813
+ : undefined;
247
814
  // Output all agent events as JSON
248
815
  session.subscribe((event) => {
816
+ if (tabTitleGenerator && !session.sessionName) {
817
+ if (event.type === "tool_execution_end") {
818
+ tabTitleGenerator.onToolEnd({
819
+ toolName: event.toolName,
820
+ isError: event.isError,
821
+ result: event.result,
822
+ });
823
+ }
824
+ else if (event.type === "message_end") {
825
+ tabTitleGenerator.onMessageEnd(event.message);
826
+ }
827
+ }
249
828
  output(event);
250
829
  });
251
830
  // Handle a single command
@@ -291,22 +870,16 @@ export async function runRpcMode(session, modelFallbackMessage) {
291
870
  // State
292
871
  // =================================================================
293
872
  case "get_state": {
294
- const state = {
295
- model: session.model,
296
- thinkingLevel: session.thinkingLevel,
297
- isStreaming: session.isStreaming,
298
- isCompacting: session.isCompacting,
299
- steeringMode: session.steeringMode,
300
- followUpMode: session.followUpMode,
301
- sessionFile: session.sessionFile,
302
- sessionId: session.sessionId,
303
- sessionName: session.sessionName,
304
- autoCompactionEnabled: session.autoCompactionEnabled,
305
- messageCount: session.messages.length,
306
- pendingMessageCount: session.pendingMessageCount,
307
- modelFallbackMessage,
308
- };
309
- return success(id, "get_state", state);
873
+ return success(id, "get_state", getStateForRpc(session, modelFallbackMessage));
874
+ }
875
+ case "get_resources": {
876
+ return success(id, "get_resources", getResourcesForRpc(session));
877
+ }
878
+ case "get_git_branch": {
879
+ return success(id, "get_git_branch", { branch: getGitBranch(session.sessionManager.getCwd()) });
880
+ }
881
+ case "get_daily_cost": {
882
+ return success(id, "get_daily_cost", { cost: await getDailyCost() });
310
883
  }
311
884
  // =================================================================
312
885
  // Model
@@ -400,6 +973,12 @@ export async function runRpcMode(session, modelFallbackMessage) {
400
973
  session.setFollowUpMode(command.mode);
401
974
  return success(id, "set_follow_up_mode");
402
975
  }
976
+ case "get_pending_messages": {
977
+ return success(id, "get_pending_messages", getPendingMessagesForRpc(session));
978
+ }
979
+ case "clear_pending_messages": {
980
+ return success(id, "clear_pending_messages", session.clearQueue());
981
+ }
403
982
  // =================================================================
404
983
  // Compaction
405
984
  // =================================================================
@@ -411,6 +990,10 @@ export async function runRpcMode(session, modelFallbackMessage) {
411
990
  session.setAutoCompactionEnabled(command.enabled);
412
991
  return success(id, "set_auto_compaction");
413
992
  }
993
+ case "abort_compaction": {
994
+ session.abortCompaction();
995
+ return success(id, "abort_compaction");
996
+ }
414
997
  // =================================================================
415
998
  // Retry
416
999
  // =================================================================
@@ -451,6 +1034,13 @@ export async function runRpcMode(session, modelFallbackMessage) {
451
1034
  const cancelled = !(await session.switchSession(command.sessionPath));
452
1035
  return success(id, "switch_session", { cancelled });
453
1036
  }
1037
+ case "delete_session": {
1038
+ const result = await deleteSessionForRpc(session.sessionManager, command.sessionPath);
1039
+ if (!result.ok) {
1040
+ return error(id, "delete_session", result.error);
1041
+ }
1042
+ return success(id, "delete_session", { method: result.method });
1043
+ }
454
1044
  case "fork": {
455
1045
  const result = await session.fork(command.entryId);
456
1046
  return success(id, "fork", { text: result.selectedText, cancelled: result.cancelled });
@@ -459,6 +1049,23 @@ export async function runRpcMode(session, modelFallbackMessage) {
459
1049
  const messages = session.getUserMessagesForForking();
460
1050
  return success(id, "get_fork_messages", { messages });
461
1051
  }
1052
+ case "get_tree": {
1053
+ return success(id, "get_tree", getTreeForRpc(session.sessionManager));
1054
+ }
1055
+ case "navigate_tree": {
1056
+ try {
1057
+ const result = await navigateTreeForRpc(session, command.targetId, {
1058
+ summarize: command.summarize,
1059
+ customInstructions: command.customInstructions,
1060
+ replaceInstructions: command.replaceInstructions,
1061
+ label: command.label,
1062
+ });
1063
+ return success(id, "navigate_tree", result);
1064
+ }
1065
+ catch (e) {
1066
+ return error(id, "navigate_tree", e instanceof Error ? e.message : String(e));
1067
+ }
1068
+ }
462
1069
  case "get_last_assistant_text": {
463
1070
  const text = session.getLastAssistantText();
464
1071
  return success(id, "get_last_assistant_text", { text });
@@ -487,17 +1094,38 @@ export async function runRpcMode(session, modelFallbackMessage) {
487
1094
  const cwd = session.sessionManager.getCwd();
488
1095
  const sessionDir = session.sessionManager.getSessionDir();
489
1096
  const sessions = await SessionManager.list(cwd, sessionDir);
490
- const data = sessions.map((s) => ({
491
- path: s.path,
492
- id: s.id,
493
- cwd: s.cwd,
494
- name: s.name,
495
- created: s.created.toISOString(),
496
- modified: s.modified.toISOString(),
497
- messageCount: s.messageCount,
498
- firstMessage: s.firstMessage,
499
- }));
500
- return success(id, "list_sessions", { sessions: data });
1097
+ return success(id, "list_sessions", { sessions: sessions.map(toRpcSessionInfo) });
1098
+ }
1099
+ case "list_all_sessions": {
1100
+ return success(id, "list_all_sessions", { sessions: await listAllSessionsForRpc() });
1101
+ }
1102
+ // =================================================================
1103
+ // Background agents
1104
+ // =================================================================
1105
+ case "list_background_agents": {
1106
+ return success(id, "list_background_agents", {
1107
+ agents: getBackgroundAgents().map(toRpcBackgroundAgentInfo),
1108
+ });
1109
+ }
1110
+ case "list_agent_types": {
1111
+ return success(id, "list_agent_types", {
1112
+ agentTypes: listAgentTypesForRpc(session.sessionManager.getCwd()),
1113
+ });
1114
+ }
1115
+ // =================================================================
1116
+ // Settings (persistent defaults)
1117
+ // =================================================================
1118
+ case "get_settings": {
1119
+ return success(id, "get_settings", getSettingsForRpc(session.settingsManager));
1120
+ }
1121
+ case "set_settings": {
1122
+ const result = await setSettingsForRpc(session.settingsManager, session.modelRegistry, command.settings);
1123
+ if (!result.ok) {
1124
+ return error(id, "set_settings", result.error);
1125
+ }
1126
+ return success(id, "set_settings", result.warnings && result.warnings.length > 0
1127
+ ? { ...result.settings, warnings: result.warnings }
1128
+ : result.settings);
501
1129
  }
502
1130
  // =================================================================
503
1131
  // Version
@@ -549,6 +1177,7 @@ export async function runRpcMode(session, modelFallbackMessage) {
549
1177
  if (currentRunner?.hasHandlers("session_shutdown")) {
550
1178
  await currentRunner.emit({ type: "session_shutdown" });
551
1179
  }
1180
+ dailyCostTracker?.dispose();
552
1181
  detachInput();
553
1182
  process.stdin.pause();
554
1183
  process.exit(0);