mixdog 1.0.5 → 1.0.7

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 (50) hide show
  1. package/README.md +206 -475
  2. package/package.json +11 -4
  3. package/src/defaults/skills/setup/references/actions.md +26 -6
  4. package/src/defaults/skills/setup/references/surfaces.md +8 -3
  5. package/src/runtime/agent/orchestrator/runtime-core/builtin-features.mjs +16 -7
  6. package/src/runtime/agent/orchestrator/runtime-core/config-helpers.mjs +6 -2
  7. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +0 -3
  8. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +0 -2
  9. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +24 -2
  10. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +25 -0
  11. package/src/runtime/agent/orchestrator/session/store-transcript-worker.mjs +14 -1
  12. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +3 -45
  13. package/src/runtime/channels/lib/config.mjs +0 -1
  14. package/src/runtime/channels/lib/owned-runtime/config-reload.mjs +1 -1
  15. package/src/runtime/channels/lib/scheduler.mjs +15 -56
  16. package/src/runtime/channels/lib/worker-main.mjs +2 -9
  17. package/src/runtime/shared/config.mjs +66 -1
  18. package/src/runtime/shared/llm/usage-ledger-quota.mjs +13 -2
  19. package/src/runtime/shared/path-executable.mjs +58 -0
  20. package/src/session-runtime/boot/apis.mjs +6 -2
  21. package/src/session-runtime/boot/begin.mjs +0 -2
  22. package/src/session-runtime/boot/providers.mjs +3 -0
  23. package/src/session-runtime/boot/tools.mjs +0 -1
  24. package/src/session-runtime/config-lifecycle.mjs +11 -0
  25. package/src/session-runtime/internal-tool-executor.mjs +0 -2
  26. package/src/session-runtime/runtime-feature-gates.mjs +2 -3
  27. package/src/session-runtime/schedule-session-run.mjs +0 -1
  28. package/src/session-runtime/services/channel-admin.mjs +8 -26
  29. package/src/session-runtime/settings-compaction-api.mjs +5 -1
  30. package/src/session-runtime/settings-system-api.mjs +2 -20
  31. package/src/session-runtime/setup-tool/executor.mjs +61 -3
  32. package/src/session-runtime/setup-tool/extended-actions.mjs +31 -6
  33. package/src/session-runtime/setup-tool/settings-contract.mjs +27 -3
  34. package/src/session-runtime/setup-tool/tool-defs.mjs +35 -5
  35. package/src/session-runtime/webhook-session-run.mjs +0 -2
  36. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +13 -3
  37. package/src/standalone/daemon-stored-session-views.mjs +4 -0
  38. package/src/standalone/session-protocol.mjs +0 -1
  39. package/src/standalone/session-service/session-calls/session-views.mjs +10 -4
  40. package/src/standalone/session-service/stored-reader.mjs +20 -2
  41. package/src/standalone/session-service/viewers.mjs +3 -3
  42. package/src/standalone/session-service.mjs +3 -1
  43. package/src/tui/App.jsx +6 -1
  44. package/src/tui/app/create-app-pickers.mjs +1 -0
  45. package/src/tui/app/doctor.mjs +6 -14
  46. package/src/tui/app/maintenance-pickers/auto-clear-picker.mjs +8 -3
  47. package/src/tui/app/settings-picker/settings-rows.mjs +5 -1
  48. package/src/tui/app/use-ui-open-request.mjs +13 -3
  49. package/src/tui/dist/index.mjs +41 -9
  50. package/src/tui/session/session-api/settings.mjs +0 -6
@@ -188,17 +188,13 @@ function scheduleToDisplay(s) {
188
188
  whenAt: s.whenAt || undefined,
189
189
  whenCron: s.whenCron || undefined,
190
190
  timezone: s.timezone || undefined,
191
- channel: s.channelId || undefined,
192
191
  model: s.model || undefined,
193
192
  cwd: s.cwd || undefined,
194
193
  workflow: s.workflow || undefined,
195
194
  attachments: s.attachments || undefined,
196
- // Delivery mode: legacy channel-target rows (pre-delivery) behaved as
197
- // relay+visible, which maps to 'both'.
198
- delivery: s.delivery || (s.target === 'channel' ? 'both' : 'app'),
199
195
  enabled: s.enabled !== false,
200
196
  instructions: s.prompt,
201
- route: s.target === 'channel' ? `channel:${s.channelId}` : 'session',
197
+ route: 'session',
202
198
  };
203
199
  }
204
200
 
