cli-jaw 2.17.72 → 2.17.73

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 (169) hide show
  1. package/README.md +16 -0
  2. package/dist/src/agent/aside-catalog.js +339 -0
  3. package/dist/src/agent/aside-catalog.js.map +1 -0
  4. package/dist/src/agent/aside-cli.js +208 -0
  5. package/dist/src/agent/aside-cli.js.map +1 -0
  6. package/dist/src/agent/aside-replay.js +120 -0
  7. package/dist/src/agent/aside-replay.js.map +1 -0
  8. package/dist/src/agent/aside-runtime.js +175 -0
  9. package/dist/src/agent/aside-runtime.js.map +1 -0
  10. package/dist/src/agent/lifecycle-handler.js +26 -18
  11. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  12. package/dist/src/agent/memory-flush-controller.js +3 -0
  13. package/dist/src/agent/memory-flush-controller.js.map +1 -1
  14. package/dist/src/agent/prompt-context.js +2 -2
  15. package/dist/src/agent/prompt-context.js.map +1 -1
  16. package/dist/src/agent/session-persistence.js +2 -0
  17. package/dist/src/agent/session-persistence.js.map +1 -1
  18. package/dist/src/agent/spawn/backend-aside.js +314 -0
  19. package/dist/src/agent/spawn/backend-aside.js.map +1 -0
  20. package/dist/src/agent/spawn/resume.js +3 -0
  21. package/dist/src/agent/spawn/resume.js.map +1 -1
  22. package/dist/src/agent/spawn-env.js +5 -0
  23. package/dist/src/agent/spawn-env.js.map +1 -1
  24. package/dist/src/agent/spawn.js +60 -6
  25. package/dist/src/agent/spawn.js.map +1 -1
  26. package/dist/src/cli/cli-status-worker.js +2 -1
  27. package/dist/src/cli/cli-status-worker.js.map +1 -1
  28. package/dist/src/cli/employee-handler.js +2 -0
  29. package/dist/src/cli/employee-handler.js.map +1 -1
  30. package/dist/src/cli/handlers-completions.js +20 -8
  31. package/dist/src/cli/handlers-completions.js.map +1 -1
  32. package/dist/src/cli/handlers-runtime.js +2 -0
  33. package/dist/src/cli/handlers-runtime.js.map +1 -1
  34. package/dist/src/cli/readiness.js +7 -3
  35. package/dist/src/cli/readiness.js.map +1 -1
  36. package/dist/src/cli/registry-live.js +47 -0
  37. package/dist/src/cli/registry-live.js.map +1 -1
  38. package/dist/src/cli/registry.js +5 -0
  39. package/dist/src/cli/registry.js.map +1 -1
  40. package/dist/src/core/config.js +10 -3
  41. package/dist/src/core/config.js.map +1 -1
  42. package/dist/src/core/employees.js +5 -3
  43. package/dist/src/core/employees.js.map +1 -1
  44. package/dist/src/core/settings-merge.js +35 -0
  45. package/dist/src/core/settings-merge.js.map +1 -1
  46. package/dist/src/core/settings-watch.js +10 -1
  47. package/dist/src/core/settings-watch.js.map +1 -1
  48. package/dist/src/orchestrator/gateway.js +2 -0
  49. package/dist/src/orchestrator/gateway.js.map +1 -1
  50. package/dist/src/routes/employees.js +8 -0
  51. package/dist/src/routes/employees.js.map +1 -1
  52. package/dist/src/routes/jaw-memory.js +4 -0
  53. package/dist/src/routes/jaw-memory.js.map +1 -1
  54. package/dist/src/routes/orchestrate.js +22 -3
  55. package/dist/src/routes/orchestrate.js.map +1 -1
  56. package/dist/src/routes/settings.js +23 -0
  57. package/dist/src/routes/settings.js.map +1 -1
  58. package/dist/src/shared/aside-contract.js +2 -0
  59. package/dist/src/shared/aside-contract.js.map +1 -0
  60. package/dist/src/types/cli-engine.js +1 -0
  61. package/dist/src/types/cli-engine.js.map +1 -1
  62. package/package.json +1 -1
  63. package/public/dist/.vite/manifest.json +186 -175
  64. package/public/dist/assets/{Activity-DPLlOJ46.js → Activity-DsBgOkCP.js} +1 -1
  65. package/public/dist/assets/{AdvancedExport-BgOgGvc5.js → AdvancedExport-COGQ7vbK.js} +1 -1
  66. package/public/dist/assets/Agent-6qGQN7lY.js +1 -0
  67. package/public/dist/assets/AsideSelectionFields-DvU5xWAP.js +1 -0
  68. package/public/dist/assets/{Browser-m6VynQXV.js → Browser-r09vOsR1.js} +1 -1
  69. package/public/dist/assets/ChannelEnablementControl-ooOkdd-Q.js +85 -0
  70. package/public/dist/assets/{ChannelSetupEntry-DHLoZEzJ.js → ChannelSetupEntry-B0b4TdhH.js} +1 -1
  71. package/public/dist/assets/{ChannelsDiscord-B-G8vsYy.js → ChannelsDiscord-9EMLt5b3.js} +1 -1
  72. package/public/dist/assets/{ChannelsSlack-oPuh922z.js → ChannelsSlack-B8ETzWFt.js} +1 -1
  73. package/public/dist/assets/{ChannelsTelegram-PmTv43KQ.js → ChannelsTelegram-CflLUsQw.js} +1 -1
  74. package/public/dist/assets/{DashboardMeta-BqEiJrvr.js → DashboardMeta-wjEuh9-E.js} +1 -1
  75. package/public/dist/assets/{Developer-CbySB7_Z.js → Developer-B3cDkldo.js} +1 -1
  76. package/public/dist/assets/{Display-DU4_76cS.js → Display-BXQxpv6-.js} +1 -1
  77. package/public/dist/assets/{Display-C0Zdamcb.js → Display-Cl4DD334.js} +1 -1
  78. package/public/dist/assets/{Embedding-CCFMOB_2.js → Embedding-D-T92-En.js} +1 -1
  79. package/public/dist/assets/{Employees-BanvF58d.js → Employees-Cf4IeF0N.js} +1 -1
  80. package/public/dist/assets/{Heartbeat-uyi_MpUd.js → Heartbeat-Bj8V_bUM.js} +1 -1
  81. package/public/dist/assets/{Mcp-rjPoL_Zo.js → Mcp-jAcut_fw.js} +1 -1
  82. package/public/dist/assets/Memory-BOXkBFKg.js +1 -0
  83. package/public/dist/assets/ModelProvider-DIDQ37z-.js +1 -0
  84. package/public/dist/assets/{Network-CuqFrOvR.js → Network-I5eOe2hf.js} +1 -1
  85. package/public/dist/assets/{Permissions-BltXiZnz.js → Permissions-B7PthH01.js} +1 -1
  86. package/public/dist/assets/{Permissions-DcCbFMIq.js → Permissions-BCYUfJg1.js} +1 -1
  87. package/public/dist/assets/Profile-CYPMVhQM.js +1 -0
  88. package/public/dist/assets/{Prompts-C9vY8ivQ.js → Prompts-BRAgZLNQ.js} +1 -1
  89. package/public/dist/assets/{SpeechKeys-BJ8V7U5E.js → SpeechKeys-UlXNSFoe.js} +1 -1
  90. package/public/dist/assets/{TelegramHub-DtyjLGFR.js → TelegramHub-BhUFhIcv.js} +1 -1
  91. package/public/dist/assets/agent-meta-v4jNRAbX.js +1 -0
  92. package/public/dist/assets/app-SVIyP1TT.js +59 -0
  93. package/public/dist/assets/bgtask-badge-HIrvpcr_.js +1 -0
  94. package/public/dist/assets/{bgtask-badge-oPvZyylH.js → bgtask-badge-tYRnplEV.js} +1 -1
  95. package/public/dist/assets/constants-_iw-lnKY.js +1 -0
  96. package/public/dist/assets/{employees-BchFBkDz.js → employees-DNTPcxtZ.js} +2 -2
  97. package/public/dist/assets/{error-normalize-clOi-AAA.js → error-normalize-jnjrKZDu.js} +1 -1
  98. package/public/dist/assets/{iframe-renderer-CcGzEi0v.js → iframe-renderer--lHbKTDh.js} +2 -2
  99. package/public/dist/assets/iframe-renderer-DigNC5FU.js +1 -0
  100. package/public/dist/assets/{manager-nquYZ7_x.js → manager-qGH-xUZ7.js} +1 -1
  101. package/public/dist/assets/{memory-DTpD4see.js → memory-BAdRkBdk.js} +1 -1
  102. package/public/dist/assets/memory-DA8x5314.js +1 -0
  103. package/public/dist/assets/message-history-CGOgZH4l.js +1 -0
  104. package/public/dist/assets/{message-history-DSvflaZ9.js → message-history-PAYeKmia.js} +1 -1
  105. package/public/dist/assets/page-shell-Cz2ylri8.js +1 -0
  106. package/public/dist/assets/{render-D8dVQ_bm.js → render-F6xF6w6c.js} +1 -1
  107. package/public/dist/assets/settings-DMbNzuTm.js +1 -0
  108. package/public/dist/assets/{settings-DMiQe469.js → settings-DZ2-ca8T.js} +1 -1
  109. package/public/dist/assets/{settings-Bp0sgQ2a.js → settings-W0WFhBf2.js} +1 -1
  110. package/public/dist/assets/settings-core-2vZXNf6n.js +2 -0
  111. package/public/dist/assets/settings-core-BofMyzL9.js +1 -0
  112. package/public/dist/assets/{settings-embedding-BRW0WZvh.js → settings-embedding-Bjwpe8xu.js} +2 -2
  113. package/public/dist/assets/{shared-D1begqvi.js → shared-CftvKNY9.js} +1 -1
  114. package/public/dist/assets/{sidebar-CrpBbYFg.js → sidebar-Clkqd78q.js} +3 -3
  115. package/public/dist/assets/{skills-Bx8U-gJa.js → skills-0aA-RuN9.js} +1 -1
  116. package/public/dist/assets/skills-SqmQXndw.js +1 -0
  117. package/public/dist/assets/{slash-commands-DkrGUOnC.js → slash-commands-4zHHXEeU.js} +1 -1
  118. package/public/dist/assets/slash-commands-D5LqONzL.js +1 -0
  119. package/public/dist/assets/{trace-drawer-Dbe5pTbf.js → trace-drawer-B4O0Tl1M.js} +1 -1
  120. package/public/dist/assets/{ui-rnVAM91F.js → ui-CVtGpivr.js} +1 -1
  121. package/public/dist/assets/ui-_cqgseFX.js +1 -0
  122. package/public/dist/index.html +1 -1
  123. package/public/dist/manager/index.html +1 -1
  124. package/public/dist/settings/index.html +1 -1
  125. package/public/js/constants.ts +5 -0
  126. package/public/js/features/employees.ts +3 -3
  127. package/public/js/features/heartbeat.ts +14 -1
  128. package/public/js/features/settings-core.ts +39 -13
  129. package/public/js/features/settings-types.ts +1 -1
  130. package/public/locales/en.json +2 -0
  131. package/public/locales/ja.json +2 -0
  132. package/public/locales/ko.json +2 -0
  133. package/public/locales/zh.json +2 -0
  134. package/public/manager/src/settings/SettingsShell.tsx +1 -1
  135. package/public/manager/src/settings/pages/Agent.tsx +137 -52
  136. package/public/manager/src/settings/pages/Heartbeat.tsx +6 -0
  137. package/public/manager/src/settings/pages/Memory.tsx +8 -6
  138. package/public/manager/src/settings/pages/ModelProvider.tsx +31 -6
  139. package/public/manager/src/settings/pages/Profile.tsx +2 -2
  140. package/public/manager/src/settings/pages/components/AsideSelectionFields.tsx +33 -0
  141. package/public/manager/src/settings/pages/components/AvatarCard.tsx +22 -34
  142. package/public/manager/src/settings/pages/components/PerCliRow.tsx +29 -1
  143. package/public/manager/src/settings/pages/components/agent/FlushAgentSection.tsx +7 -6
  144. package/public/manager/src/settings/pages/components/agent/RuntimeEmployeeRow.tsx +4 -4
  145. package/public/manager/src/settings/pages/components/agent/RuntimeHeader.tsx +5 -0
  146. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +7 -0
  147. package/public/manager/src/settings/pages/components/agent/runtime-employees-helpers.ts +3 -2
  148. package/public/manager/src/settings/pages/components/aside-models.ts +104 -0
  149. package/public/manager/src/settings/settings-client.ts +3 -1
  150. package/public/manager/src/settings/types.ts +1 -0
  151. package/public/dist/assets/Agent-O_U8ny90.js +0 -1
  152. package/public/dist/assets/ChannelEnablementControl-BSuKQhTW.js +0 -85
  153. package/public/dist/assets/Memory-C9I33pIi.js +0 -1
  154. package/public/dist/assets/ModelProvider-fiUktMaO.js +0 -1
  155. package/public/dist/assets/Profile-DUL7-RE_.js +0 -1
  156. package/public/dist/assets/agent-meta-94LMwZZK.js +0 -1
  157. package/public/dist/assets/app-Iu76iEiU.js +0 -59
  158. package/public/dist/assets/bgtask-badge-DHo2mkMI.js +0 -1
  159. package/public/dist/assets/constants-DmZ9Rjym.js +0 -1
  160. package/public/dist/assets/iframe-renderer-DSLsfZq8.js +0 -1
  161. package/public/dist/assets/memory-BgUF3M9l.js +0 -1
  162. package/public/dist/assets/message-history-DkB768L3.js +0 -1
  163. package/public/dist/assets/page-shell-DfinU3mb.js +0 -1
  164. package/public/dist/assets/settings-DxjoyiIM.js +0 -1
  165. package/public/dist/assets/settings-core-CXWmPKUE.js +0 -1
  166. package/public/dist/assets/settings-core-D4zGy72z.js +0 -2
  167. package/public/dist/assets/skills-BY-hQnqM.js +0 -1
  168. package/public/dist/assets/slash-commands-C53T9J3g.js +0 -1
  169. package/public/dist/assets/ui-C0o427q6.js +0 -1
