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
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type MemoryBlock,
|
|
38
38
|
type MemoryEntry,
|
|
39
39
|
} from './components/memory-helpers';
|
|
40
|
-
import { metaFor,
|
|
40
|
+
import { metaFor, auxiliaryRuntimeOptions, isRetiredCliSelection, retiredRuntimeLabel } from './components/agent/agent-meta';
|
|
41
41
|
|
|
42
42
|
// Re-export pure helpers for unit tests (Heartbeat pattern).
|
|
43
43
|
export {
|
|
@@ -125,6 +125,7 @@ export default function Memory({ port, client, dirty, registerSave }: SettingsPa
|
|
|
125
125
|
if (isMemorySettingsKey(key)) filtered[key] = value;
|
|
126
126
|
}
|
|
127
127
|
if (Object.keys(filtered).length === 0) return;
|
|
128
|
+
if (filtered['memory.cli'] === 'aside' || (filtered['memory.cli'] === '' && settingsSnap.state.kind === 'ready' && settingsSnap.state.data['cli'] === 'aside')) throw new Error('Aside cannot run memory flush.');
|
|
128
129
|
const patch = expandPatch(filtered);
|
|
129
130
|
const updated = await client.put<SettingsSnapshot>('/api/settings', patch);
|
|
130
131
|
const fresh = (updated && typeof updated === 'object' && 'data' in updated
|
|
@@ -211,12 +212,13 @@ export default function Memory({ port, client, dirty, registerSave }: SettingsPa
|
|
|
211
212
|
const retentionError = validatePositiveInt(retentionDays, 'Retention days');
|
|
212
213
|
|
|
213
214
|
const perCli = settingsSnap.state.data.perCli || {};
|
|
214
|
-
const cliKeys =
|
|
215
|
+
const cliKeys = auxiliaryRuntimeOptions(Object.keys(perCli));
|
|
216
|
+
const asideInherited = !cli && settingsSnap.state.data['cli'] === 'aside';
|
|
215
217
|
const cliOptions = [
|
|
216
|
-
{ value: '', label: '(profile default)' },
|
|
218
|
+
...(settingsSnap.state.data['cli'] === 'aside' ? [] : [{ value: '', label: '(profile default)' }]),
|
|
217
219
|
...cliKeys.map((c) => ({ value: c, label: metaFor(c).label || c })),
|
|
218
220
|
];
|
|
219
|
-
if (cli && !isRetiredCliSelection(cli) && !cliOptions.some((opt) => opt.value === cli)) {
|
|
221
|
+
if (cli && cli !== 'aside' && !isRetiredCliSelection(cli) && !cliOptions.some((opt) => opt.value === cli)) {
|
|
220
222
|
cliOptions.push({ value: cli, label: `${metaFor(cli).label || cli} (legacy)` });
|
|
221
223
|
}
|
|
222
224
|
|
|
@@ -276,8 +278,8 @@ export default function Memory({ port, client, dirty, registerSave }: SettingsPa
|
|
|
276
278
|
id="memory-cli"
|
|
277
279
|
label="Flush CLI"
|
|
278
280
|
value={cli}
|
|
279
|
-
missingValueLabel={isRetiredCliSelection(cli) ? retiredRuntimeLabel(cli) : undefined}
|
|
280
|
-
error={isRetiredCliSelection(cli) ? 'The saved runtime is retired. Choose an available runtime.' : null}
|
|
281
|
+
missingValueLabel={asideInherited ? '(profile default unavailable)' : cli === 'aside' ? 'Aside (main-only)' : isRetiredCliSelection(cli) ? retiredRuntimeLabel(cli) : undefined}
|
|
282
|
+
error={asideInherited || cli === 'aside' ? 'Aside cannot run memory flush. Choose a separate runtime.' : isRetiredCliSelection(cli) ? 'The saved runtime is retired. Choose an available runtime.' : null}
|
|
281
283
|
options={cliOptions}
|
|
282
284
|
onChange={(next) => {
|
|
283
285
|
setCli(next);
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
usePageSnapshot,
|
|
17
17
|
type SnapshotState,
|
|
18
18
|
} from './page-shell';
|
|
19
|
+
import { useAsideModels, asideSelectionError } from './components/aside-models';
|
|
19
20
|
import { PerCliRow, type PiRegistration } from './components/PerCliRow';
|
|
20
21
|
import { metaFor, normalizeCliMetaRegistry, selectableRuntimeOptions, isRetiredCliSelection, retiredRuntimeLabel } from './components/agent/agent-meta';
|
|
21
22
|
import type { CliMeta, PerCliEntry } from './components/agent/agent-meta';
|
|
@@ -29,9 +30,9 @@ type ModelSnapshot = {
|
|
|
29
30
|
pi?: PiSettingsView;
|
|
30
31
|
[key: string]: unknown;
|
|
31
32
|
};
|
|
32
|
-
type ModelInstance = Pick<SettingsPageProps, 'client' | 'port' | 'dirty'>;
|
|
33
|
+
type ModelInstance = Pick<SettingsPageProps, 'client' | 'port' | 'instanceUrl' | 'dirty'>;
|
|
33
34
|
type BoundSnapshot = { instance: ModelInstance; value: ModelSnapshot };
|
|
34
|
-
const ownsModelKey = (key: string) => key === 'fallbackOrder' || key.startsWith('perCli.');
|
|
35
|
+
const ownsModelKey = (key: string) => key === 'fallbackOrder' || key.startsWith('perCli.') || key === 'activeOverrides.aside.model' || key === 'activeOverrides.aside.effort';
|
|
35
36
|
|
|
36
37
|
function savedModelSnapshot(updated: unknown): ModelSnapshot {
|
|
37
38
|
const value = updated && typeof updated === 'object' && 'data' in updated ? updated.data : updated;
|
|
@@ -62,8 +63,8 @@ export function buildResetOverridesPatch(snapshot: ModelSnapshot): {
|
|
|
62
63
|
return { activeOverrides };
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export default function ModelProvider({ port, client, dirty, registerSave }: SettingsPageProps) {
|
|
66
|
-
const instance = useMemo<ModelInstance>(() => ({ client, port, dirty }), [client, port, dirty]);
|
|
66
|
+
export default function ModelProvider({ port, instanceUrl, client, dirty, registerSave }: SettingsPageProps) {
|
|
67
|
+
const instance = useMemo<ModelInstance>(() => ({ client, port, instanceUrl, dirty }), [client, port, instanceUrl, dirty]);
|
|
67
68
|
const activeInstance = useRef<ModelInstance | null>(null);
|
|
68
69
|
const activeOperation = useRef<{ instance: ModelInstance; kind: 'save' | 'reset'; promise: Promise<void> } | null>(null);
|
|
69
70
|
const metadataGeneration = useRef(0);
|
|
@@ -88,6 +89,20 @@ export default function ModelProvider({ port, client, dirty, registerSave }: Set
|
|
|
88
89
|
const [saving, setSaving] = useState(false);
|
|
89
90
|
const [cliMeta, setCliMeta] = useState<Record<string, CliMeta> | null>(null);
|
|
90
91
|
|
|
92
|
+
const asideDraft = perCliDraft['aside'];
|
|
93
|
+
const aside = useAsideModels(client, instanceUrl, port, asideDraft?.account || '', !!asideDraft);
|
|
94
|
+
const asideSaveError = asideDraft ? asideDraft.host !== 'local' ? 'Aside requires the local host.'
|
|
95
|
+
: asideSelectionError(aside.inventory, asideDraft.model || '', asideDraft.effort || '') : null;
|
|
96
|
+
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
for (const [key, entry] of dirty.pending) {
|
|
99
|
+
if (key.startsWith('perCli.aside.') || key.startsWith('activeOverrides.aside.')) {
|
|
100
|
+
const valid = !asideSaveError;
|
|
101
|
+
if (entry.valid !== valid) dirty.set(key, { ...entry, valid });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}, [asideSaveError, dirty]);
|
|
105
|
+
|
|
91
106
|
useLayoutEffect(() => {
|
|
92
107
|
activeInstance.current = instance; activeOperation.current = null; ++metadataGeneration.current;
|
|
93
108
|
setSaving(false); setResetting(false); setSaveError(null); setResetError(null); setCliMeta(null);
|
|
@@ -159,8 +174,15 @@ export default function ModelProvider({ port, client, dirty, registerSave }: Set
|
|
|
159
174
|
const pending = activeOperation.current;
|
|
160
175
|
if (pending) return pending.kind === 'save' ? pending.promise
|
|
161
176
|
: Promise.reject(new Error('Wait for the active override reset before saving.'));
|
|
177
|
+
if ([...dirty.pending.keys()].some(key => key.startsWith('perCli.aside.') || key.startsWith('activeOverrides.aside.')) && asideSaveError) return Promise.reject(new Error(asideSaveError));
|
|
162
178
|
const bundle = Object.fromEntries(Object.entries(dirty.saveBundle()).filter(([key]) => ownsModelKey(key)));
|
|
163
179
|
if (Object.keys(bundle).length === 0) return Promise.resolve();
|
|
180
|
+
if (Object.keys(bundle).some(key => key.startsWith('perCli.aside.') || key.startsWith('activeOverrides.aside.')) && asideSaveError) return Promise.reject(new Error(asideSaveError));
|
|
181
|
+
if (bundle['fallbackOrder'] && (bundle['fallbackOrder'] as string[]).includes('aside')) return Promise.reject(new Error('Aside cannot be used as a fallback.'));
|
|
182
|
+
if (Object.hasOwn(bundle, 'perCli.aside.account')) {
|
|
183
|
+
bundle['activeOverrides.aside.model'] = '';
|
|
184
|
+
bundle['activeOverrides.aside.effort'] = '';
|
|
185
|
+
}
|
|
164
186
|
const patch = expandPatch(bundle);
|
|
165
187
|
const submitted = new Map([...dirty.pending].filter(([key]) => Object.hasOwn(bundle, key)));
|
|
166
188
|
setSaving(true); setSaveError(null);
|
|
@@ -182,7 +204,7 @@ export default function ModelProvider({ port, client, dirty, registerSave }: Set
|
|
|
182
204
|
});
|
|
183
205
|
activeOperation.current = operation;
|
|
184
206
|
return operation.promise;
|
|
185
|
-
}, [client, dirty, instance, loadCliMeta, refresh, setData]);
|
|
207
|
+
}, [client, dirty, instance, loadCliMeta, refresh, setData, asideSaveError]);
|
|
186
208
|
|
|
187
209
|
useEffect(() => {
|
|
188
210
|
if (!registerSave) return;
|
|
@@ -254,6 +276,8 @@ export default function ModelProvider({ port, client, dirty, registerSave }: Set
|
|
|
254
276
|
setValue={(next) => { if (canEdit()) setPerCliDraft({ ...perCliDraft, [cli]: next }); }}
|
|
255
277
|
setEntry={setEntry}
|
|
256
278
|
client={client}
|
|
279
|
+
aside={aside}
|
|
280
|
+
asideOverride={overrides[cli] || {}}
|
|
257
281
|
pi={piDraft}
|
|
258
282
|
setPi={(next) => { if (canEdit()) setPiDraft(next); }}
|
|
259
283
|
onPiRegistered={onPiRegistered}
|
|
@@ -270,6 +294,7 @@ export default function ModelProvider({ port, client, dirty, registerSave }: Set
|
|
|
270
294
|
id="model-fallbackOrder"
|
|
271
295
|
label="Fallback order"
|
|
272
296
|
error={(() => {
|
|
297
|
+
if (fallback.includes('aside')) return 'Aside is main-only. Remove it from the fallback order.';
|
|
273
298
|
const retired = fallback.find(isRetiredCliSelection);
|
|
274
299
|
if (!retired) return null;
|
|
275
300
|
const name = retiredRuntimeLabel(retired).replace(' (retired)', '');
|
|
@@ -283,7 +308,7 @@ export default function ModelProvider({ port, client, dirty, registerSave }: Set
|
|
|
283
308
|
setEntry('fallbackOrder', {
|
|
284
309
|
value: next,
|
|
285
310
|
original: data.fallbackOrder || [],
|
|
286
|
-
valid: !next.some(isRetiredCliSelection),
|
|
311
|
+
valid: !next.includes('aside') && !next.some(isRetiredCliSelection),
|
|
287
312
|
});
|
|
288
313
|
}}
|
|
289
314
|
placeholder="cli name"
|
|
@@ -118,8 +118,8 @@ export default function Profile({ port, client, dirty, registerSave }: SettingsP
|
|
|
118
118
|
</SettingsSection>
|
|
119
119
|
|
|
120
120
|
<SettingsSection title="Avatars" hint="Uploads write to the instance's home folder.">
|
|
121
|
-
<AvatarCard kind="agent"
|
|
122
|
-
<AvatarCard kind="user"
|
|
121
|
+
<AvatarCard kind="agent" client={client} />
|
|
122
|
+
<AvatarCard kind="user" client={client} />
|
|
123
123
|
</SettingsSection>
|
|
124
124
|
</form>
|
|
125
125
|
);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SelectField, TextField } from '../../fields';
|
|
2
|
+
import { asideEffortChoices, asideModelChoices, asideSelectionError, isAsideAccount, type AsideInventory } from './aside-models';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
id: string; account: string; host?: string | undefined; model: string; effort: string;
|
|
6
|
+
inventory: AsideInventory; disabled?: boolean;
|
|
7
|
+
onAccountChange(value: string): void; onModelChange(value: string): void; onEffortChange(value: string): void;
|
|
8
|
+
refresh(): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function AsideSelectionFields({ id, account, host, model, effort, inventory, disabled = false,
|
|
12
|
+
onAccountChange, onModelChange, onEffortChange, refresh }: Props) {
|
|
13
|
+
const models = asideModelChoices(inventory);
|
|
14
|
+
const efforts = asideEffortChoices(inventory, model);
|
|
15
|
+
const error = asideSelectionError(inventory, model, effort);
|
|
16
|
+
return <>
|
|
17
|
+
<p className="settings-percli-note">Experimental · local main agent only. Scheduled heartbeat runs are unsupported with Aside as the main runtime and will be rejected. Account selection does not confirm sign-in or model access.</p>
|
|
18
|
+
<TextField id={`${id}-account`} label="Aside account" value={account} placeholder="uN" disabled={disabled}
|
|
19
|
+
error={isAsideAccount(account) ? null : 'Enter an explicit account (uN).'} onChange={onAccountChange} />
|
|
20
|
+
<TextField id={`${id}-host`} label="Host" value={host || 'local'} disabled
|
|
21
|
+
error={host && host !== 'local' ? 'Only the local host is supported. Edit the account to use local.' : null} onChange={() => {}} />
|
|
22
|
+
<SelectField id={`${id}-model`} label="Model" value={model || 'default'} options={models}
|
|
23
|
+
missingValueLabel={`${model || 'default'} (unavailable)`} disabled={disabled || inventory.kind !== 'ready' || models.length === 0}
|
|
24
|
+
error={error} onChange={onModelChange} />
|
|
25
|
+
<SelectField id={`${id}-effort`} label="Effort" value={effort === 'default' ? '' : effort}
|
|
26
|
+
options={[{ value: '', label: '(default)' }, ...efforts.map(value => ({ value, label: value }))]}
|
|
27
|
+
missingValueLabel={`${effort} (unavailable)`} disabled={disabled || inventory.kind !== 'ready' || (efforts.length === 0 && !effort)}
|
|
28
|
+
error={error?.includes('effort') ? error : null} onChange={onEffortChange} />
|
|
29
|
+
{inventory.kind === 'ready' && inventory.catalog.status !== 'available'
|
|
30
|
+
? <p className="settings-percli-note" role="status">Catalog {inventory.catalog.status}{inventory.catalog.diagnostics.map(d => ` · ${d.code}`).join('')}</p> : null}
|
|
31
|
+
<button type="button" className="settings-action" disabled={disabled || !isAsideAccount(account)} onClick={refresh}>Refresh Aside models</button>
|
|
32
|
+
</>;
|
|
33
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
// Phase 2 — avatar upload card.
|
|
2
2
|
//
|
|
3
|
-
// Avatar
|
|
4
|
-
//
|
|
5
|
-
// `X-Filename`. We mount our own fetch + state instead of registering with the
|
|
6
|
-
// page-level dirty store: uploads are atomic side-effects, not pending edits.
|
|
3
|
+
// Avatar uploads use the configured settings transport with a raw image body.
|
|
4
|
+
// They remain atomic side-effects outside the page-level dirty store.
|
|
7
5
|
|
|
8
6
|
import { useEffect, useRef, useState } from 'react';
|
|
9
7
|
import { icon } from '../../../../../js/icons';
|
|
8
|
+
import type { SettingsClient } from '../../types';
|
|
10
9
|
|
|
11
10
|
type AvatarKind = 'agent' | 'user';
|
|
12
11
|
|
|
@@ -20,12 +19,12 @@ type EnvelopeMeta =
|
|
|
20
19
|
|
|
21
20
|
type Props = {
|
|
22
21
|
kind: AvatarKind;
|
|
23
|
-
|
|
22
|
+
client: SettingsClient;
|
|
24
23
|
};
|
|
25
24
|
|
|
26
25
|
const ACCEPTED_TYPES = 'image/png,image/jpeg,image/webp,image/gif';
|
|
27
26
|
|
|
28
|
-
export function AvatarCard({ kind,
|
|
27
|
+
export function AvatarCard({ kind, client }: Props) {
|
|
29
28
|
const [meta, setMeta] = useState<AvatarMeta | null>(null);
|
|
30
29
|
const [busy, setBusy] = useState(false);
|
|
31
30
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -35,9 +34,8 @@ export function AvatarCard({ kind, port }: Props) {
|
|
|
35
34
|
let cancelled = false;
|
|
36
35
|
setError(null);
|
|
37
36
|
setMeta(null);
|
|
38
|
-
|
|
39
|
-
.then((
|
|
40
|
-
.then((envelope: EnvelopeMeta) => {
|
|
37
|
+
client.get<EnvelopeMeta>('/api/avatar')
|
|
38
|
+
.then((envelope) => {
|
|
41
39
|
if (cancelled) return;
|
|
42
40
|
const inner = 'data' in envelope && envelope.data ? envelope.data : envelope;
|
|
43
41
|
const got = (inner as { agent?: AvatarMeta; user?: AvatarMeta })[kind] || null;
|
|
@@ -50,7 +48,7 @@ export function AvatarCard({ kind, port }: Props) {
|
|
|
50
48
|
return () => {
|
|
51
49
|
cancelled = true;
|
|
52
50
|
};
|
|
53
|
-
}, [kind,
|
|
51
|
+
}, [kind, client]);
|
|
54
52
|
|
|
55
53
|
const onPick = () => inputRef.current?.click();
|
|
56
54
|
|
|
@@ -60,21 +58,17 @@ export function AvatarCard({ kind, port }: Props) {
|
|
|
60
58
|
try {
|
|
61
59
|
const buf = await file.arrayBuffer();
|
|
62
60
|
const ct = file.type || 'application/octet-stream';
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
const envelope = await client.post<{ ok?: boolean; data?: AvatarMeta } | AvatarMeta>(
|
|
62
|
+
`/api/avatar/${kind}/upload`, undefined, {
|
|
63
|
+
headers: {
|
|
64
|
+
'content-type': ct,
|
|
65
|
+
'x-filename': encodeURIComponent(file.name),
|
|
66
|
+
},
|
|
67
|
+
body: buf,
|
|
68
|
+
// Image transfers may exceed the client's JSON request timeout.
|
|
69
|
+
signal: new AbortController().signal,
|
|
68
70
|
},
|
|
69
|
-
|
|
70
|
-
});
|
|
71
|
-
if (!response.ok) {
|
|
72
|
-
const text = await response.text().catch(() => '');
|
|
73
|
-
throw new Error(`upload failed: ${response.status} ${text}`);
|
|
74
|
-
}
|
|
75
|
-
const envelope = (await response.json()) as
|
|
76
|
-
| { ok?: boolean; data?: AvatarMeta }
|
|
77
|
-
| AvatarMeta;
|
|
71
|
+
);
|
|
78
72
|
const next: AvatarMeta = 'data' in envelope && envelope.data
|
|
79
73
|
? (envelope.data as AvatarMeta)
|
|
80
74
|
: (envelope as AvatarMeta);
|
|
@@ -91,15 +85,9 @@ export function AvatarCard({ kind, port }: Props) {
|
|
|
91
85
|
setBusy(true);
|
|
92
86
|
setError(null);
|
|
93
87
|
try {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!response.ok) {
|
|
98
|
-
throw new Error(`clear failed: ${response.status}`);
|
|
99
|
-
}
|
|
100
|
-
const envelope = (await response.json()) as
|
|
101
|
-
| { ok?: boolean; data?: AvatarMeta }
|
|
102
|
-
| AvatarMeta;
|
|
88
|
+
const envelope = await client.delete<{ ok?: boolean; data?: AvatarMeta } | AvatarMeta>(
|
|
89
|
+
`/api/avatar/${kind}/image`,
|
|
90
|
+
);
|
|
103
91
|
const next: AvatarMeta = 'data' in envelope && envelope.data
|
|
104
92
|
? (envelope.data as AvatarMeta)
|
|
105
93
|
: (envelope as AvatarMeta);
|
|
@@ -112,7 +100,7 @@ export function AvatarCard({ kind, port }: Props) {
|
|
|
112
100
|
};
|
|
113
101
|
|
|
114
102
|
const imageUrl = meta && meta.kind === 'image'
|
|
115
|
-
?
|
|
103
|
+
? client.url(meta.imageUrl)
|
|
116
104
|
: null;
|
|
117
105
|
const label = kind === 'agent' ? 'Agent avatar' : 'User avatar';
|
|
118
106
|
|
|
@@ -10,6 +10,8 @@ import { coerceEffortForModel, effortChoicesForModel } from './agent/agent-meta'
|
|
|
10
10
|
import type { CliMeta, PerCliEntry } from './agent/agent-meta';
|
|
11
11
|
import { PiProfileDialog } from './PiProfileDialog';
|
|
12
12
|
import { piModelOptions, piProfileOptions, type PiSettingsView } from './pi-profile';
|
|
13
|
+
import { AsideSelectionFields } from './AsideSelectionFields';
|
|
14
|
+
import { type AsideInventory, isAsideAccount } from './aside-models';
|
|
13
15
|
import { hasRuntimeTransportChoice, RuntimeTransportField } from './runtime-transport-field';
|
|
14
16
|
export type PiRegistration = Parameters<ComponentProps<typeof PiProfileDialog>['onRegistered']>[0];
|
|
15
17
|
|
|
@@ -23,6 +25,8 @@ type Props = {
|
|
|
23
25
|
dirty: DirtyStore;
|
|
24
26
|
disabled?: boolean;
|
|
25
27
|
client?: SettingsClient;
|
|
28
|
+
aside?: { inventory: AsideInventory; refresh(): void };
|
|
29
|
+
asideOverride?: { model?: string; effort?: string };
|
|
26
30
|
pi?: PiSettingsView | undefined;
|
|
27
31
|
setPi?: (next: PiSettingsView) => void;
|
|
28
32
|
onPiRegistered?: (next: PiRegistration) => void;
|
|
@@ -32,7 +36,7 @@ function entryFor(value: unknown, original: unknown, valid = true): DirtyEntry {
|
|
|
32
36
|
return { value, original, valid };
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
export function PerCliRow({ cli, meta, original, value, setValue, setEntry, dirty, disabled = false, client, pi, setPi, onPiRegistered }: Props) {
|
|
39
|
+
export function PerCliRow({ cli, meta, original, value, setValue, setEntry, dirty, disabled = false, client, aside, asideOverride, pi, setPi, onPiRegistered }: Props) {
|
|
36
40
|
const modelDatalistId = `percli-${cli}-models`;
|
|
37
41
|
const isPi = cli === 'pi';
|
|
38
42
|
const [piDialogOpen, setPiDialogOpen] = useState(false);
|
|
@@ -43,6 +47,30 @@ export function PerCliRow({ cli, meta, original, value, setValue, setEntry, dirt
|
|
|
43
47
|
blocked.current = disabled;
|
|
44
48
|
return () => { blocked.current = true; };
|
|
45
49
|
}, [disabled]);
|
|
50
|
+
if (cli === 'aside' && aside) return <div className="settings-percli-row" data-cli={cli}>
|
|
51
|
+
<SettingsToolbar><strong className="settings-percli-title">Aside</strong></SettingsToolbar>
|
|
52
|
+
<AsideSelectionFields id="percli-aside" account={value.account || ''} host={value.host}
|
|
53
|
+
model={value.model || ''} effort={value.effort || ''} inventory={aside.inventory} disabled={disabled}
|
|
54
|
+
refresh={aside.refresh}
|
|
55
|
+
onAccountChange={next => {
|
|
56
|
+
if (blocked.current) return;
|
|
57
|
+
setValue({ ...value, account: next, host: 'local' });
|
|
58
|
+
setEntry('perCli.aside.account', entryFor(next, original.account || '', isAsideAccount(next)));
|
|
59
|
+
setEntry('perCli.aside.host', entryFor('local', original.host || ''));
|
|
60
|
+
setEntry('activeOverrides.aside.model', entryFor('', asideOverride?.model || ''));
|
|
61
|
+
setEntry('activeOverrides.aside.effort', entryFor('', asideOverride?.effort || ''));
|
|
62
|
+
}}
|
|
63
|
+
onModelChange={next => {
|
|
64
|
+
if (blocked.current) return;
|
|
65
|
+
setValue({ ...value, model: next });
|
|
66
|
+
setEntry('perCli.aside.model', entryFor(next, original.model || ''));
|
|
67
|
+
}}
|
|
68
|
+
onEffortChange={next => {
|
|
69
|
+
if (blocked.current) return;
|
|
70
|
+
setValue({ ...value, effort: next });
|
|
71
|
+
setEntry('perCli.aside.effort', entryFor(next, original.effort || ''));
|
|
72
|
+
}} />
|
|
73
|
+
</div>;
|
|
46
74
|
const provider = value.provider || meta.defaultProvider || meta.providers?.[0] || 'claude';
|
|
47
75
|
const piModels = isPi ? piModelOptions(pi, provider, value.model || '') : [];
|
|
48
76
|
const hasProviders = !isPi && (meta.providers?.length ?? 0) > 0;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SelectField } from '../../../fields';
|
|
2
2
|
import { SettingsSection } from '../../page-shell';
|
|
3
|
-
import { metaFor,
|
|
3
|
+
import { metaFor, auxiliaryRuntimeOptions, isRetiredCliSelection, retiredRuntimeLabel, type CliMeta } from './agent-meta';
|
|
4
4
|
|
|
5
5
|
type FlushAgentSectionProps = {
|
|
6
6
|
activeCli: string;
|
|
@@ -28,6 +28,7 @@ export function FlushAgentSection({
|
|
|
28
28
|
onFlushModelChange,
|
|
29
29
|
}: FlushAgentSectionProps) {
|
|
30
30
|
const effectiveCli = flushCli || activeCli;
|
|
31
|
+
const unsupported = effectiveCli === 'aside';
|
|
31
32
|
const effectiveCliLabel = effectiveCli ? metaFor(effectiveCli, cliMeta).label || effectiveCli : 'active';
|
|
32
33
|
return (
|
|
33
34
|
<SettingsSection
|
|
@@ -46,11 +47,11 @@ export function FlushAgentSection({
|
|
|
46
47
|
id="agent-flush-cli"
|
|
47
48
|
label="Flush CLI"
|
|
48
49
|
value={flushCli}
|
|
49
|
-
missingValueLabel={isRetiredCliSelection(flushCli) ? retiredRuntimeLabel(flushCli) : undefined}
|
|
50
|
-
error={isRetiredCliSelection(effectiveCli) ? 'The saved flush runtime is retired. Choose an available runtime.' : null}
|
|
50
|
+
missingValueLabel={!flushCli && activeCli === 'aside' ? '(active CLI unavailable)' : flushCli === 'aside' ? 'Aside (main-only)' : isRetiredCliSelection(flushCli) ? retiredRuntimeLabel(flushCli) : undefined}
|
|
51
|
+
error={unsupported ? 'Aside cannot run memory flush. Choose a separate runtime.' : isRetiredCliSelection(effectiveCli) ? 'The saved flush runtime is retired. Choose an available runtime.' : null}
|
|
51
52
|
options={[
|
|
52
|
-
{ value: '', label: '(active CLI)' },
|
|
53
|
-
...
|
|
53
|
+
...(activeCli === 'aside' ? [] : [{ value: '', label: '(active CLI)' }]),
|
|
54
|
+
...auxiliaryRuntimeOptions(cliOptions).map((value) => ({ value, label: metaFor(value, cliMeta).label || value })),
|
|
54
55
|
]}
|
|
55
56
|
onChange={onFlushCliChange}
|
|
56
57
|
/>
|
|
@@ -58,7 +59,7 @@ export function FlushAgentSection({
|
|
|
58
59
|
id="agent-flush-model"
|
|
59
60
|
label="Flush model"
|
|
60
61
|
value={flushModel}
|
|
61
|
-
disabled={isRetiredCliSelection(effectiveCli)}
|
|
62
|
+
disabled={unsupported || isRetiredCliSelection(effectiveCli)}
|
|
62
63
|
options={[{ value: '', label: '(default)' }, ...modelOptions]}
|
|
63
64
|
onChange={onFlushModelChange}
|
|
64
65
|
/>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SelectField, TextField } from '../../../fields';
|
|
2
2
|
import { SettingsActions, StatusBadge } from '../../page-shell';
|
|
3
|
-
import { metaFor, optionList,
|
|
3
|
+
import { metaFor, optionList, auxiliaryRuntimeOptions, isRetiredCliSelection, retiredRuntimeLabel, type CliMeta } from './agent-meta';
|
|
4
4
|
import {
|
|
5
5
|
isStaticEmployee,
|
|
6
6
|
runtimeEmployeeError,
|
|
@@ -28,8 +28,8 @@ export function RuntimeEmployeeRow({
|
|
|
28
28
|
const meta = metaFor(employee.cli, cliMeta);
|
|
29
29
|
const modelOptions = optionList(meta.models, employee.model);
|
|
30
30
|
const error = runtimeEmployeeError(employee);
|
|
31
|
-
const retired = isRetiredCliSelection(employee.cli);
|
|
32
|
-
const cliChoices =
|
|
31
|
+
const retired = isRetiredCliSelection(employee.cli) || employee.cli === 'aside';
|
|
32
|
+
const cliChoices = auxiliaryRuntimeOptions(Array.from(new Set([...cliOptions, employee.cli, 'claude'])).filter(Boolean));
|
|
33
33
|
|
|
34
34
|
const employeeName = employee.name || `Employee ${index + 1}`;
|
|
35
35
|
return (
|
|
@@ -51,7 +51,7 @@ export function RuntimeEmployeeRow({
|
|
|
51
51
|
id={`runtime-employee-${employee.id}-cli`}
|
|
52
52
|
label="CLI"
|
|
53
53
|
value={employee.cli}
|
|
54
|
-
missingValueLabel={isRetiredCliSelection(employee.cli) ? retiredRuntimeLabel(employee.cli) : undefined}
|
|
54
|
+
missingValueLabel={employee.cli === 'aside' ? 'Aside (main-only)' : isRetiredCliSelection(employee.cli) ? retiredRuntimeLabel(employee.cli) : undefined}
|
|
55
55
|
error={retired ? 'This saved runtime cannot execute. Choose an available runtime.' : null}
|
|
56
56
|
disabled={locked}
|
|
57
57
|
options={cliChoices.map((value) => ({ value, label: metaFor(value, cliMeta).label || value }))}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import { SelectField, TextField } from '../../../fields';
|
|
2
3
|
import { SettingsSection } from '../../page-shell';
|
|
3
4
|
import { metaFor, orderRuntimeCliOptions, PRIMARY_CLIS, isRetiredCliSelection, retiredRuntimeLabel, type CliMeta } from './agent-meta';
|
|
4
5
|
|
|
5
6
|
type RuntimeHeaderProps = {
|
|
7
|
+
asideFields?: ReactNode;
|
|
6
8
|
cli: string;
|
|
7
9
|
cliOptions: ReadonlyArray<string>;
|
|
8
10
|
provider?: string;
|
|
@@ -22,6 +24,7 @@ type RuntimeHeaderProps = {
|
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export function RuntimeHeader({
|
|
27
|
+
asideFields,
|
|
25
28
|
cli,
|
|
26
29
|
cliOptions,
|
|
27
30
|
provider = '',
|
|
@@ -58,6 +61,7 @@ export function RuntimeHeader({
|
|
|
58
61
|
collapsedAfter={orderedPrimaryCliCount}
|
|
59
62
|
onChange={onCliChange}
|
|
60
63
|
/>
|
|
64
|
+
{asideFields || <>
|
|
61
65
|
{providerOptions.length > 0 ? (
|
|
62
66
|
<SelectField
|
|
63
67
|
id={`agent-${cli}-provider`}
|
|
@@ -86,6 +90,7 @@ export function RuntimeHeader({
|
|
|
86
90
|
disabled={retired || effortOptions.length === 0}
|
|
87
91
|
onChange={onEffortChange}
|
|
88
92
|
/>
|
|
93
|
+
</>}
|
|
89
94
|
<TextField
|
|
90
95
|
id="agent-workingDir"
|
|
91
96
|
label="Working directory"
|
|
@@ -31,6 +31,8 @@ export type CliMeta = {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
export type PerCliEntry = {
|
|
34
|
+
account?: string;
|
|
35
|
+
host?: string;
|
|
34
36
|
transport?: RuntimeTransport;
|
|
35
37
|
provider?: string;
|
|
36
38
|
model?: string;
|
|
@@ -50,6 +52,7 @@ const CODEX_MODELS: ReadonlyArray<string> = ['gpt-5.5', 'gpt-5.4', 'gpt-5.4-mini
|
|
|
50
52
|
export const PRIMARY_CLIS: ReadonlyArray<string> = ['pi', 'claude', 'agy', 'codex', 'cursor', 'kiro-code', 'gemini'];
|
|
51
53
|
|
|
52
54
|
export const CLI_META: Record<string, CliMeta> = {
|
|
55
|
+
aside: { label: 'Aside (experimental)', models: [], efforts: [] },
|
|
53
56
|
agy: {
|
|
54
57
|
label: 'Antigravity',
|
|
55
58
|
// Label form is what `agy --model` accepts when no --effort is sent
|
|
@@ -316,6 +319,10 @@ export function selectableRuntimeOptions(cliOptions: ReadonlyArray<string>): str
|
|
|
316
319
|
return cliOptions.filter(value => !isRetiredCliSelection(value));
|
|
317
320
|
}
|
|
318
321
|
|
|
322
|
+
export function auxiliaryRuntimeOptions(cliOptions: ReadonlyArray<string>): string[] {
|
|
323
|
+
return selectableRuntimeOptions(cliOptions).filter(cli => cli !== 'aside');
|
|
324
|
+
}
|
|
325
|
+
|
|
319
326
|
export function orderRuntimeCliOptions(input: ReadonlyArray<string>): string[] {
|
|
320
327
|
const cliOptions = selectableRuntimeOptions(input);
|
|
321
328
|
const primary = PRIMARY_CLIS.filter((value) => cliOptions.includes(value));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SettingsClient } from '../../../types';
|
|
2
|
-
import {
|
|
2
|
+
import { auxiliaryRuntimeOptions, type CliMeta } from './agent-meta';
|
|
3
3
|
|
|
4
4
|
export type RuntimeEmployeeSource = 'static' | 'db';
|
|
5
5
|
|
|
@@ -42,7 +42,7 @@ export function makeDefaultRuntimeEmployee(
|
|
|
42
42
|
cliOptions: ReadonlyArray<string>,
|
|
43
43
|
cliMeta?: Record<string, CliMeta> | null,
|
|
44
44
|
): RuntimeEmployeeRecord {
|
|
45
|
-
const cli =
|
|
45
|
+
const cli = auxiliaryRuntimeOptions(cliOptions)[0] || 'claude';
|
|
46
46
|
const model = cliMeta?.[cli]?.models?.[0] || 'default';
|
|
47
47
|
return {
|
|
48
48
|
id: newRuntimeEmployeeId(),
|
|
@@ -81,6 +81,7 @@ export function normalizeRuntimeEmployee(value: unknown): RuntimeEmployeeRecord
|
|
|
81
81
|
|
|
82
82
|
export function runtimeEmployeeError(row: RuntimeEmployeeRecord): string | null {
|
|
83
83
|
if (!row.name.trim()) return 'Name is required';
|
|
84
|
+
if (row.cli === 'aside') return 'Aside supports the main agent only';
|
|
84
85
|
if (!row.cli.trim()) return 'CLI is required';
|
|
85
86
|
if (!row.model.trim()) return 'Model is required';
|
|
86
87
|
return null;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import type { SettingsClient } from '../../types';
|
|
3
|
+
import type { AsideCatalog, AsideModelEntry, AsideThinkingLevel } from '../../../../../../src/shared/aside-contract';
|
|
4
|
+
|
|
5
|
+
export const isAsideAccount = (account: string) => /^u(?:0|[1-9][0-9]{0,8})$/.test(account);
|
|
6
|
+
const levels: readonly string[] = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
7
|
+
const record = (value: unknown): value is Record<string, unknown> => !!value && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
const qualified = (value: unknown): value is string => typeof value === 'string' && /^[^/\s]+\/.+/.test(value);
|
|
9
|
+
|
|
10
|
+
/** SettingsClient returns raw JSON. Reject mismatched context and malformed wire data. */
|
|
11
|
+
export function unwrapAsideCatalog(raw: unknown, account: string): AsideCatalog {
|
|
12
|
+
if (!record(raw) || raw['ok'] !== true || !record(raw['data'])) throw new Error('Aside catalog response is unavailable.');
|
|
13
|
+
const data = raw['data'];
|
|
14
|
+
const context = data['context'];
|
|
15
|
+
if (!record(context) || context['account'] !== account || context['host'] !== 'local'
|
|
16
|
+
|| data['source'] !== 'local-files' || !['available', 'partial', 'unavailable'].includes(String(data['status']))
|
|
17
|
+
|| !Array.isArray(data['entries']) || !Array.isArray(data['cachedIds']) || !Array.isArray(data['diagnostics'])
|
|
18
|
+
|| (data['defaultModel'] !== null && !qualified(data['defaultModel']))) throw new Error('Invalid Aside catalog response.');
|
|
19
|
+
const seen = new Set<string>();
|
|
20
|
+
for (const entry of data['entries']) {
|
|
21
|
+
if (!record(entry) || !qualified(entry['id']) || typeof entry['provider'] !== 'string'
|
|
22
|
+
|| typeof entry['modelId'] !== 'string' || entry['id'] !== `${entry['provider']}/${entry['modelId']}`
|
|
23
|
+
|| typeof entry['name'] !== 'string' || !['registered', 'unknown'].includes(String(entry['capability']))
|
|
24
|
+
|| !Array.isArray(entry['efforts']) || !entry['efforts'].every(e => typeof e === 'string' && levels.includes(e))
|
|
25
|
+
|| !record(entry['thinkingLevelMap']) || seen.has(entry['id'])) throw new Error('Invalid Aside model catalog.');
|
|
26
|
+
seen.add(entry['id']);
|
|
27
|
+
}
|
|
28
|
+
const preference = data['configuredDefault'];
|
|
29
|
+
if (preference !== null && (!record(preference) || typeof preference['provider'] !== 'string'
|
|
30
|
+
|| typeof preference['modelId'] !== 'string'
|
|
31
|
+
|| (preference['thinkingLevel'] !== undefined && !levels.includes(String(preference['thinkingLevel'])))))
|
|
32
|
+
throw new Error('Invalid Aside default model.');
|
|
33
|
+
if (data['defaultModel'] !== null && (!record(preference)
|
|
34
|
+
|| data['defaultModel'] !== `${preference['provider']}/${preference['modelId']}`)) throw new Error('Invalid Aside default model.');
|
|
35
|
+
for (const diagnostic of data['diagnostics']) {
|
|
36
|
+
if (!record(diagnostic) || !['context', 'models', 'settings'].includes(String(diagnostic['source']))
|
|
37
|
+
|| typeof diagnostic['code'] !== 'string' || typeof diagnostic['message'] !== 'string') throw new Error('Invalid Aside diagnostics.');
|
|
38
|
+
}
|
|
39
|
+
return data as unknown as AsideCatalog;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type AsideInventory = { kind: 'loading' } | { kind: 'error'; message: string } | { kind: 'ready'; catalog: AsideCatalog };
|
|
43
|
+
type Context = { client: SettingsClient; instanceUrl: string; port: number; account: string; enabled: boolean; revision: number };
|
|
44
|
+
|
|
45
|
+
export function useAsideModels(client: SettingsClient, instanceUrl: string, port: number, account: string, enabled = true) {
|
|
46
|
+
const [revision, setRevision] = useState(0);
|
|
47
|
+
const context = useMemo<Context>(() => ({ client, instanceUrl, port, account, enabled, revision }), [client, instanceUrl, port, account, enabled, revision]);
|
|
48
|
+
const generation = useRef(0);
|
|
49
|
+
const [result, setResult] = useState<{ context: Context; value: AsideInventory } | null>(null);
|
|
50
|
+
useLayoutEffect(() => {
|
|
51
|
+
const epoch = ++generation.current;
|
|
52
|
+
let live = true;
|
|
53
|
+
if (enabled && isAsideAccount(account)) {
|
|
54
|
+
void client.get<unknown>(`/api/aside/models?account=${encodeURIComponent(account)}&host=local`).then(raw => {
|
|
55
|
+
if (!live || epoch !== generation.current) return;
|
|
56
|
+
setResult({ context, value: { kind: 'ready', catalog: unwrapAsideCatalog(raw, account) } });
|
|
57
|
+
}).catch(() => {
|
|
58
|
+
if (live && epoch === generation.current) setResult({ context, value: { kind: 'error', message: 'Could not read the local Aside catalog. Refresh to try again.' } });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return () => { live = false; ++generation.current; };
|
|
62
|
+
}, [context, client, account, enabled]);
|
|
63
|
+
// Hide old inventory in the first render of a changed context, before effects.
|
|
64
|
+
const inventory: AsideInventory = !isAsideAccount(account)
|
|
65
|
+
? { kind: 'error', message: 'Enter an explicit Aside account (uN).' }
|
|
66
|
+
: result?.context === context ? result.value : { kind: 'loading' };
|
|
67
|
+
const refresh = useCallback(() => setRevision(value => value + 1), []);
|
|
68
|
+
return { inventory, refresh };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function asideModelChoices(inventory: AsideInventory): Array<{ value: string; label: string }> {
|
|
72
|
+
if (inventory.kind !== 'ready') return [];
|
|
73
|
+
const catalog = inventory.catalog;
|
|
74
|
+
if (catalog.status === 'unavailable' || catalog.diagnostics.some(d => d.source === 'models' || d.source === 'context')) return [];
|
|
75
|
+
const ids = new Set(catalog.entries.map(e => e.id));
|
|
76
|
+
if (catalog.defaultModel) ids.add(catalog.defaultModel);
|
|
77
|
+
const defaultEntry = catalog.entries.find(e => e.id === catalog.defaultModel);
|
|
78
|
+
const level = catalog.configuredDefault?.thinkingLevel;
|
|
79
|
+
const defaultReady = !!catalog.defaultModel && (!level || defaultEntry?.capability !== 'registered' || defaultEntry.efforts.includes(level));
|
|
80
|
+
return [
|
|
81
|
+
...(defaultReady ? [{ value: 'default', label: `Default (${catalog.defaultModel})` }] : []),
|
|
82
|
+
...Array.from(ids, value => ({ value, label: value })),
|
|
83
|
+
];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function asideEffortChoices(inventory: AsideInventory, model: string): readonly string[] {
|
|
87
|
+
if (inventory.kind !== 'ready' || !asideModelChoices(inventory).some(e => e.value === (model || 'default'))) return [];
|
|
88
|
+
const catalog = inventory.catalog;
|
|
89
|
+
const concrete = !model || model === 'default' ? catalog.defaultModel : model;
|
|
90
|
+
const entry: AsideModelEntry | undefined = catalog.entries.find(e => e.id === concrete && e.capability === 'registered');
|
|
91
|
+
if (entry) return entry.efforts;
|
|
92
|
+
return concrete === catalog.defaultModel && catalog.configuredDefault?.thinkingLevel ? [catalog.configuredDefault.thinkingLevel] : [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function asideSelectionError(inventory: AsideInventory, model: string, effort: string): string | null {
|
|
96
|
+
if (inventory.kind === 'loading') return 'Loading Aside models…';
|
|
97
|
+
if (inventory.kind === 'error') return inventory.message;
|
|
98
|
+
if (!asideModelChoices(inventory).some(e => e.value === (model || 'default'))) return 'This model is unavailable for the selected Aside account.';
|
|
99
|
+
const concrete = !model || model === 'default' ? inventory.catalog.defaultModel : model;
|
|
100
|
+
const effectiveEffort = effort && effort !== 'default' ? effort
|
|
101
|
+
: concrete === inventory.catalog.defaultModel ? inventory.catalog.configuredDefault?.thinkingLevel : undefined;
|
|
102
|
+
if (effectiveEffort && !asideEffortChoices(inventory, model).includes(effectiveEffort as AsideThinkingLevel)) return 'This effort is unavailable for the selected Aside model.';
|
|
103
|
+
return null;
|
|
104
|
+
}
|