@@ -210,7 +206,7 @@ export async function listSchedules() {
210
206
  // Register or update a schedule in the PG store. Recurring input maps `time`
211
207
  // (+ optional `days`) to a cron; one-shot input maps an `at` datetime; the two
212
208
  // are mutually exclusive (also enforced by the store's when_at/when_cron XOR).
213
- // `channel` selects a channel target; otherwise session. Both require a model.
209
+ // Every schedule runs as a visible app session and requires a model.
214
210
  export async function saveSchedule({
215
211
  name,
216
212
  description = '',
@@ -218,12 +214,10 @@ export async function saveSchedule({
218
214
  at,
219
215
  timezone,
220
216
  days,
221
- channel,
222
217
  model,
223
218
  cwd,
224
219
  workflow,
225
220
  attachments,
226
- delivery,
227
221
  enabled,
228
222
  instructions,
229
223
  overwrite = false,
@@ -233,11 +227,6 @@ export async function saveSchedule({
233
227
  if (!body) throw new Error('schedule instructions are required');
234
228
  const scheduleModel = String(model || '').trim();
235
229
  if (!scheduleModel) throw new Error('schedule model is required — choose a model before saving');
236
- // 'app' → session-only; 'channel'/'both' → the run result relays to the
237
- // main channel (target 'channel', channelId resolved at fire time).
238
- const requestedMode = String(delivery || '').trim();
239
- let mode = channel ? 'both' : 'app';
240
- if (['app', 'channel', 'both'].includes(requestedMode)) mode = requestedMode;
241
230
  const hasTime = time != null && String(time).trim() !== '';
242
231
  const hasAt = at != null && String(at).trim() !== '';
243
232
  if (hasTime && hasAt) throw new Error('provide either `time` (recurring) or `at` (one-shot), not both');
@@ -254,13 +243,13 @@ export async function saveSchedule({
254
243
  whenCron,
255
244
  whenAt,
256
245
  timezone: scheduleTimezone,
257
- target: mode === 'app' ? 'session' : 'channel',
258
- channelId: channel ? String(channel).trim() : null,
246
+ target: 'session',
247
+ channelId: null,
259
248
  model: scheduleModel,
260
249
  cwd: cwd ? String(cwd).trim() : null,
261
250
  workflow: workflow ? String(workflow).trim() : null,
262
251
  attachments: normalizeAutomationAttachments(attachments),
263
- delivery: mode,
252
+ delivery: 'app',
264
253
  prompt: body,
265
254
  enabled: enabled !== false,
266
255
  nextFireAt: whenAt,
@@ -294,19 +283,17 @@ async function listWebhooks() {
294
283
  name: ep.name,
295
284
  description: ep.description || '',
296
285
  parser: ep.parser || 'github',
297
- ...(ep.channelId ? { channel: ep.channelId } : {}),
298
286
  ...(ep.model ? { model: ep.model } : {}),
299
287
  ...(ep.cwd ? { cwd: ep.cwd } : {}),
300
288
  ...(ep.workflow ? { workflow: ep.workflow } : {}),
301
289
  ...(ep.attachments ? { attachments: ep.attachments } : {}),
302
- delivery: ep.delivery || 'app',
303
290
  enabled: ep.enabled,
304
291
  // The store never projects the plaintext secret through list paths; it
305
292
  // exposes a presence flag (secretSet) instead.
306
293
  secretSet: ep.secretSet === true,
307
294
  secret: undefined,
308
295
  instructions: ep.instructions,
309
- route: ep.channelId ? `channel:${ep.channelId}` : 'session',
296
+ route: 'session',
310
297
  }));
311
298
  }
312
299
 
@@ -315,12 +302,10 @@ export async function saveWebhook({
315
302
  description = '',
316
303
  parser = 'github',
317
304
  secret,
318
- channel,
319
305
  model,
320
306
  cwd,
321
307
  workflow,
322
308
  attachments,
323
- delivery,
324
309
  enabled,
325
310
  instructions,
326
311
  overwrite = false,
@@ -334,7 +319,6 @@ export async function saveWebhook({
334
319
  }
335
320
  const body = String(instructions || '').trim();
336
321
  if (!body) throw new Error('webhook instructions are required');
337
- if (channel && !model) throw new Error('model is required when channel is set');
338
322
  if (overwrite !== true && (await dbLoadEndpoint(id))) {
339
323
  throw new Error(`webhook "${id}" already exists`);
340
324
  }
@@ -350,12 +334,12 @@ export async function saveWebhook({
350
334
  name: id,
351
335
  description: String(description || '').trim(),
352
336
  parser: nextParser,
353
- channelId: channel ? String(channel).trim() : null,
337
+ channelId: null,
354
338
  model: model ? String(model).trim() : null,
355
339
  cwd: cwd ? String(cwd).trim() : null,
356
340
  workflow: workflow ? String(workflow).trim() : null,
357
341
  attachments: normalizeAutomationAttachments(attachments),
358
- delivery: ['app', 'channel', 'both'].includes(String(delivery || '').trim()) ? String(delivery).trim() : 'app',
342
+ delivery: 'app',
359
343
  secret: secretValue,
360
344
  instructions: body,
361
345
  enabled: enabled !== false,
@@ -365,12 +349,10 @@ export async function saveWebhook({
365
349
  name: id,
366
350
  description: saved.description,
367
351
  parser: saved.parser,
368
- ...(saved.channelId ? { channel: saved.channelId } : {}),
369
352
  ...(saved.model ? { model: saved.model } : {}),
370
353
  ...(saved.cwd ? { cwd: saved.cwd } : {}),
371
354
  ...(saved.workflow ? { workflow: saved.workflow } : {}),
372
355
  ...(saved.attachments ? { attachments: saved.attachments } : {}),
373
- delivery: saved.delivery || 'app',
374
356
  ...(enabled === false ? { enabled: false } : {}),
375
357
  secret: secretValue,
376
358
  instructions: body,
@@ -39,6 +39,7 @@ export function createCompactionSettings({
39
39
  hasOwn,
40
40
  normalizeAutoClearConfig,
41
41
  autoClearIdleMsForProvider,
42
+ resolveAutoClearIdleMs,
42
43
  normalizeCompactionConfig,
43
44
  autoClearProviderDefaults,
44
45
  parseDurationMs,
@@ -96,7 +97,9 @@ export function createCompactionSettings({
96
97
  const normalized = normalizeAutoClearConfig(config.autoClear);
97
98
  const provider = route?.provider || null;
98
99
  const providerDefault = autoClearIdleMsForProvider(provider, normalized.providerIdleMs);
99
- const idleMs = normalized.custom ? normalized.idleMs : providerDefault;
100
+ const idleMs = resolveAutoClearIdleMs(config, provider);
101
+ const key = String(provider || '').toLowerCase();
102
+ const providerCustom = Boolean(key && key !== 'default' && normalized.providerIdleMs?.[key]);
100
103
  // Advanced picker shows only providers the user actually has enabled
101
104
  // (config.providers[*].enabled), plus the active route provider, any
102
105
  // provider with a custom override, and the 'default' fallback row —
@@ -114,6 +117,7 @@ export function createCompactionSettings({
114
117
  enabled: normalized.enabled,
115
118
  idleMs,
116
119
  custom: normalized.custom,
120
+ providerCustom,
117
121
  providerDefault,
118
122
  provider,
119
123
  providerDefaults,
@@ -1,39 +1,21 @@
1
- // Channels, the system shell and self-update settings.
1
+ // Automation worker status, the system shell and self-update settings.
2
2
  export function createSystemSettings({
3
3
  getConfig,
4
4
  saveConfigAndAdopt,
5
5
  normalizeSystemShellConfig,
6
6
  normalizeSystemShellCommand,
7
7
  setConfiguredShell,
8
- setModuleEnabledInConfig,
9
8
  localPackageVersion,
10
- channelsEnabled,
11
9
  autoUpdateEnabled,
12
10
  getUpdateCheckState,
13
11
  getUpdateProcessState,
14
- invalidatePreSessionToolSurface,
15
12
  channels,
16
- clearChannelStartTimer,
17
13
  checkForUpdateInternal,
18
14
  runUpdateNowInternal,
19
15
  }) {
20
16
  return {
21
17
  getChannelSettings(options = {}) {
22
- return {
23
- enabled: channelsEnabled(),
24
- ...(options?.includeStatus === false ? {} : { status: channels.status() }),
25
- };
26
- },
27
- async setChannelsEnabled(enabled) {
28
- const config = getConfig();
29
- const nextConfig = setModuleEnabledInConfig({ ...config }, 'channels', enabled !== false);
30
- saveConfigAndAdopt(nextConfig);
31
- if (!channelsEnabled()) {
32
- clearChannelStartTimer();
33
- await channels.stop('settings-disabled', { waitForExit: false }).catch(() => {});
34
- }
35
- invalidatePreSessionToolSurface();
36
- return this.getChannelSettings();
18
+ return options?.includeStatus === false ? {} : { status: channels.status() };
37
19
  },
38
20
  getSystemShell() {
39
21
  const config = getConfig();
@@ -18,6 +18,12 @@ import {
18
18
  } from './tool-defs.mjs';
19
19
  import { schemaValueError } from '../../runtime/shared/schema-value-error.mjs';
20
20
  import { clean } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
21
+ import profileConfig from '../../runtime/shared/profile-config.cjs';
22
+
23
+ const PROFILE_VALUE_IDS = Object.freeze({
24
+ language: profileConfig.PROFILE_LANGUAGES.map((entry) => entry.id),
25
+ experienceLevel: ['', ...profileConfig.PROFILE_EXPERIENCE_LEVELS.map((entry) => entry.id)],
26
+ });
21
27
 
22
28
  const ACTION_APPLIES_TO = {
23
29
  set_recap_enabled: 'background Memory cycles; no restart required',
@@ -49,6 +55,9 @@ const OPEN_TARGET_HINTS = Object.freeze({
49
55
  usage: 'Desktop: /usage in the composer · TUI: /usage',
50
56
  doctor: 'Desktop: Settings → System → Doctor, or /doctor in the composer · TUI: /doctor',
51
57
  context: 'Desktop: /context in the composer · TUI: /context',
58
+ developer: 'Desktop: Settings → Developer options · TUI: /setting → Developer',
59
+ voice: 'Desktop: Extensions → Plugin → Built-in (Voice) · TUI: /setting → Voice',
60
+ connection: 'Desktop only: Settings → Connection',
52
61
  });
53
62
 
54
63
  function requireEnum(value, allowed, label) {
@@ -123,6 +132,20 @@ function publicProviderRows(setup) {
123
132
  };
124
133
  }
125
134
 
135
+ /** OAuth account roster without usage meters or provider-side identities. */
136
+ function publicAccounts(pool) {
137
+ return {
138
+ selectedId: pool.selectedId,
139
+ auto: pool.auto,
140
+ accounts: pool.accounts.map(({ id, label, authenticated, reauthRequired }) => ({
141
+ id,
142
+ label,
143
+ authenticated,
144
+ reauthRequired,
145
+ })),
146
+ };
147
+ }
148
+
126
149
  function mcpRows(status) {
127
150
  return {
128
151
  connectedCount: Number(status?.connectedCount || 0),
@@ -241,7 +264,14 @@ const SETUP_STATUS_READERS = {
241
264
  };
242
265
  },
243
266
  shell: (rt) => rt.getSystemShell?.() || {},
244
- providers: async (rt) => publicProviderRows(await rt.getProviderSetup?.({})),
267
+ providers: async (rt) => {
268
+ const rows = publicProviderRows(await rt.getProviderSetup?.({}));
269
+ rows.oauth = rows.oauth.map((row) =>
270
+ row.authenticated ? { ...row, accounts: publicAccounts(rt.getProviderAccounts(row.id)) } : row
271
+ );
272
+ return rows;
273
+ },
274
+ developer: (rt) => rt.getDeveloperSettings(),
245
275
  mcp: (rt) => mcpRows(rt.mcpStatus?.()),
246
276
  skills: (rt) => ({ ...(rt.skillsStatus?.() || {}), disabled: rt.getDisabledSkills?.()?.disabled || [] }),
247
277
  plugins: (rt) => rt.pluginsStatus?.() || {},
@@ -274,7 +304,7 @@ function validateSetupInput(args) {
274
304
  throw new Error('route.contextPercent is only accepted by set_route');
275
305
  }
276
306
  }
277
- for (const field of ['desktop', 'appearance', 'webhook', 'compaction']) {
307
+ for (const field of ['desktop', 'appearance', 'webhook', 'compaction', 'providerAccount']) {
278
308
  if (args[field] && !Object.keys(args[field]).length) throw new Error(`${field} requires at least one setting`);
279
309
  }
280
310
  return action;
@@ -326,6 +356,11 @@ const SETUP_ACTION_HANDLERS = {
326
356
  const profile = args.profile && typeof args.profile === 'object' ? args.profile : null;
327
357
  if (!profile || !Object.keys(profile).length)
328
358
  throw new Error('profile with title, language, or experienceLevel is required');
359
+ for (const [field, ids] of Object.entries(PROFILE_VALUE_IDS)) {
360
+ if (Object.hasOwn(profile, field) && !ids.includes(profile[field])) {
361
+ throw new Error(`profile.${field} must be one of ${ids.map((id) => JSON.stringify(id)).join(', ')}`);
362
+ }
363
+ }
329
364
  const result = rt.setProfile(profile);
330
365
  return {
331
366
  title: result.title || '',
@@ -414,7 +449,23 @@ const SETUP_ACTION_HANDLERS = {
414
449
  rt.deleteLocalProviderModel(requireText(args.confirmationToken, 'confirmationToken')),
415
450
  set_system_shell: (rt, args) => rt.setSystemShell({ command: clean(args.command) }),
416
451
  set_auto_update: (rt, args) => rt.setAutoUpdate(requireBoolean(args.enabled)),
417
- forget_provider_auth: (rt, args) => rt.forgetProviderAuth(requireText(args.name, 'name')),
452
+ forget_provider_auth: async (rt, args) => {
453
+ const name = requireText(args.name, 'name');
454
+ const setup = await rt.getProviderSetup({});
455
+ if (!(setup?.oauth || []).some((row) => row.id === name)) {
456
+ if (Object.hasOwn(args, 'accountId')) throw new Error('accountId applies only to OAuth providers');
457
+ return rt.forgetProviderAuth(name);
458
+ }
459
+ const { accounts } = rt.getProviderAccounts(name);
460
+ if (!accounts.length) throw new Error(`${name} has no connected account`);
461
+ if (!Object.hasOwn(args, 'accountId') && accounts.length > 1) {
462
+ throw new Error(`${name} has ${accounts.length} accounts; pass accountId from status providers`);
463
+ }
464
+ const accountId = Object.hasOwn(args, 'accountId') ? requireText(args.accountId, 'accountId') : accounts[0].id;
465
+ return { ...(await rt.forgetProviderAuth(name, accountId)), accountId };
466
+ },
467
+ set_provider_account: async (rt, args) =>
468
+ publicAccounts(await rt.updateProviderAccounts(requireText(args.name, 'name'), args.providerAccount)),
418
469
  add_mcp_server: async (rt, args) => {
419
470
  const server = mcpServerInput(args);
420
471
  requireText(server.name, 'server.name');
@@ -456,6 +507,13 @@ const SETUP_ACTION_HANDLERS = {
456
507
  remove_plugin: async (rt, args) => ({
457
508
  plugin: (await rt.removePlugin(requireText(args.name, 'name')))?.plugin || null,
458
509
  }),
510
+ enable_plugin_mcp: async (rt, args) => {
511
+ const name = requireText(args.name, 'name');
512
+ const plugin = ((await rt.pluginsStatus())?.plugins || []).find((row) => row.name === name || row.id === name);
513
+ if (!plugin) throw new Error(`plugin "${name}" not found`);
514
+ const result = await rt.enablePluginMcp(plugin);
515
+ return { serverName: result.serverName, mcp: mcpRows(result.status) };
516
+ },
459
517
  };
460
518
 
461
519
  const READ_ONLY_SETUP_ACTIONS = new Set([
@@ -1,4 +1,3 @@
1
- import { parseSkillDocument } from '../../runtime/shared/skill-document.mjs';
2
1
  import { SETUP_EXTENDED_PROPERTIES, SETUP_DESKTOP_ACTIONS } from './settings-contract.mjs';
3
2
 
4
3
  const own = (value, key) => Object.hasOwn(value || {}, key);
@@ -76,15 +75,16 @@ export function validateMcpInput(server) {
76
75
  async function readDefinition(rt, kind, name) {
77
76
  if (kind === 'workflow') return rt.getWorkflowPack(name);
78
77
  if (kind === 'agent') return rt.getAgentDefinition(name);
78
+ // skillContent already returns the SKILL.md body without frontmatter; the
79
+ // frontmatter fields come from the catalog row.
79
80
  const resource = await rt.skillContent(name);
80
81
  const metadata = (await rt.skillsStatus()).skills.find((row) => row.name === name) || {};
81
- const parsed = parseSkillDocument(resource.content);
82
82
  return {
83
83
  name,
84
84
  originalName: name,
85
- description: metadata.description || parsed.description || '',
86
- whenToUse: metadata.whenToUse || parsed.whenToUse || '',
87
- body: parsed.body,
85
+ description: metadata.description || '',
86
+ whenToUse: metadata.whenToUse || '',
87
+ body: resource.content,
88
88
  toolDependencies: metadata.toolDependencies || [],
89
89
  editable: metadata.editable === true,
90
90
  };
@@ -103,6 +103,19 @@ async function saveDefinition(rt, args) {
103
103
  const id = input.originalName || input.id || input.name;
104
104
  if (!creating && !String(id || '').trim()) throw new Error('An existing definition identity is required');
105
105
  const current = creating ? {} : await readDefinition(rt, kind, id);
106
+ if (kind === 'skill' && !creating && !current.editable) {
107
+ // Built-in and plugin skills keep their document; like the Skills editor,
108
+ // only their tool dependencies are user-editable (null restores them).
109
+ const edits = Object.keys(input).filter((key) => key !== 'originalName' && key !== 'name');
110
+ if (edits.length !== 1 || edits[0] !== 'toolDependencies' || (own(input, 'name') && input.name !== current.name)) {
111
+ throw new Error('Built-in and plugin skills accept only toolDependencies edits');
112
+ }
113
+ return rt.saveSkill({
114
+ originalName: current.originalName,
115
+ dependenciesOnly: true,
116
+ toolDependencies: input.toolDependencies,
117
+ });
118
+ }
106
119
  const next = { ...pick(current, definitionKeys), ...input };
107
120
  if (!String(next.body || '').trim()) throw new Error('definition.body must not be empty');
108
121
  if (kind === 'workflow') {
@@ -115,7 +128,6 @@ async function saveDefinition(rt, args) {
115
128
  }
116
129
  return rt.saveAgentDefinition(creating ? next : { ...next, id: current.id });
117
130
  }
118
- if (!creating && !current.editable) throw new Error('Only machine-global user skills can be edited');
119
131
  return creating ? rt.addSkill(next) : rt.saveSkill({ ...next, originalName: current.originalName });
120
132
  }
121
133
 
@@ -188,6 +200,19 @@ export const EXTENDED_SETUP_HANDLERS = Object.freeze({
188
200
  args.automationKind === 'schedule'
189
201
  ? rt.setScheduleEnabled(args.name, args.enabled)
190
202
  : rt.setWebhookEnabled(args.name, args.enabled),
203
+ async set_developer_option(rt, args) {
204
+ const option = rt
205
+ .getDeveloperSettings()
206
+ .sections.flatMap((section) => section.options)
207
+ .find((row) => row.id === args.name);
208
+ if (!option) throw new Error(`Unknown developer option "${args.name}"; read status developer`);
209
+ if (args.enabled && option.warning && args.riskAccepted !== true) {
210
+ throw new Error(
211
+ `Enabling ${option.label} requires riskAccepted:true after the user explicitly accepts: ${option.warning}`
212
+ );
213
+ }
214
+ return { developer: await rt.setDeveloperOption(option.id, args.enabled) };
215
+ },
191
216
  async set_webhook_config(rt, args) {
192
217
  await rt.setWebhookConfig(args.webhook);
193
218
  const status = await rt.getChannelSetup();
@@ -31,10 +31,26 @@ export const SETUP_EXTENDED_ACTION_FIELDS = Object.freeze({
31
31
  save_project: 'project',
32
32
  remove_project: 'projectPath',
33
33
  revoke_linked_device: 'name',
34
+ set_activity_rail_pins: 'activityRailPins',
35
+ set_developer_option: 'name enabled riskAccepted?',
34
36
  });
35
37
 
38
+ /** Mirrors ACTIVITY_RAIL_PIN_IDS in apps/desktop/src/shared/activity-rail-pins.ts. */
39
+ export const SETUP_ACTIVITY_RAIL_PIN_IDS = Object.freeze([
40
+ 'sessions',
41
+ 'agents',
42
+ 'schedules',
43
+ 'workflows',
44
+ 'projects',
45
+ 'extensions',
46
+ 'source-control',
47
+ 'search',
48
+ 'webhooks',
49
+ ]);
50
+
36
51
  export const SETUP_EXTENDED_DOMAINS = Object.freeze([
37
52
  'capabilities',
53
+ 'developer',
38
54
  'desktop',
39
55
  'appearance',
40
56
  'projects',
@@ -49,6 +65,7 @@ export const SETUP_DESKTOP_ACTIONS = Object.freeze([
49
65
  'save_project',
50
66
  'remove_project',
51
67
  'revoke_linked_device',
68
+ 'set_activity_rail_pins',
52
69
  ]);
53
70
  export const SETUP_DESKTOP_DOMAINS = Object.freeze(['desktop', 'appearance', 'projects', 'connection']);
54
71
 
@@ -128,7 +145,6 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
128
145
  timezone: text,
129
146
  days: { ...text, description: 'Schedule day selector, e.g. weekdays or mon,wed,fri.' },
130
147
  parser: { type: 'string', enum: ['github', 'generic', 'stripe', 'sentry'] },
131
- channel: text,
132
148
  model: {
133
149
  ...text,
134
150
  description:
@@ -153,7 +169,6 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
153
169
  ['kind', 'data']
154
170
  ),
155
171
  },
156
- delivery: { type: 'string', enum: ['app', 'channel', 'both'] },
157
172
  enabled: boolean,
158
173
  overwrite: { ...boolean, description: 'Explicitly update an existing entry; omitted fields are preserved.' },
159
174
  },
@@ -178,10 +193,19 @@ export const SETUP_EXTENDED_PROPERTIES = Object.freeze({
178
193
  description: 'Desktop UI language id from status appearance; independent of profile language.',
179
194
  },
180
195
  sidePanels: { type: 'string', enum: ['close-left', 'close-right', 'close-both', 'keep-open'] },
181
- zoom: { type: 'number', minimum: 0.5, maximum: 2 },
182
196
  }),
183
197
  project: object({ path: text, alias: text }, ['path']),
184
198
  projectPath: { ...text, description: 'Exact registered Project root.' },
199
+ activityRailPins: {
200
+ type: 'array',
201
+ items: { type: 'string', enum: [...SETUP_ACTIVITY_RAIL_PIN_IDS] },
202
+ description: 'set_activity_rail_pins: complete ordered list of pinned activity rail items; it replaces the current list.',
203
+ },
204
+ riskAccepted: {
205
+ ...boolean,
206
+ description:
207
+ "set_developer_option: the user's explicit acceptance of the option warning; required to enable an option that has one.",
208
+ },
185
209
  });
186
210
 
187
211
  export const SETUP_HANDOFFS = Object.freeze({
@@ -32,8 +32,13 @@ export const SETUP_STATUS_DOMAINS = Object.freeze([
32
32
  ...SETUP_EXTENDED_DOMAINS,
33
33
  ]);
34
34
 
35
+ /** Settings sections without a slash command. Each UI routes them with its
36
+ * own setup-open table (Desktop app-shell-ui-open-request.ts, TUI
37
+ * use-ui-open-request.mjs). */
38
+ export const SETUP_SECTION_OPEN_TARGETS = Object.freeze(['developer', 'voice', 'connection']);
39
+
35
40
  /** Slash-command names shared by TUI (slash-commands.mjs) and Desktop
36
- * (slash-commands.ts); each UI routes the name with its own table. */
41
+ * (slash-commands.ts), then the settings sections above. */
37
42
  export const SETUP_OPEN_TARGETS = Object.freeze([
38
43
  'settings',
39
44
  'providers',
@@ -53,6 +58,7 @@ export const SETUP_OPEN_TARGETS = Object.freeze([
53
58
  'usage',
54
59
  'doctor',
55
60
  'context',
61
+ ...SETUP_SECTION_OPEN_TARGETS,
56
62
  ]);
57
63
 
58
64
  // Runtime dispatch contract. A trailing ? denotes an optional field. Keep
@@ -86,7 +92,8 @@ export const SETUP_ACTION_FIELDS = Object.freeze({
86
92
  delete_local_model: 'confirmationToken',
87
93
  set_system_shell: 'command',
88
94
  set_auto_update: 'enabled',
89
- forget_provider_auth: 'name',
95
+ forget_provider_auth: 'name accountId?',
96
+ set_provider_account: 'name providerAccount',
90
97
  add_mcp_server: 'server',
91
98
  save_mcp_server: 'server',
92
99
  remove_mcp_server: 'name',
@@ -98,6 +105,7 @@ export const SETUP_ACTION_FIELDS = Object.freeze({
98
105
  update_plugin: 'name',
99
106
  set_plugin_enabled: 'name enabled',
100
107
  remove_plugin: 'name',
108
+ enable_plugin_mcp: 'name',
101
109
  ...SETUP_EXTENDED_ACTION_FIELDS,
102
110
  });
103
111
 
@@ -160,7 +168,7 @@ export const SETUP_TOOL_DEFS = Object.freeze([
160
168
  target: {
161
169
  type: 'string',
162
170
  enum: [...SETUP_OPEN_TARGETS],
163
- description: 'open: settings surface, named by its slash command.',
171
+ description: 'open: settings surface, named by its slash command or settings section.',
164
172
  },
165
173
  route: {
166
174
  ...ROUTE_SCHEMA,
@@ -174,7 +182,8 @@ export const SETUP_TOOL_DEFS = Object.freeze([
174
182
  type: 'object',
175
183
  additionalProperties: false,
176
184
  properties: { title: { type: 'string' }, language: { type: 'string' }, experienceLevel: { type: 'string' } },
177
- description: 'set_profile: only the given fields change.',
185
+ description:
186
+ 'set_profile: only the given fields change. language and experienceLevel take ids from status profile; experienceLevel "" clears it.',
178
187
  },
179
188
  autoclear: {
180
189
  type: 'object',
@@ -204,7 +213,28 @@ export const SETUP_TOOL_DEFS = Object.freeze([
204
213
  },
205
214
  name: {
206
215
  type: 'string',
207
- description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. set_first_use_approval: browser|computer; revoke_linked_device: client id from status connection, not its display name; else MCP/plugin/provider id.`,
216
+ description: `set_builtin_enabled: ${SETUP_BUILTIN_TOGGLE_FEATURES.join('|')}; install_builtin also accepts memory. Memory toggles use set_memory_enabled. set_first_use_approval: browser|computer; revoke_linked_device: client id from status connection, not its display name; set_developer_option: option id from status developer; else MCP/plugin/provider id.`,
217
+ },
218
+ accountId: {
219
+ type: 'string',
220
+ description:
221
+ 'forget_provider_auth: OAuth account id from status providers; required when the provider has several accounts.',
222
+ },
223
+ providerAccount: {
224
+ type: 'object',
225
+ additionalProperties: false,
226
+ properties: {
227
+ selectedId: { type: 'string', description: 'Account to use now.' },
228
+ auto: { type: 'boolean', description: 'Automatic switching to another account when one is exhausted.' },
229
+ order: { type: 'array', items: { type: 'string' }, description: 'Every connected account id exactly once.' },
230
+ rename: {
231
+ type: 'object',
232
+ additionalProperties: false,
233
+ required: ['id', 'label'],
234
+ properties: { id: { type: 'string' }, label: { type: 'string', maxLength: 80 } },
235
+ },
236
+ },
237
+ description: 'set_provider_account: OAuth account change; account ids from status providers.',
208
238
  },
209
239
  modelId: {
210
240
  type: 'string',
@@ -36,7 +36,6 @@ export async function runWebhookSession({
36
36
  cwd = null,
37
37
  workflow = null,
38
38
  attachments = null,
39
- delivery = null,
40
39
  signal = null,
41
40
  }) {
42
41
  const endpoint = String(name || '').trim();
@@ -53,7 +52,6 @@ export async function runWebhookSession({
53
52
  owner: 'user',
54
53
  sourceType: 'webhook',
55
54
  sourceName: endpoint,
56
- sourceDelivery: delivery || null,
57
55
  tools: WEBHOOK_SESSION_TOOLS,
58
56
  ...(projectCwd ? { cwd: projectCwd } : {}),
59
57
  desktopSession: projectCwd
@@ -5,7 +5,11 @@ import { hasOwn } from '../../runtime/shared/object.mjs';
5
5
  import { serializeFrontmatterDoc } from '../../runtime/shared/markdown-frontmatter.mjs';
6
6
  import { isHiddenAgent } from '../../runtime/agent/orchestrator/internal-agents.mjs';
7
7
  import { AGENT_DELETED_MARKER, FIXED_AGENT_SLOTS, availableAgentId, clearAgentDefinitionCache } from '../../runtime/agent/orchestrator/runtime-core/workflow.mjs';
8
- import { canonicalizeAgentRouteStorage, isAgentDisabled } from '../../runtime/shared/agent-route-config.mjs';
8
+ import {
9
+ canonicalizeAgentRouteStorage,
10
+ isAgentDisabled,
11
+ withAgentDisabled,
12
+ } from '../../runtime/shared/agent-route-config.mjs';
9
13
  import { agentEditorId, effectiveAgentRoute, oneLine, resolveDataDir } from './shared.mjs';
10
14
 
11
15
  const isFixedAgent = (id) => FIXED_AGENT_SLOTS.some((agent) => agent.id === id);
@@ -95,15 +99,21 @@ export function createAgentEditorApi(deps) {
95
99
  if (!body) throw new Error('AGENT.md body must not be empty');
96
100
  const dataDir = resolveDataDir(deps);
97
101
  const { id, name, description } = resolveSaveTarget(payload, dataDir);
102
+ const existed = Boolean(loadAgentDefinition(dataDir, id));
98
103
  writeAgentFiles(join(dataDir, 'agents', id), { name: name || id, description, body });
99
104
  clearAgentDefinitionCache(id);
105
+ // A newly created agent starts enabled; a stale roster entry left by an
106
+ // earlier agent of the same id must not disable it.
107
+ if (!existed && isAgentDisabled(getConfig(), id)) saveConfigAndAdopt(withAgentDisabled(getConfig(), id, false));
100
108
  if (payload.route) await setAgentRoute(id, payload.route);
101
109
  return getAgentDefinition(id);
102
110
  }
103
111
 
104
- // A removed custom agent must not leave a dangling route/preset.
112
+ // A removed custom agent must not leave a dangling route/preset or a
113
+ // disabled-roster entry that would silently disable a later agent of the
114
+ // same id.
105
115
  function dropAgentRoute(id) {
106
- const nextConfig = { ...getConfig() };
116
+ const nextConfig = withAgentDisabled(getConfig(), id, false);
107
117
  if (nextConfig.agents && id in nextConfig.agents) {
108
118
  const agents = { ...nextConfig.agents };
109
119
  delete agents[id];
@@ -29,6 +29,10 @@ export function createStoredSessionViews({ desktopRuntime, dataDir }) {
29
29
  const store = await desktopRuntime.loadSessionStore();
30
30
  store.forgetStoredSessionTranscript?.(sessionId);
31
31
  },
32
+ releaseStoredSession: async (sessionId) => {
33
+ const store = await desktopRuntime.loadSessionStore();
34
+ store.releaseStoredSessionTranscript?.(sessionId);
35
+ },
32
36
  readStoredGoal: async (sessionId) =>
33
37
  readStoredGoalSnapshot({
34
38
  dataDir,
@@ -138,7 +138,6 @@ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
138
138
  'setAgentRoute',
139
139
  'setAutoClear',
140
140
  'setAutoUpdate',
141
- 'setChannelsEnabled',
142
141
  'setCompactionSettings',
143
142
  'setCwd',
144
143
  'setDeveloperOption',
@@ -38,8 +38,13 @@ export function createSessionViewCalls(ctx) {
38
38
  bindExternalSessionView,
39
39
  destroy,
40
40
  } = ctx.entries;
41
- const { storedSessionProjection, storedProjectionUnchanged, requestedMessageSlice, forgetStoredSession } =
42
- ctx.storedReader;
41
+ const {
42
+ storedSessionProjection,
43
+ storedProjectionUnchanged,
44
+ requestedMessageSlice,
45
+ forgetStoredSession,
46
+ releaseStoredSession,
47
+ } = ctx.storedReader;
43
48
 
44
49
  /** A read/subscribe names the transcript window its view wants (none: the
45
50
  * whole transcript). A window grown past what a resumed runtime holds is
@@ -229,8 +234,9 @@ export function createSessionViewCalls(ctx) {
229
234
  if (token) external.subscribers?.delete(token);
230
235
  if ((external.subscribers?.size || 0) === 0) externalViewEntries.delete(id);
231
236
  }
232
- // The last cold view left: its disk projection is no longer needed.
233
- if (!coldViewers) forgetStoredSession(id);
237
+ // The last cold view left: its disk projection turns idle, kept within
238
+ // the store's idle budget so a revisit skips the parse.
239
+ if (!coldViewers) releaseStoredSession(id);
234
240
  return { sessionId: id, unsubscribed: true };
235
241
  }
236
242
  const entry = owner;