@@ -77,7 +77,7 @@ export function renderEmployees(): void {
77
77
  el.innerHTML = `<div style="color:var(--text-dim);font-size:11px;padding:4px 0">${t('emp.addPrompt')}</div>`;
78
78
  return;
79
79
  }
80
- const cliKeys = getCliKeys();
80
+ const cliKeys = getCliKeys().filter(cli => cli !== 'aside');
81
81
  el.innerHTML = employees.map(a => {
82
82
  const isStatic = a.source === 'static';
83
83
  const models = MODEL_MAP[a.cli] || [];
@@ -115,7 +115,7 @@ export function renderEmployees(): void {
115
115
  // the Model column.
116
116
  const cliField = isStatic
117
117
  ? `<select disabled title="CLI fixed for built-in employee"><option>${escapeHtml(a.cli)}</option></select>`
118
- : `<select data-emp-cli="${escapeHtml(a.id)}">${cliKeys.map(c => `<option${a.cli === c ? ' selected' : ''}>${escapeHtml(c)}</option>`).join('')}</select>`;
118
+ : `<select data-emp-cli="${escapeHtml(a.id)}">${a.cli === 'aside' ? '<option value="aside" selected disabled>Aside (main-only)</option>' : ''}${cliKeys.map(c => `<option${a.cli === c ? ' selected' : ''}>${escapeHtml(c)}</option>`).join('')}</select>`;
119
119
  const roleField = isStatic
120
120
  ? `<div style="font-size:11px;color:var(--text-dim);padding:4px 0">${escapeHtml(a.role || '')}</div>`
121
121
  : `<select data-emp-role="${escapeHtml(a.id)}">${ROLE_PRESETS.map(r => `<option value="${r.value}"${presetVal === r.value ? ' selected' : ''}>${r.label}</option>`).join('')}</select>
@@ -136,7 +136,7 @@ export function renderEmployees(): void {
136
136
  </div>
137
137
  <div>
138
138
  <label>Model</label>
139
- <select data-emp-model="${escapeHtml(a.id)}">
139
+ <select data-emp-model="${escapeHtml(a.id)}"${a.cli === 'aside' ? ' disabled title="Aside supports the main agent only"' : ''}>
140
140
  <option value="default"${selectedModel === 'default' ? ' selected' : ''}>default</option>
141
141
  ${models.map(m => `<option${selectedModel === m ? ' selected' : ''}>${escapeHtml(m)}</option>`).join('')}
142
142
  ${selectedModel !== 'default' && !models.includes(selectedModel) ? `<option selected>${escapeHtml(selectedModel)}</option>` : ''}
@@ -14,8 +14,14 @@ interface HeartbeatData {
14
14
  jobs: HeartbeatJob[];
15
15
  }
16
16
 
17
+ let mainCli: string | undefined;
18
+
17
19
  export async function openHeartbeatModal(): Promise<void> {
18
- const data = await api<HeartbeatData>('/api/heartbeat');
20
+ const [data, settings] = await Promise.all([
21
+ api<HeartbeatData>('/api/heartbeat'),
22
+ api<{ cli?: string }>('/api/settings'),
23
+ ]);
24
+ mainCli = settings?.cli;
19
25
  state.heartbeatJobs = (data?.jobs || []).map(normalizeHeartbeatJob);
20
26
  state.heartbeatErrors = collectHeartbeatErrors(state.heartbeatJobs as HeartbeatJob[]);
21
27
  renderHeartbeatJobs();
@@ -75,6 +81,13 @@ export function renderHeartbeatJobs(): void {
75
81
  `;
76
82
  }).join('');
77
83
  }
