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.
- package/README.md +16 -0
- package/dist/src/agent/aside-catalog.js +339 -0
- package/dist/src/agent/aside-catalog.js.map +1 -0
- package/dist/src/agent/aside-cli.js +208 -0
- package/dist/src/agent/aside-cli.js.map +1 -0
- package/dist/src/agent/aside-replay.js +120 -0
- package/dist/src/agent/aside-replay.js.map +1 -0
- package/dist/src/agent/aside-runtime.js +175 -0
- package/dist/src/agent/aside-runtime.js.map +1 -0
- package/dist/src/agent/lifecycle-handler.js +26 -18
- package/dist/src/agent/lifecycle-handler.js.map +1 -1
- package/dist/src/agent/memory-flush-controller.js +3 -0
- package/dist/src/agent/memory-flush-controller.js.map +1 -1
- package/dist/src/agent/prompt-context.js +2 -2
- package/dist/src/agent/prompt-context.js.map +1 -1
- package/dist/src/agent/session-persistence.js +2 -0
- package/dist/src/agent/session-persistence.js.map +1 -1
- package/dist/src/agent/spawn/backend-aside.js +314 -0
- package/dist/src/agent/spawn/backend-aside.js.map +1 -0
- package/dist/src/agent/spawn/resume.js +3 -0
- package/dist/src/agent/spawn/resume.js.map +1 -1
- package/dist/src/agent/spawn-env.js +5 -0
- package/dist/src/agent/spawn-env.js.map +1 -1
- package/dist/src/agent/spawn.js +60 -6
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/cli-status-worker.js +2 -1
- package/dist/src/cli/cli-status-worker.js.map +1 -1
- package/dist/src/cli/employee-handler.js +2 -0
- package/dist/src/cli/employee-handler.js.map +1 -1
- package/dist/src/cli/handlers-completions.js +20 -8
- package/dist/src/cli/handlers-completions.js.map +1 -1
- package/dist/src/cli/handlers-runtime.js +2 -0
- package/dist/src/cli/handlers-runtime.js.map +1 -1
- package/dist/src/cli/readiness.js +7 -3
- package/dist/src/cli/readiness.js.map +1 -1
- package/dist/src/cli/registry-live.js +47 -0
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/cli/registry.js +5 -0
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/core/config.js +10 -3
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/employees.js +5 -3
- package/dist/src/core/employees.js.map +1 -1
- package/dist/src/core/settings-merge.js +35 -0
- package/dist/src/core/settings-merge.js.map +1 -1
- package/dist/src/core/settings-watch.js +10 -1
- package/dist/src/core/settings-watch.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +2 -0
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/routes/employees.js +8 -0
- package/dist/src/routes/employees.js.map +1 -1
- package/dist/src/routes/jaw-memory.js +4 -0
- package/dist/src/routes/jaw-memory.js.map +1 -1
- package/dist/src/routes/orchestrate.js +22 -3
- package/dist/src/routes/orchestrate.js.map +1 -1
- package/dist/src/routes/settings.js +23 -0
- package/dist/src/routes/settings.js.map +1 -1
- package/dist/src/shared/aside-contract.js +2 -0
- package/dist/src/shared/aside-contract.js.map +1 -0
- package/dist/src/types/cli-engine.js +1 -0
- package/dist/src/types/cli-engine.js.map +1 -1
- package/package.json +1 -1
- package/public/dist/.vite/manifest.json +186 -175
- package/public/dist/assets/{Activity-DPLlOJ46.js → Activity-DsBgOkCP.js} +1 -1
- package/public/dist/assets/{AdvancedExport-BgOgGvc5.js → AdvancedExport-COGQ7vbK.js} +1 -1
- package/public/dist/assets/Agent-6qGQN7lY.js +1 -0
- package/public/dist/assets/AsideSelectionFields-DvU5xWAP.js +1 -0
- package/public/dist/assets/{Browser-m6VynQXV.js → Browser-r09vOsR1.js} +1 -1
- package/public/dist/assets/ChannelEnablementControl-ooOkdd-Q.js +85 -0
- package/public/dist/assets/{ChannelSetupEntry-DHLoZEzJ.js → ChannelSetupEntry-B0b4TdhH.js} +1 -1
- package/public/dist/assets/{ChannelsDiscord-B-G8vsYy.js → ChannelsDiscord-9EMLt5b3.js} +1 -1
- package/public/dist/assets/{ChannelsSlack-oPuh922z.js → ChannelsSlack-B8ETzWFt.js} +1 -1
- package/public/dist/assets/{ChannelsTelegram-PmTv43KQ.js → ChannelsTelegram-CflLUsQw.js} +1 -1
- package/public/dist/assets/{DashboardMeta-BqEiJrvr.js → DashboardMeta-wjEuh9-E.js} +1 -1
- package/public/dist/assets/{Developer-CbySB7_Z.js → Developer-B3cDkldo.js} +1 -1
- package/public/dist/assets/{Display-DU4_76cS.js → Display-BXQxpv6-.js} +1 -1
- package/public/dist/assets/{Display-C0Zdamcb.js → Display-Cl4DD334.js} +1 -1
- package/public/dist/assets/{Embedding-CCFMOB_2.js → Embedding-D-T92-En.js} +1 -1
- package/public/dist/assets/{Employees-BanvF58d.js → Employees-Cf4IeF0N.js} +1 -1
- package/public/dist/assets/{Heartbeat-uyi_MpUd.js → Heartbeat-Bj8V_bUM.js} +1 -1
- package/public/dist/assets/{Mcp-rjPoL_Zo.js → Mcp-jAcut_fw.js} +1 -1
- package/public/dist/assets/Memory-BOXkBFKg.js +1 -0
- package/public/dist/assets/ModelProvider-DIDQ37z-.js +1 -0
- package/public/dist/assets/{Network-CuqFrOvR.js → Network-I5eOe2hf.js} +1 -1
- package/public/dist/assets/{Permissions-BltXiZnz.js → Permissions-B7PthH01.js} +1 -1
- package/public/dist/assets/{Permissions-DcCbFMIq.js → Permissions-BCYUfJg1.js} +1 -1
- package/public/dist/assets/Profile-CYPMVhQM.js +1 -0
- package/public/dist/assets/{Prompts-C9vY8ivQ.js → Prompts-BRAgZLNQ.js} +1 -1
- package/public/dist/assets/{SpeechKeys-BJ8V7U5E.js → SpeechKeys-UlXNSFoe.js} +1 -1
- package/public/dist/assets/{TelegramHub-DtyjLGFR.js → TelegramHub-BhUFhIcv.js} +1 -1
- package/public/dist/assets/agent-meta-v4jNRAbX.js +1 -0
- package/public/dist/assets/app-SVIyP1TT.js +59 -0
- package/public/dist/assets/bgtask-badge-HIrvpcr_.js +1 -0
- package/public/dist/assets/{bgtask-badge-oPvZyylH.js → bgtask-badge-tYRnplEV.js} +1 -1
- package/public/dist/assets/constants-_iw-lnKY.js +1 -0
- package/public/dist/assets/{employees-BchFBkDz.js → employees-DNTPcxtZ.js} +2 -2
- package/public/dist/assets/{error-normalize-clOi-AAA.js → error-normalize-jnjrKZDu.js} +1 -1
- package/public/dist/assets/{iframe-renderer-CcGzEi0v.js → iframe-renderer--lHbKTDh.js} +2 -2
- package/public/dist/assets/iframe-renderer-DigNC5FU.js +1 -0
- package/public/dist/assets/{manager-nquYZ7_x.js → manager-qGH-xUZ7.js} +1 -1
- package/public/dist/assets/{memory-DTpD4see.js → memory-BAdRkBdk.js} +1 -1
- package/public/dist/assets/memory-DA8x5314.js +1 -0
- package/public/dist/assets/message-history-CGOgZH4l.js +1 -0
- package/public/dist/assets/{message-history-DSvflaZ9.js → message-history-PAYeKmia.js} +1 -1
- package/public/dist/assets/page-shell-Cz2ylri8.js +1 -0
- package/public/dist/assets/{render-D8dVQ_bm.js → render-F6xF6w6c.js} +1 -1
- package/public/dist/assets/settings-DMbNzuTm.js +1 -0
- package/public/dist/assets/{settings-DMiQe469.js → settings-DZ2-ca8T.js} +1 -1
- package/public/dist/assets/{settings-Bp0sgQ2a.js → settings-W0WFhBf2.js} +1 -1
- package/public/dist/assets/settings-core-2vZXNf6n.js +2 -0
- package/public/dist/assets/settings-core-BofMyzL9.js +1 -0
- package/public/dist/assets/{settings-embedding-BRW0WZvh.js → settings-embedding-Bjwpe8xu.js} +2 -2
- package/public/dist/assets/{shared-D1begqvi.js → shared-CftvKNY9.js} +1 -1
- package/public/dist/assets/{sidebar-CrpBbYFg.js → sidebar-Clkqd78q.js} +3 -3
- package/public/dist/assets/{skills-Bx8U-gJa.js → skills-0aA-RuN9.js} +1 -1
- package/public/dist/assets/skills-SqmQXndw.js +1 -0
- package/public/dist/assets/{slash-commands-DkrGUOnC.js → slash-commands-4zHHXEeU.js} +1 -1
- package/public/dist/assets/slash-commands-D5LqONzL.js +1 -0
- package/public/dist/assets/{trace-drawer-Dbe5pTbf.js → trace-drawer-B4O0Tl1M.js} +1 -1
- package/public/dist/assets/{ui-rnVAM91F.js → ui-CVtGpivr.js} +1 -1
- package/public/dist/assets/ui-_cqgseFX.js +1 -0
- package/public/dist/index.html +1 -1
- package/public/dist/manager/index.html +1 -1
- package/public/dist/settings/index.html +1 -1
- package/public/js/constants.ts +5 -0
- package/public/js/features/employees.ts +3 -3
- package/public/js/features/heartbeat.ts +14 -1
- package/public/js/features/settings-core.ts +39 -13
- package/public/js/features/settings-types.ts +1 -1
- package/public/locales/en.json +2 -0
- package/public/locales/ja.json +2 -0
- package/public/locales/ko.json +2 -0
- package/public/locales/zh.json +2 -0
- package/public/manager/src/settings/SettingsShell.tsx +1 -1
- package/public/manager/src/settings/pages/Agent.tsx +137 -52
- package/public/manager/src/settings/pages/Heartbeat.tsx +6 -0
- package/public/manager/src/settings/pages/Memory.tsx +8 -6
- package/public/manager/src/settings/pages/ModelProvider.tsx +31 -6
- package/public/manager/src/settings/pages/Profile.tsx +2 -2
- package/public/manager/src/settings/pages/components/AsideSelectionFields.tsx +33 -0
- package/public/manager/src/settings/pages/components/AvatarCard.tsx +22 -34
- package/public/manager/src/settings/pages/components/PerCliRow.tsx +29 -1
- package/public/manager/src/settings/pages/components/agent/FlushAgentSection.tsx +7 -6
- package/public/manager/src/settings/pages/components/agent/RuntimeEmployeeRow.tsx +4 -4
- package/public/manager/src/settings/pages/components/agent/RuntimeHeader.tsx +5 -0
- package/public/manager/src/settings/pages/components/agent/agent-meta.ts +7 -0
- package/public/manager/src/settings/pages/components/agent/runtime-employees-helpers.ts +3 -2
- package/public/manager/src/settings/pages/components/aside-models.ts +104 -0
- package/public/manager/src/settings/settings-client.ts +3 -1
- package/public/manager/src/settings/types.ts +1 -0
- package/public/dist/assets/Agent-O_U8ny90.js +0 -1
- package/public/dist/assets/ChannelEnablementControl-BSuKQhTW.js +0 -85
- package/public/dist/assets/Memory-C9I33pIi.js +0 -1
- package/public/dist/assets/ModelProvider-fiUktMaO.js +0 -1
- package/public/dist/assets/Profile-DUL7-RE_.js +0 -1
- package/public/dist/assets/agent-meta-94LMwZZK.js +0 -1
- package/public/dist/assets/app-Iu76iEiU.js +0 -59
- package/public/dist/assets/bgtask-badge-DHo2mkMI.js +0 -1
- package/public/dist/assets/constants-DmZ9Rjym.js +0 -1
- package/public/dist/assets/iframe-renderer-DSLsfZq8.js +0 -1
- package/public/dist/assets/memory-BgUF3M9l.js +0 -1
- package/public/dist/assets/message-history-DkB768L3.js +0 -1
- package/public/dist/assets/page-shell-DfinU3mb.js +0 -1
- package/public/dist/assets/settings-DxjoyiIM.js +0 -1
- package/public/dist/assets/settings-core-CXWmPKUE.js +0 -1
- package/public/dist/assets/settings-core-D4zGy72z.js +0 -2
- package/public/dist/assets/skills-BY-hQnqM.js +0 -1
- package/public/dist/assets/slash-commands-C53T9J3g.js +0 -1
- 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
|
|
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 (
|
|
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:
|
|
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:
|
|
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,
|
package/public/locales/en.json
CHANGED
|
@@ -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...",
|
package/public/locales/ja.json
CHANGED
|
@@ -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": "プロンプト...",
|
package/public/locales/ko.json
CHANGED
|
@@ -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": "프롬프트...",
|
package/public/locales/zh.json
CHANGED
|
@@ -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
|
-
|
|
134
|
-
|
|
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
|
-
}, [
|
|
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
|
-
}, [
|
|
348
|
+
}, [state.kind === 'ready' ? state.data : null]);
|
|
301
349
|
|
|
302
|
-
useEffect(() => {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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:
|
|
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"
|