84
+ if (mainCli === 'aside') {
85
+ const notice = document.createElement('p');
86
+ notice.className = 'hb-schedule-meta hb-help';
87
+ notice.setAttribute('role', 'status');
88
+ notice.textContent = t('hb.asideUnsupported');
89
+ container.prepend(notice);
90
+ }
78
91
  const active = jobs.filter(j => j.enabled).length;
79
92
  const btn = document.getElementById('hbSidebarBtn');
80
93
  if (btn) btn.innerHTML = `${ICONS.heartPulse} Heartbeat (${active})`;
@@ -225,8 +225,9 @@ function syncCliOptionSelects(settings: SettingsData | null = null): void {
225
225
  const flushCli = document.getElementById('flushCli') as HTMLSelectElement | null;
226
226
  if (flushCli) {
227
227
  const current = settings?.memory?.cli || flushCli.value || '';
228
- flushCli.innerHTML = '<option value="">(active CLI)</option>' +
229
- cliKeys.map(cli => `<option value="${escapeHtml(cli)}">${escapeHtml(cliDisplayLabel(cli))}</option>`).join('');
228
+ flushCli.innerHTML = (settings?.cli === 'aside' || selCli?.value === 'aside' ? '' : '<option value="">(active CLI)</option>') +
229
+ cliKeys.filter(cli => cli !== 'aside').map(cli => `<option value="${escapeHtml(cli)}">${escapeHtml(cliDisplayLabel(cli))}</option>`).join('');
230
+ if (current === 'aside') { appendCustomOption(flushCli, current); const option = Array.from(flushCli.options).find(o => o.value === current); if (option) { option.disabled = true; option.textContent = 'Aside (main-only)'; } }
230
231
  preserveRetiredRuntimeOption(flushCli, current);
231
232
  if (Array.from(flushCli.options).some(o => o.value === current)) flushCli.value = current;
232
233
  }
@@ -290,7 +291,16 @@ function syncActiveEffortOptions(cli: string, selected = '', model?: string): vo
290
291
  cli,
291
292
  model ?? (document.getElementById('selModel') as HTMLSelectElement | null)?.value ?? '',
292
293
  );
293
- const effortsList = resolveEffortChoices(meta, activeModel, providerEfforts, cliProvider || undefined);
294
+ const effortsList = cli === 'aside' ? meta?.effortsByModel?.[activeModel || 'default'] || [] : resolveEffortChoices(meta, activeModel, providerEfforts, cliProvider || undefined);
295
+ if (cli === 'aside') {
296
+ const valid = !selected || selected === 'default' || effortsList.includes(selected);
297
+ selEffort.innerHTML = '<option value="">(default)</option>' + effortsList.map(e => `<option value="${escapeHtml(e)}">${escapeHtml(e)}</option>`).join('');
298
+ if (!valid) { appendCustomOption(selEffort, selected); const option = Array.from(selEffort.options).find(o => o.value === selected); if (option) { option.disabled = true; option.textContent = `${selected} (unavailable)`; } }
299
+ selEffort.value = selected === 'default' ? '' : selected;
300
+ selEffort.disabled = effortsList.length === 0 && valid;
301
+ selEffort.title = valid ? 'Aside effort belongs to the selected concrete model.' : 'Saved Aside effort is unavailable.';
302
+ return;
303
+ }
294
304
  if (effortsList.length === 0) {
295
305
  const note = meta?.effortNote || '— none';
296
306
  selEffort.innerHTML = `<option value="">${escapeHtml(note)}</option>`;
@@ -388,12 +398,13 @@ export async function loadSettings(): Promise<void> {
388
398
  const ao = s.activeOverrides?.[s.cli] || {};
389
399
  const pc = s.perCli?.[s.cli] || {};
390
400
  const activeModel = ao.model || pc.model;
391
- const activeEffort = ao.effort ?? pc.effort ?? '';
401
+ const activeEffort = s.cli === 'aside' ? ao.effort || pc.effort || '' : ao.effort ?? pc.effort ?? '';
392
402
  const selModel = document.getElementById('selModel') as HTMLSelectElement | null;
393
403
  if (activeModel && selModel) {
394
404
  const displayModel = normalizeModelForDisplay(s.cli, activeModel);
395
405
  if (displayModel && !Array.from(selModel.options).some(o => o.value === displayModel)) {
396
406
  appendCustomOption(selModel, displayModel);
407
+ if (s.cli === 'aside') { const option = Array.from(selModel.options).find(o => o.value === displayModel); if (option) { option.disabled = true; option.textContent = `${displayModel} (unavailable)`; } }
397
408
  }
398
409
  selModel.value = displayModel;
399
410
  }
@@ -506,7 +517,13 @@ export function onCliChange(save = true): void {
506
517
  syncActiveEffortOptions(cli);
507
518
  return;
508
519
  }
509
- if (meta?.modelNote && modelSel) {
520
+ if (cli === 'aside' && modelSel) {
521
+ setSelectOptions(modelSel, models);
522
+ const defaultOption = Array.from(modelSel.options).find(o => o.value === 'default');
523
+ if (defaultOption && meta?.observedDefaultModel) defaultOption.textContent = `Default (${meta.observedDefaultModel})`;
524
+ modelSel.disabled = models.length === 0;
525
+ modelSel.title = t('cli.asideHelp');
526
+ } else if (meta?.modelNote && modelSel) {
510
527
  modelSel.innerHTML = `<option value="">${escapeHtml(meta.modelNote)}</option>`;
511
528
  modelSel.title = meta.modelNote;
512
529
  modelSel.disabled = true;
@@ -537,7 +554,7 @@ export function onCliChange(save = true): void {
537
554
  saveActiveCliSettings();
538
555
  };
539
556
  if (!modelSel) { if (save) updateSettings(); return; }
540
- modelSel.parentElement?.appendChild(inp);
557
+ if (cli !== 'aside') modelSel.parentElement?.appendChild(inp);
541
558
  modelSel.onchange = function () {
542
559
  if ((this as HTMLSelectElement).value === '__custom__') {
543
560
  inp.style.display = 'block';
@@ -562,13 +579,13 @@ export function onCliChange(save = true): void {
562
579
  || selCli.value !== cli
563
580
  || document.getElementById('selCliProvider') !== providerSel
564
581
  || document.getElementById('selModel') !== modelSel
565
- || document.getElementById('selModelCustom') !== inp
582
+ || (cli !== 'aside' && document.getElementById('selModelCustom') !== inp)
566
583
  || document.getElementById('selEffort') !== effortSel
567
584
  || ((meta?.providers?.length ?? 0) > 0 && getSelectedCliProvider(cli) !== cliProvider)) return;
568
585
  const ao = s.activeOverrides?.[cli] || {};
569
586
  const pc = s.perCli?.[cli] || {};
570
587
  const model = ao.model || pc.model;
571
- const effort = ao.effort ?? pc.effort ?? '';
588
+ const effort = cli === 'aside' ? ao.effort || pc.effort || '' : ao.effort ?? pc.effort ?? '';
572
589
  if (cli !== 'pi' && meta?.providers?.length) {
573
590
  const savedProvider = pc.provider || meta.defaultProvider || '';
574
591
  if (savedProvider !== cliProvider) return;
@@ -576,6 +593,7 @@ export function onCliChange(save = true): void {
576
593
  if (model && modelSel) {
577
594
  const displayModel = normalizeModelForDisplay(cli, model);
578
595
  appendCustomOption(modelSel, displayModel);
596
+ if (cli === 'aside' && !models.includes(displayModel)) { const option = Array.from(modelSel.options).find(o => o.value === displayModel); if (option) { option.disabled = true; option.textContent = `${displayModel} (unavailable)`; } }
579
597
  modelSel.value = displayModel;
580
598
  }
581
599
  syncActiveEffortOptions(cli, effort);
@@ -595,7 +613,12 @@ export async function saveActiveCliSettings(): Promise<void> {
595
613
  const effortEl = document.getElementById('selEffort') as HTMLSelectElement | null;
596
614
  const overrides: Record<string, PerCliConfig> = {};
597
615
  overrides[cli] = { model };
598
- if (effortEl && !effortEl.disabled) overrides[cli].effort = effortEl.value || '';
616
+ if (effortEl && (!effortEl.disabled || cli === 'aside')) overrides[cli].effort = effortEl.value || (cli === 'aside' ? 'default' : '');
617
+ if (cli === 'aside') {
618
+ const meta = getCliMeta(cli);
619
+ const effort = effortEl?.value || '';
620
+ if (!meta?.models.includes(model) || (effort && !meta.effortsByModel?.[model]?.includes(effort))) return;
621
+ }
599
622
  const patch: Record<string, unknown> = { activeOverrides: overrides };
600
623
  const patchMeta = getCliMeta(cli);
601
624
  if (cli !== 'pi' && patchMeta?.providers?.length) patch['perCli'] = { [cli]: { provider: getSelectedCliProvider(cli) } };
@@ -609,9 +632,10 @@ export async function saveActiveCliSettings(): Promise<void> {
609
632
  export function onFlushCliChange(): void {
610
633
  const flushCli = (document.getElementById('flushCli') as HTMLSelectElement)?.value || '';
611
634
  const effectiveCli = flushCli || (document.getElementById('selCli') as HTMLSelectElement)?.value || 'claude';
612
- const models = MODEL_MAP[effectiveCli] || [];
635
+ const models = effectiveCli === 'aside' ? [] : MODEL_MAP[effectiveCli] || [];
613
636
  const flushModelSel = document.getElementById('flushModel') as HTMLSelectElement | null;
614
- setSelectOptions(flushModelSel, models, { includeDefault: true });
637
+ setSelectOptions(flushModelSel, models, { includeDefault: effectiveCli !== 'aside' });
638
+ if (flushModelSel) { flushModelSel.disabled = effectiveCli === 'aside'; flushModelSel.title = effectiveCli === 'aside' ? 'Aside cannot run memory flush.' : ''; }
615
639
  updateFlushBadge();
616
640
  saveFlushAgentSettings();
617
641
  }
@@ -624,8 +648,9 @@ export async function loadFlushAgentSidebar(): Promise<void> {
624
648
  if (flushCliSel && data.cli) flushCliSel.value = data.cli;
625
649
 
626
650
  const effectiveCli = data.cli || (document.getElementById('selCli') as HTMLSelectElement)?.value || 'claude';
627
- const models = MODEL_MAP[effectiveCli] || [];
628
- setSelectOptions(flushModelSel, models, { includeDefault: true });
651
+ const models = effectiveCli === 'aside' ? [] : MODEL_MAP[effectiveCli] || [];
652
+ setSelectOptions(flushModelSel, models, { includeDefault: effectiveCli !== 'aside' });
653
+ if (flushModelSel) { flushModelSel.disabled = effectiveCli === 'aside'; flushModelSel.title = effectiveCli === 'aside' ? 'Aside cannot run memory flush.' : ''; }
629
654
  if (flushModelSel && data.model) {
630
655
  appendCustomOption(flushModelSel, data.model);
631
656
  flushModelSel.value = data.model;
@@ -636,6 +661,7 @@ export async function loadFlushAgentSidebar(): Promise<void> {
636
661
  async function saveFlushAgentSettings(): Promise<void> {
637
662
  const cli = (document.getElementById('flushCli') as HTMLSelectElement)?.value || '';
638
663
  const model = (document.getElementById('flushModel') as HTMLSelectElement)?.value || '';
664
+ if (cli === 'aside' || (!cli && (document.getElementById('selCli') as HTMLSelectElement)?.value === 'aside')) return;
639
665
  await apiJson('/api/memory-files/settings', 'PUT', { cli, model });
640
666
  }
641
667
 
@@ -3,7 +3,7 @@
3
3
  import type { RuntimeTransport } from '../../../src/shared/runtime-contract.js';
4
4
  import type { PresentationMode } from '../../../src/shared/presentation.js';
5
5
 
6
- export interface PerCliConfig { provider?: string; model?: string; effort?: string; transport?: RuntimeTransport; fastMode?: boolean; }
6
+ export interface PerCliConfig { account?: string; host?: string; provider?: string; model?: string; effort?: string; transport?: RuntimeTransport; fastMode?: boolean; }
7
7
  export interface TelegramConfig { enabled?: boolean; token?: string; allowedChatIds?: number[]; forwardAll?: boolean; mentionOnly?: boolean; }
8
8
  export interface DiscordConfig { enabled?: boolean; token?: string; guildId?: string; channelIds?: string[]; forwardAll?: boolean; allowBots?: boolean; mentionOnly?: boolean; }
9
9
  // Slack needs TWO distinctly-scoped tokens: the bot token drives the Web API,
@@ -273,6 +273,8 @@
273
273
  "role.label.security": "Security",
274
274
  "role.label.testing": "Testing",
275
275
  "role.label.custom": "Custom...",
276
+ "cli.asideHelp": "Experimental · local main agent only. Scheduled heartbeat runs are unsupported with Aside as the main runtime and will be rejected. Configure the explicit account in Settings / Agent.",
277
+ "hb.asideUnsupported": "Scheduled heartbeat runs are unsupported with Aside as the main runtime and will be rejected. Choose another main runtime in Agent settings to use heartbeat.",
276
278
  "hb.empty": "No heartbeats",
277
279
  "hb.name": "Name",
278
280
  "hb.prompt": "Prompt...",
@@ -271,6 +271,8 @@
271
271
  "role.label.security": "セキュリティ",
272
272
  "role.label.testing": "テスト",
273
273
  "role.label.custom": "カスタム...",
274
+ "cli.asideHelp": "実験的機能 · ローカルのメインエージェント専用です。Asideをメインランタイムにすると、定期ハートビート実行はサポートされず拒否されます。設定 / エージェントでアカウントを明示的に指定してください。",
275
+ "hb.asideUnsupported": "Asideをメインランタイムにすると、定期ハートビート実行はサポートされず拒否されます。ハートビートを使うには、エージェント設定で別のメインランタイムを選択してください。",
274
276
  "hb.empty": "heartbeat はありません",
275
277
  "hb.name": "名前",
276
278
  "hb.prompt": "プロンプト...",
@@ -273,6 +273,8 @@
273
273
  "role.label.security": "보안",
274
274
  "role.label.testing": "테스트",
275
275
  "role.label.custom": "커스텀...",
276
+ "cli.asideHelp": "실험 기능 · 로컬 메인 에이전트 전용입니다. Aside를 메인 런타임으로 사용하면 예약 하트비트 실행은 지원되지 않으며 거부됩니다. 설정 / 에이전트에서 계정을 명시적으로 지정하세요.",
277
+ "hb.asideUnsupported": "Aside를 메인 런타임으로 사용하면 예약 하트비트 실행은 지원되지 않으며 거부됩니다. 하트비트를 사용하려면 에이전트 설정에서 다른 메인 런타임을 선택하세요.",
276
278
  "hb.empty": "하트비트가 없습니다",
277
279
  "hb.name": "이름",
278
280
  "hb.prompt": "프롬프트...",
@@ -271,6 +271,8 @@
271
271
  "role.label.security": "安全",
272
272
  "role.label.testing": "测试",
273
273
  "role.label.custom": "自定义...",
274
+ "cli.asideHelp": "实验性功能 · 仅限本地主要代理。使用 Aside 作为主运行时时,不支持定时心跳运行,运行将被拒绝。请在设置 / 代理中明确指定账户。",
275
+ "hb.asideUnsupported": "使用 Aside 作为主运行时时,不支持定时心跳运行,运行将被拒绝。要使用心跳,请在代理设置中选择其他主运行时。",
274
276
  "hb.empty": "暂无 heartbeat",
275
277
  "hb.name": "名称",
276
278
  "hb.prompt": "提示...",
@@ -80,7 +80,7 @@ export function SettingsShell({ port = null, instanceUrl = null, onDirtyChange,
80
80
  const proxyClient = useMemo(() => port === null ? null : createSettingsClient(port), [port]);
81
81
  const unavailableClient: SettingsClient = useMemo(() => {
82
82
  const reject = async (): Promise<never> => { throw new Error('No selected instance'); };
83
- return { get: reject, put: reject, post: reject, delete: reject };
83
+ return { url: () => { throw new Error('No selected instance'); }, get: reject, put: reject, post: reject, delete: reject };
84
84
  }, []);
85
85
  const client = suppliedClient ?? proxyClient ?? unavailableClient;
86
86
  const available = entriesForScopes(scopes, hasInstance, locale).filter(entry => entry.scope !== 'manager' || manager);
@@ -1,5 +1,5 @@
1
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
- import type { SettingsPageProps, DirtyEntry } from '../types';
1
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
2
+ import type { SettingsPageProps, SettingsClient, DirtyEntry } from '../types';
3
3
  import {
4
4
  CLI_STATUS_POLL_HORIZON_MS,
5
5
  planCliStatusPoll,
@@ -9,8 +9,11 @@ import {
9
9
  PageLoading,
10
10
  PageOffline,
11
11
  usePageSnapshot,
12
+ type SnapshotState,
12
13
  } from './page-shell';
13
14
  import { parsePermissionsValue, permissionsEditMode } from './Permissions';
15
+ import { AsideSelectionFields } from './components/AsideSelectionFields';
16
+ import { useAsideModels, asideSelectionError, isAsideAccount } from './components/aside-models';
14
17
  import { RuntimeHeader } from './components/agent/RuntimeHeader';
15
18
  import { PermissionQuickSection } from './components/agent/PermissionQuickSection';
16
19
  import { FlushAgentSection } from './components/agent/FlushAgentSection';
@@ -120,6 +123,8 @@ type FlushSnapshot = {
120
123
 
121
124
  type RuntimeDraft = {
122
125
  cli: string;
126
+ account: string;
127
+ host: string;
123
128
  provider: string;
124
129
  model: string;
125
130
  effort: string;
@@ -130,10 +135,26 @@ type RuntimeDraft = {
130
135
  permissions: unknown;
131
136
  };
132
137
 
133
- export default function Agent({ port, client, dirty, registerSave }: SettingsPageProps) {
134
- const { state, refresh, setData } = usePageSnapshot<AgentSnapshot>(client, '/api/settings');
138
+ const ownsAgentKey = (key: string) => ['cli', 'workingDir', 'permissions', 'runtimeEmployees', 'flushCli', 'flushModel', 'multiSession.midRunPolicy'].includes(key) || key.startsWith('activeOverrides.') || key.startsWith('perCli.');
139
+
140
+ export default function Agent({ port, instanceUrl, client, dirty, registerSave }: SettingsPageProps) {
141
+ const instance = useMemo(() => ({ client, port, instanceUrl, dirty }), [client, port, instanceUrl, dirty]);
142
+ const activeInstance = useRef<typeof instance | null>(null);
143
+ const activeOperation = useRef<Promise<void> | null>(null);
144
+ const metadataGeneration = useRef(0);
145
+ const [saving, setSaving] = useState(false);
146
+ const [saveError, setSaveError] = useState<string | null>(null);
147
+ const snapshotClient = useMemo<SettingsClient>(() => ({ ...client,
148
+ get: async <T,>(path: string, init?: RequestInit) => ({ instance, value: await client.get<AgentSnapshot>(path, init) }) as T,
149
+ }), [client, instance]);
150
+ const { state: boundState, refresh, setData: setBoundData } = usePageSnapshot<{ instance: typeof instance; value: AgentSnapshot }>(snapshotClient, '/api/settings');
151
+ const state: SnapshotState<AgentSnapshot> = boundState.kind === 'ready'
152
+ ? boundState.data.instance === instance ? { kind: 'ready', data: boundState.data.value } : { kind: 'loading' } : boundState;
153
+ const setData = useCallback((value: AgentSnapshot) => setBoundData({ instance, value }), [instance, setBoundData]);
135
154
  const [draft, setDraft] = useState<RuntimeDraft>({
136
155
  cli: '',
156
+ account: '',
157
+ host: '',
137
158
  provider: '',
138
159
  model: '',
139
160
  effort: '',
@@ -156,16 +177,37 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
156
177
  const [sessionMigrationError, setSessionMigrationError] = useState<string | null>(null);
157
178
 
158
179
  const loadCliMeta = useCallback(async () => {
180
+ const generation = ++metadataGeneration.current;
159
181
  try {
160
182
  const response = await client.get<{ data?: unknown } | Record<string, unknown>>('/api/cli-registry');
183
+ if (activeInstance.current !== instance || generation !== metadataGeneration.current) return;
161
184
  const data = response && typeof response === 'object' && 'data' in response
162
185
  ? (response as { data?: unknown }).data
163
186
  : response;
164
187
  setCliMeta(normalizeCliMetaRegistry(data));
165
188
  } catch {
166
- setCliMeta(null);
189
+ if (activeInstance.current === instance && generation === metadataGeneration.current) setCliMeta(null);
190
+ }
191
+ }, [client, instance]);
192
+
193
+ const aside = useAsideModels(client, instanceUrl, port, draft.account);
194
+ const savedAside = state.kind === 'ready' ? state.data.perCli?.['aside'] : undefined;
195
+ const asideModel = draft.cli === 'aside' ? draft.model : String(dirty.pending.get('activeOverrides.aside.model')?.value || savedAside?.model || '');
196
+ const asideEffort = draft.cli === 'aside' ? draft.effort : String(dirty.pending.get('activeOverrides.aside.effort')?.value || savedAside?.effort || '');
197
+ const asideError = draft.host !== 'local' ? 'Aside requires the local host.' : asideSelectionError(aside.inventory, asideModel, asideEffort);
198
+ useEffect(() => {
199
+ for (const [key, entry] of dirty.pending) {
200
+ if (key.startsWith('perCli.aside.') || key.startsWith('activeOverrides.aside.') || (key === 'cli' && draft.cli === 'aside')) {
201
+ const valid = !asideError;
202
+ if (entry.valid !== valid) dirty.set(key, { ...entry, valid });
203
+ }
167
204
  }
168
- }, [client]);
205
+ }, [asideError, dirty, draft.cli]);
206
+ useLayoutEffect(() => {
207
+ activeInstance.current = instance; activeOperation.current = null; ++metadataGeneration.current;
208
+ setSaving(false); setSaveError(null); setCliMeta(null);
209
+ return () => { activeInstance.current = null; activeOperation.current = null; ++metadataGeneration.current; };
210
+ }, [instance]);
169
211
 
170
212
  // Generation ref, same convention as Browser.tsx: clearing a timer does not
171
213
  // stop a request that is already in flight, and that response would
@@ -177,45 +219,49 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
177
219
  const loadCliStatus = useCallback(async (gen?: number) => {
178
220
  try {
179
221
  const next = await client.get<Record<string, CliStatusInfo>>('/api/cli-status');
180
- if (gen !== undefined && gen !== cliStatusGenRef.current) return;
222
+ if (activeInstance.current !== instance || (gen !== undefined && gen !== cliStatusGenRef.current)) return;
181
223
  cliStatusRef.current = next;
182
224
  setCliStatus(next);
183
225
  } catch {
184
- if (gen !== undefined && gen !== cliStatusGenRef.current) return;
226
+ if (activeInstance.current !== instance || (gen !== undefined && gen !== cliStatusGenRef.current)) return;
185
227
  cliStatusRef.current = {};
186
228
  setCliStatus({});
187
229
  }
188
- }, [client]);
230
+ }, [client, instance]);
189
231
 
190
232
  const loadFlush = useCallback(async () => {
191
233
  setFlushLoading(true);
192
234
  setFlushError(null);
193
235
  try {
194
236
  const data = await client.get<FlushSnapshot>('/api/memory-files');
237
+ if (activeInstance.current !== instance) return;
195
238
  const next = { cli: data.cli || '', model: data.model || '' };
196
239
  setFlushOriginal(next);
197
240
  setFlushDraft(next);
198
241
  } catch (err: unknown) {
242
+ if (activeInstance.current !== instance) return;
199
243
  setFlushError(err instanceof Error ? err.message : String(err));
200
244
  } finally {
201
- setFlushLoading(false);
245
+ if (activeInstance.current === instance) setFlushLoading(false);
202
246
  }
203
- }, [client]);
247
+ }, [client, instance]);
204
248
 
205
249
  const loadEmployees = useCallback(async () => {
206
250
  setEmployeeLoading(true);
207
251
  setEmployeeError(null);
208
252
  try {
209
253
  const response = await client.get<RuntimeEmployeesResponse>('/api/employees');
254
+ if (activeInstance.current !== instance) return;
210
255
  const rows = unwrapRuntimeEmployees(response);
211
256
  setEmployeeOriginal(rows);
212
257
  setEmployeeDraft(rows);
213
258
  } catch (err: unknown) {
259
+ if (activeInstance.current !== instance) return;
214
260
  setEmployeeError(err instanceof Error ? err.message : String(err));
215
261
  } finally {
216
- setEmployeeLoading(false);
262
+ if (activeInstance.current === instance) setEmployeeLoading(false);
217
263
  }
218
- }, [client]);
264
+ }, [client, instance]);
219
265
 
220
266
  useEffect(() => {
221
267
  void loadCliMeta();
@@ -284,6 +330,8 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
284
330
  const meta = metaFor(cli, cliMeta);
285
331
  setDraft({
286
332
  cli,
333
+ account: state.data.perCli?.['aside']?.account || '',
334
+ host: state.data.perCli?.['aside']?.host || '',
287
335
  provider: state.data.perCli?.[cli]?.provider || meta.defaultProvider || '',
288
336
  model: runtimeModelFor(cli, state.data.perCli, state.data.activeOverrides),
289
337
  effort: runtimeEffortFor(cli, state.data.perCli, state.data.activeOverrides),
@@ -297,39 +345,50 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
297
345
  : permissions.mode === 'safe' ? 'safe'
298
346
  : 'auto',
299
347
  });
300
- }, [cliMeta, state]);
348
+ }, [state.kind === 'ready' ? state.data : null]);
301
349
 
302
- useEffect(() => {
303
- return () => {
304
- for (const key of Array.from(dirty.pending.keys())) {
305
- if (
306
- key === 'cli' ||
307
- key === 'workingDir' ||
308
- key === 'permissions' ||
309
- key === 'runtimeEmployees' ||
310
- key === 'flushCli' ||
311
- key === 'flushModel' ||
312
- key.startsWith('activeOverrides.')
313
- ) {
314
- dirty.remove(key);
315
- }
316
- }
317
- };
318
- }, [dirty]);
319
-
320
- const setEntry = useCallback((key: string, entry: DirtyEntry) => dirty.set(key, entry), [dirty]);
321
-
322
- const onSave = useCallback(async () => {
323
- const bundle = dirty.saveBundle();
324
- if (Object.keys(bundle).length === 0) return;
325
- const freshSettings = await saveAgentRuntime({ client, bundle, employeeDraft, employeeOriginal });
326
- dirty.clear();
327
- if (freshSettings) setData(freshSettings as AgentSnapshot);
328
- await refresh();
329
- await loadCliMeta();
330
- await loadFlush();
331
- await loadEmployees();
332
- }, [client, dirty, employeeDraft, employeeOriginal, loadCliMeta, loadEmployees, loadFlush, refresh, setData]);
350
+ useEffect(() => () => {
351
+ for (const key of Array.from(dirty.pending.keys())) if (ownsAgentKey(key)) dirty.remove(key);
352
+ }, [dirty, instance]);
353
+
354
+ const setEntry = useCallback((key: string, entry: DirtyEntry) => {
355
+ if (activeInstance.current === instance && !activeOperation.current && ownsAgentKey(key)) dirty.set(key, entry);
356
+ }, [dirty, instance]);
357
+
358
+ const onSave = useCallback((): Promise<void> => {
359
+ if (activeInstance.current !== instance) return Promise.resolve();
360
+ if (activeOperation.current) return activeOperation.current;
361
+ if ([...dirty.pending.keys()].some(key => key.startsWith('perCli.aside.') || key.startsWith('activeOverrides.aside.')) && asideError) return Promise.reject(new Error(asideError));
362
+ const bundle = Object.fromEntries(Object.entries(dirty.saveBundle()).filter(([key]) => ownsAgentKey(key)));
363
+ if (!Object.keys(bundle).length) return Promise.resolve();
364
+ if ((draft.cli === 'aside' || Object.keys(bundle).some(key => key.startsWith('perCli.aside.') || key.startsWith('activeOverrides.aside.'))) && asideError)
365
+ return Promise.reject(new Error(asideError));
366
+ if ((Object.hasOwn(bundle, 'flushCli') || Object.hasOwn(bundle, 'flushModel')) && (flushDraft.cli || draft.cli) === 'aside') return Promise.reject(new Error('Aside cannot run memory flush.'));
367
+ if (Object.hasOwn(bundle, 'perCli.aside.account')) {
368
+ bundle['activeOverrides.aside.model'] ??= '';
369
+ bundle['activeOverrides.aside.effort'] ??= '';
370
+ }
371
+ const submitted = new Map([...dirty.pending].filter(([key]) => Object.hasOwn(bundle, key)));
372
+ setSaving(true); setSaveError(null);
373
+ const operation = Promise.resolve().then(async () => {
374
+ if (activeInstance.current !== instance) return;
375
+ const freshSettings = await saveAgentRuntime({ client, bundle, employeeDraft, employeeOriginal });
376
+ if (activeInstance.current !== instance) return;
377
+ for (const [key, entry] of submitted) if (dirty.pending.get(key) === entry) dirty.remove(key);
378
+ if (freshSettings) setData(freshSettings as AgentSnapshot);
379
+ await refresh();
380
+ await loadCliMeta();
381
+ await loadFlush();
382
+ await loadEmployees();
383
+ }).catch(error => {
384
+ if (activeInstance.current === instance) throw error;
385
+ }).finally(() => {
386
+ if (activeOperation.current === operation) activeOperation.current = null;
387
+ if (activeInstance.current === instance) setSaving(false);
388
+ });
389
+ activeOperation.current = operation;
390
+ return operation;
391
+ }, [client, dirty, draft.cli, flushDraft.cli, asideError, instance, employeeDraft, employeeOriginal, loadCliMeta, loadEmployees, loadFlush, refresh, setData]);
333
392
 
334
393
  useEffect(() => {
335
394
  if (!registerSave) return;
@@ -414,12 +473,12 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
414
473
 
415
474
  function resetActiveOverrideKeys(): void {
416
475
  for (const key of Array.from(dirty.pending.keys())) {
417
- if (key.startsWith('activeOverrides.')) dirty.remove(key);
476
+ if (key.startsWith('activeOverrides.') && !key.startsWith('activeOverrides.aside.')) dirty.remove(key);
418
477
  }
419
478
  }
420
479
 
421
480
  function setRuntimeDraft(next: RuntimeDraft): void {
422
- setDraft(next);
481
+ if (activeInstance.current === instance && !activeOperation.current) setDraft(next);
423
482
  }
424
483
 
425
484
  return (
@@ -427,9 +486,11 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
427
486
  className="settings-page-form"
428
487
  onSubmit={(event) => {
429
488
  event.preventDefault();
430
- void onSave();
489
+ void onSave().catch(error => { if (activeInstance.current === instance) setSaveError(describeError(error)); });
431
490
  }}
432
491
  >
492
+ {saveError ? <PageError message={saveError} /> : null}
493
+ <fieldset disabled={saving} style={{ border: 0, padding: 0, margin: 0, minWidth: 0 }}>
433
494
  {settingsData.runtimeDefaultMigration?.state === 'pending' ? (
434
495
  <div className="settings-inline-notice" role="status">
435
496
  <strong>기본 런타임 변경 안내</strong>
@@ -480,6 +541,27 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
480
541
  </label>
481
542
  <CliProbeNotice status={cliStatus[draft.cli]} exhausted={cliStatusExhausted} />
482
543
  <RuntimeHeader
544
+ asideFields={draft.cli === 'aside' ? <AsideSelectionFields id="agent-aside" account={draft.account} host={draft.host}
545
+ model={draft.model} effort={draft.effort} inventory={aside.inventory} disabled={saving} refresh={aside.refresh}
546
+ onAccountChange={next => {
547
+ if (activeInstance.current !== instance || activeOperation.current) return;
548
+ setRuntimeDraft({ ...draft, account: next, host: 'local', model: perCli['aside']?.model || '', effort: perCli['aside']?.effort || '' });
549
+ setEntry('perCli.aside.account', { value: next, original: perCli['aside']?.account || '', valid: isAsideAccount(next) });
550
+ setEntry('perCli.aside.host', { value: 'local', original: perCli['aside']?.host || '', valid: true });
551
+ setEntry('activeOverrides.aside.model', { value: '', original: activeOverrides['aside']?.model || '', valid: true });
552
+ setEntry('activeOverrides.aside.effort', { value: '', original: activeOverrides['aside']?.effort || '', valid: true });
553
+ }}
554
+ onModelChange={next => {
555
+ if (activeInstance.current !== instance || activeOperation.current) return;
556
+ setRuntimeDraft({ ...draft, model: next });
557
+ setEntry('activeOverrides.aside.model', { value: next, original: dirty.pending.has('perCli.aside.account') ? '' : activeOverrides['aside']?.model || '', valid: true });
558
+ }}
559
+ onEffortChange={next => {
560
+ if (activeInstance.current !== instance || activeOperation.current) return;
561
+ const effort = next || 'default';
562
+ setRuntimeDraft({ ...draft, effort });
563
+ setEntry('activeOverrides.aside.effort', { value: effort, original: dirty.pending.has('perCli.aside.account') ? '' : activeOverrides['aside']?.effort || '', valid: true });
564
+ }} /> : null}
483
565
  cli={draft.cli}
484
566
  cliOptions={cliOptions}
485
567
  provider={activeProvider}
@@ -496,9 +578,11 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
496
578
  const nextDraft = {
497
579
  ...draft,
498
580
  cli: next,
581
+ account: draft.account,
582
+ host: draft.host,
499
583
  provider: perCli[next]?.provider || nextMeta.defaultProvider || '',
500
- model: runtimeModelFor(next, perCli, activeOverrides),
501
- effort: runtimeEffortFor(next, perCli, activeOverrides),
584
+ model: next === 'aside' && (dirty.pending.has('activeOverrides.aside.model') || dirty.pending.has('perCli.aside.account')) ? String(dirty.pending.get('activeOverrides.aside.model')?.value || perCli['aside']?.model || '') : runtimeModelFor(next, perCli, activeOverrides),
585
+ effort: next === 'aside' && (dirty.pending.has('activeOverrides.aside.effort') || dirty.pending.has('perCli.aside.account')) ? String(dirty.pending.get('activeOverrides.aside.effort')?.value || perCli['aside']?.effort || '') : runtimeEffortFor(next, perCli, activeOverrides),
502
586
  };
503
587
  resetActiveOverrideKeys();
504
588
  setRuntimeDraft(nextDraft);
@@ -594,7 +678,7 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
594
678
  onFlushCliChange={(next) => {
595
679
  const model = next ? metaFor(next, cliMeta).models[0] || '' : '';
596
680
  setFlushDraft({ cli: next, model });
597
- setEntry('flushCli', { value: next, original: flushOriginal.cli || '', valid: true });
681
+ setEntry('flushCli', { value: next, original: flushOriginal.cli || '', valid: next !== 'aside' && (next !== '' || draft.cli !== 'aside') });
598
682
  setEntry('flushModel', { value: model, original: flushOriginal.model || '', valid: true });
599
683
  }}
600
684
  onFlushModelChange={(next) => {
@@ -605,7 +689,7 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
605
689
  <AgentEmployeesSection
606
690
  roster={employeeDraft}
607
691
  original={employeeOriginal}
608
- cliOptions={cliOptions}
692
+ cliOptions={cliOptions.filter(cli => cli !== 'aside')}
609
693
  cliMeta={cliMeta}
610
694
  loading={employeeLoading}
611
695
  error={employeeError}
@@ -622,6 +706,7 @@ export default function Agent({ port, client, dirty, registerSave }: SettingsPag
622
706
  });
623
707
  }}
624
708
  />
709
+ </fieldset>
625
710
  </form>
626
711
  );
627
712
  }
@@ -65,6 +65,7 @@ type HeartbeatBlock = {
65
65
  target?: string;
66
66
  };
67
67
  type SettingsSnapshot = {
68
+ cli?: string;
68
69
  heartbeat?: HeartbeatBlock;
69
70
  perCli?: Record<string, unknown>;
70
71
  [key: string]: unknown;
@@ -178,6 +179,11 @@ export default function Heartbeat({ port, client, dirty, registerSave }: Setting
178
179
  title="Heartbeat"
179
180
  hint="Background prompts the agent runs on a schedule."
180
181
  >
182
+ {settingsSnap.state.data.cli === 'aside' ? (
183
+ <div className="settings-inline-notice" role="status">
184
+ <p>Scheduled heartbeat runs are unsupported with Aside as the main runtime and will be rejected. Choose another main runtime in Agent settings to use heartbeat.</p>
185
+ </div>
186
+ ) : null}
181
187
  <ToggleField
182
188
  id="hb-enabled"
183
189
  label="Heartbeat enabled"