talos-code 0.1.0 → 0.2.1
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/CHANGELOG.md +112 -0
- package/README.md +9 -4
- package/dist/args.js +4 -1
- package/dist/commands/provider-cli.js +146 -87
- package/dist/config/types.js +1 -1
- package/dist/i18n/en/approval.js +3 -0
- package/dist/i18n/en/credentials.js +10 -0
- package/dist/i18n/en/errors.js +21 -0
- package/dist/i18n/en/firstrun.js +12 -0
- package/dist/i18n/en/screen.js +38 -0
- package/dist/i18n/error-view.js +1 -1
- package/dist/main.js +2 -1
- package/dist/provider/openrouter-login.js +172 -0
- package/dist/runtime/context-archive.js +69 -0
- package/dist/runtime/repo.js +11 -0
- package/dist/runtime/supervisor.js +5 -2
- package/dist/runtime/talos-composition.js +9 -3
- package/dist/tui/agent-view.js +51 -0
- package/dist/tui/app.js +364 -27
- package/dist/tui/catalog-service.js +2 -1
- package/dist/tui/components/status-indicator.js +10 -4
- package/dist/tui/components/terminal-shell.js +10 -1
- package/dist/tui/descendant-approvals.js +104 -0
- package/dist/tui/launch-state.js +9 -0
- package/dist/tui/live-activity.js +13 -2
- package/dist/tui/overlays/agent-tree.js +7 -3
- package/dist/tui/overlays/approval-dialog.js +3 -1
- package/dist/tui/overlays/model-picker.js +5 -0
- package/dist/tui/overlays/provider-picker.js +26 -0
- package/dist/tui/project-trust-prompt.js +5 -3
- package/dist/tui/session-controller.js +25 -12
- package/dist/tui/setup-wizard.js +46 -0
- package/dist/tui/slash-commands.js +3 -1
- package/dist/tui/theme-catalog.js +3 -1
- package/dist/version.js +1 -1
- package/dist/workspace/checkpoint-store.js +32 -1
- package/dist/workspace/checkpoint.js +40 -12
- package/package.json +3 -1
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +149 -2
- package/vendor/manifest.json +2 -2
package/dist/tui/app.js
CHANGED
|
@@ -44,9 +44,10 @@ import { APPROVAL_DEFAULT_SELECTED, approvalDialogFit, approvalDialogModel, appr
|
|
|
44
44
|
import { fitOverlay, renderFittedOverlay, renderOverlay } from "./overlays/scroll-window.js";
|
|
45
45
|
import { commitModelSelection, createModelPickerModel, modelPickerOverlay } from "./overlays/model-picker.js";
|
|
46
46
|
import { effortLineOverlay, orderedEfforts } from "./overlays/effort-line.js";
|
|
47
|
-
import { consentQuestion, createProviderPickerModel, maskedSecret, providerConsentOverlay, providerKeyOverlay, providerKeySourceLabel, providerPickerEnter, providerPickerItems, providerPickerOverlay } from "./overlays/provider-picker.js";
|
|
47
|
+
import { consentQuestion, createProviderPickerModel, maskedSecret, providerConsentOverlay, providerKeyOverlay, providerKeySourceLabel, providerPickerEnter, providerPickerItems, providerPickerOverlay, providerLoginChoiceOverlay, providerOAuthOverlay } from "./overlays/provider-picker.js";
|
|
48
48
|
import { createSessionPickerModel, filterSessions, sessionPickerOverlay } from "./overlays/session-picker.js";
|
|
49
49
|
import { agentTreeOverlay as agentTreeOverlayContent, createAgentTreeOverlayModel, moveAgentTreeSelection, selectedAgentRow } from "./overlays/agent-tree.js";
|
|
50
|
+
import { createAgentView } from "./agent-view.js";
|
|
50
51
|
import { agentRosterRows } from "./agent-roster.js";
|
|
51
52
|
import { helpDialogOverlay } from "./overlays/help-dialog.js";
|
|
52
53
|
import { queueEditorOverlay, queueEditorSelected } from "./overlays/queue-editor.js";
|
|
@@ -81,6 +82,7 @@ import { parseSlashCommand, SLASH_COMMANDS } from "./slash-commands.js";
|
|
|
81
82
|
import { initialTuiState, reasoningUsageLabel, reduceTuiEvent } from "./state.js";
|
|
82
83
|
import { appendTranscriptMessage, appendTranscriptWarning, createTranscriptModel, transcriptItemRaw, transcriptItemText } from "./transcript-model.js";
|
|
83
84
|
import { createTuiTheme, themeColor } from "./theme.js";
|
|
85
|
+
import { DEFAULT_THEME_ACCENT } from "./theme-catalog.js";
|
|
84
86
|
import { THEME_CHOICES, createThemeStore, parseThemeChoice } from "./theme-store.js";
|
|
85
87
|
import { themeLabel, themePickerOverlay } from "./overlays/theme-picker.js";
|
|
86
88
|
import { checkpointFilesText, rangeFiles, undoPickerOverlay, undoRange } from "./overlays/checkpoint-picker.js";
|
|
@@ -99,6 +101,7 @@ import { decideShellInput } from "./shell-input.js";
|
|
|
99
101
|
import { createRenderCoordinator } from "./render-coordinator.js";
|
|
100
102
|
import { MOTION_FRAME_MS, frameIndex } from "./render-scheduler.js";
|
|
101
103
|
import { onboardingSteps } from "./onboarding.js";
|
|
104
|
+
import { setupEndLines, setupOverlay, setupStart } from "./setup-wizard.js";
|
|
102
105
|
import { createVimState, handleVimInput, toggleVim, vimModeHint } from "./vim-mode.js";
|
|
103
106
|
export { closeOverlay, createTuiAppState, permissionDryRunSlash, queuedActionPreview, reduceAppAction, slashCommandIntent, slashCommandToCliArgs, togglePlanMode } from "./shell-model.js";
|
|
104
107
|
const execFileAsync = promisify(execFile);
|
|
@@ -141,6 +144,8 @@ export function reasoningEffortChoices(model) {
|
|
|
141
144
|
export async function selectExplicitModel(id, catalog, setControllerModel) { await commitModelSelection({ id, catalog, setControllerModel }); }
|
|
142
145
|
export async function runControllerSlashAction(name, controller) { if (name !== 'compact')
|
|
143
146
|
return null; const result = await controller.compact(); return result?.compattato === false ? t('screen.compact.already') : t('screen.compact.done'); }
|
|
147
|
+
/* P13: what still works while an agent is watched (the rest of the keys do nothing; Esc goes back). */
|
|
148
|
+
const AGENT_VIEW_ACTIONS = new Set(['interrupt', 'exit', 'agent-tree', 'page-up', 'page-down', 'transcript-top', 'transcript-bottom', 'redraw', 'reasoning-toggle', 'tool-details', 'help']);
|
|
144
149
|
function localMessage(state, text) { const id = `local-${Date.now()}-${Math.random()}`; const base = state.messages.length >= 511 ? state.messages.slice(-511) : state.messages; return { ...state, messages: [...base, { id, role: 'assistant', text }], transcript: appendTranscriptWarning(state.transcript, text) }; }
|
|
145
150
|
function userMessage(state, text, id = `user-${Date.now()}-${Math.random()}`) { const base = state.messages.length >= 511 ? state.messages.slice(-511) : state.messages; return { ...state, messages: [...base, { id, role: 'user', text }], transcript: appendTranscriptMessage(state.transcript, { role: 'user', text }) }; }
|
|
146
151
|
/* R5b D4 (Q-R5-14): the theme is live. run.ts hands the app the store it paints the background from; without one (tests,
|
|
@@ -148,7 +153,7 @@ function userMessage(state, text, id = `user-${Date.now()}-${Math.random()}`) {
|
|
|
148
153
|
at every render, and a change of theme remounts the main tree (its key), so memoised rows are drawn again. */
|
|
149
154
|
export function createTuiAppComponent(React, Ink, baseCapabilities = { interactive: true, color: true, motion: true, unicode: true }, accentId, givenThemeStore) {
|
|
150
155
|
const h = React.createElement;
|
|
151
|
-
const themeStore = givenThemeStore ?? createThemeStore(baseCapabilities, accentId ??
|
|
156
|
+
const themeStore = givenThemeStore ?? createThemeStore(baseCapabilities, accentId ?? DEFAULT_THEME_ACCENT);
|
|
152
157
|
let capabilities = baseCapabilities, theme = createTuiTheme(capabilities, accentId);
|
|
153
158
|
let bootColor, transcriptColor, pickerColor, approvalColor, toolColor, footerColor;
|
|
154
159
|
const applyLive = (live) => {
|
|
@@ -356,11 +361,26 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
356
361
|
const [sessionRows, setSessionRows] = React.useState([]);
|
|
357
362
|
const [agentRows, setAgentRows] = React.useState([]);
|
|
358
363
|
const [agentTreeOverlay, setAgentTreeOverlay] = React.useState(null);
|
|
364
|
+
/* P13 (owner decisions 2026-09-26): the agent whose transcript takes the main view (agent-view.ts), read-only. */
|
|
365
|
+
const [agentView, setAgentView] = React.useState(null);
|
|
366
|
+
const agentViewRef = React.useRef(null);
|
|
367
|
+
const mainViewportRef = React.useRef(null);
|
|
359
368
|
const [providerStep, setProviderStep] = React.useState({ kind: 'list' });
|
|
360
369
|
const [chosenProvider, setChosenProvider] = React.useState(null);
|
|
361
370
|
const [selectionLoaded, setSelectionLoaded] = React.useState(false);
|
|
362
371
|
const [keyMissing, setKeyMissing] = React.useState(false);
|
|
363
|
-
const [modelList, setModelList] = React.useState({ loading: false, verified: false, notice: '', label: '' });
|
|
372
|
+
const [modelList, setModelList] = React.useState({ loading: false, verified: false, notice: '', label: '', engineDown: false });
|
|
373
|
+
/* P17 (owner decisions 2026-09-26): the guided setup — first launch when something is missing, /setup, `talos setup`.
|
|
374
|
+
It chains the provider, key and model windows that exist; `setupTransitionRef` holds the setup open while one window
|
|
375
|
+
hands over to the next (the provider window closes, the model window opens). */
|
|
376
|
+
const [setupActive, setSetupActive] = React.useState(false);
|
|
377
|
+
const setupRef = React.useRef(false);
|
|
378
|
+
const setupTransitionRef = React.useRef(false);
|
|
379
|
+
/* A local engine chosen in the setup that did not answer: its model window says so (R retries, Esc goes back), and the
|
|
380
|
+
engine becomes the chosen provider only when one of its models is chosen. */
|
|
381
|
+
const setupPendingProviderRef = React.useRef(null);
|
|
382
|
+
/* P21: the OpenRouter sign-in under way (its loopback port closes on cancel, timeout or success). */
|
|
383
|
+
const oauthLoginRef = React.useRef(null);
|
|
364
384
|
const [providerSecret, setProviderSecret] = React.useState('');
|
|
365
385
|
const [auxCompletions, setAuxCompletions] = React.useState([]);
|
|
366
386
|
const [fileCompletion, setFileCompletion] = React.useState(null);
|
|
@@ -575,6 +595,26 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
575
595
|
}
|
|
576
596
|
}
|
|
577
597
|
React.useEffect(() => { void refreshContextStatus(); }, [state.sessionId, state.running]);
|
|
598
|
+
function closeAgentView() {
|
|
599
|
+
const handle = agentViewRef.current;
|
|
600
|
+
agentViewRef.current = null;
|
|
601
|
+
handle?.close();
|
|
602
|
+
setAgentView(null);
|
|
603
|
+
const saved = mainViewportRef.current;
|
|
604
|
+
mainViewportRef.current = null;
|
|
605
|
+
setViewport((view) => saved ?? { ...view, offset: 0, unseen: 0, followingTail: true });
|
|
606
|
+
}
|
|
607
|
+
function openAgentView(row) {
|
|
608
|
+
agentViewRef.current?.close();
|
|
609
|
+
if (!mainViewportRef.current)
|
|
610
|
+
mainViewportRef.current = viewport;
|
|
611
|
+
setViewport((view) => ({ ...view, offset: 0, unseen: 0, followingTail: true }));
|
|
612
|
+
agentViewRef.current = createAgentView({ runtime, target: { sessionId: row.id, task: row.task }, secretValues: secretValuesFromEnvironment(process.env), onChange: (next) => setAgentView(next) });
|
|
613
|
+
}
|
|
614
|
+
/* Another session in use (resume, fork, /clear) and the app closing let go of the watched agent. */
|
|
615
|
+
React.useEffect(() => { if (agentViewRef.current)
|
|
616
|
+
closeAgentView(); }, [state.sessionId]);
|
|
617
|
+
React.useEffect(() => () => { agentViewRef.current?.close(); agentViewRef.current = null; }, []);
|
|
578
618
|
async function refreshAgentRows() {
|
|
579
619
|
try {
|
|
580
620
|
const tree = await controller.agentTree();
|
|
@@ -842,10 +882,34 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
842
882
|
if (!steps.length)
|
|
843
883
|
return;
|
|
844
884
|
onboardingShownRef.current = true;
|
|
845
|
-
const info = steps.filter(step => step.kind === 'info').map(step => step.text)
|
|
885
|
+
const info = steps.filter(step => step.kind === 'info').map(step => step.text);
|
|
886
|
+
/* P17: on the first launch the missing steps are the guided setup, not a list of commands to type. */
|
|
887
|
+
if (bootPlanRef.current?.firstLaunch && !initialPrompt?.trim() && !invocation.setup) {
|
|
888
|
+
void (async () => {
|
|
889
|
+
let row;
|
|
890
|
+
try {
|
|
891
|
+
row = chosenProvider ? (await catalog.listProviders()).find((entry) => entry.id === chosenProvider) : undefined;
|
|
892
|
+
}
|
|
893
|
+
catch {
|
|
894
|
+
row = undefined;
|
|
895
|
+
}
|
|
896
|
+
const plan = setupStart({ firstLaunch: true, providerChosen: Boolean(chosenProvider), providerUsable: Boolean(row?.configured), keyBenched: Boolean(row?.benched), modelChosen: Boolean(chosenProvider && providerOfModel(controller.model()) === chosenProvider) });
|
|
897
|
+
if (info.length)
|
|
898
|
+
setState((current) => localMessage(current, info.join('\n')));
|
|
899
|
+
if (plan.start) {
|
|
900
|
+
await startSetup(plan.from);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const todo = steps.filter(step => step.kind !== 'info');
|
|
904
|
+
if (todo.length)
|
|
905
|
+
setState((current) => localMessage(current, [t('screen.onboarding.setup'), ...todo.map(step => '• ' + step.text + (step.command ? ' ' + step.command : ''))].join('\n')));
|
|
906
|
+
})();
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const todo = steps.filter(step => step.kind !== 'info');
|
|
846
910
|
setState((current) => localMessage(current, [...info, ...(todo.length ? [t('screen.onboarding.setup'), ...todo.map(step => '• ' + step.text + (step.command ? ' ' + step.command : ''))] : [])].join('\n')));
|
|
847
911
|
}, [selectionLoaded, chosenProvider, currentModelMeta, keyMissing]);
|
|
848
|
-
const closeCurrentOverlay = () => { setTrustCenter(null); setMcpCenter(null); setHookCenter(null); setPluginCenter(null); setMemoryCenter(null); setNotesCenter(null); setTasksCenter(null); setLibraryCenter(null); setProviderSecret(''); setProviderStep({ kind: 'list' }); setModelEffortStep(null); setEffortOnly(false); setPickerQuery(''); setPickerSelection({ query: '', selected: 0, pageSize: 10 }); setAppState((current) => closeOverlay(current)); };
|
|
912
|
+
const closeCurrentOverlay = () => { oauthLoginRef.current?.cancel(); oauthLoginRef.current = null; setTrustCenter(null); setMcpCenter(null); setHookCenter(null); setPluginCenter(null); setMemoryCenter(null); setNotesCenter(null); setTasksCenter(null); setLibraryCenter(null); setProviderSecret(''); setProviderStep({ kind: 'list' }); setModelEffortStep(null); setEffortOnly(false); setPickerQuery(''); setPickerSelection({ query: '', selected: 0, pageSize: 10 }); setAppState((current) => closeOverlay(current)); };
|
|
849
913
|
const applyOverlay = (overlay) => setAppState((current) => openOverlay(current, overlay));
|
|
850
914
|
/* B1 slice 18: the models of the chosen provider only; with none chosen, /provider opens first and comes back here. */
|
|
851
915
|
const loadModels = async (provider) => {
|
|
@@ -859,7 +923,7 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
859
923
|
setPickerQuery('');
|
|
860
924
|
setPickerSelection({ query: '', selected: 0, pageSize: 10 });
|
|
861
925
|
setModelRows([]);
|
|
862
|
-
setModelList({ loading: true, verified: false, notice: t('screen.model.loading'), label: target });
|
|
926
|
+
setModelList({ loading: true, verified: false, notice: t('screen.model.loading'), label: target, engineDown: false });
|
|
863
927
|
modelPickerRef.current?.cancel();
|
|
864
928
|
const model = createModelPickerModel(catalog, { provider: target, current: controller.model() });
|
|
865
929
|
modelPickerRef.current = model;
|
|
@@ -868,7 +932,7 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
868
932
|
if (modelPickerRef.current !== model)
|
|
869
933
|
return;
|
|
870
934
|
setModelRows([...model.rows()]);
|
|
871
|
-
setModelList({ loading: false, verified: model.verified(), notice: model.notice(), label: model.label() });
|
|
935
|
+
setModelList({ loading: false, verified: model.verified(), notice: model.notice(), label: model.label(), engineDown: model.engineDown() });
|
|
872
936
|
}
|
|
873
937
|
catch (error) {
|
|
874
938
|
addLocal(screenErrorText(codeOnly(error), act('screen.action.modelPicker')));
|
|
@@ -884,11 +948,87 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
884
948
|
}
|
|
885
949
|
setChosenProvider(row.id);
|
|
886
950
|
showModel(row.id, controller.model());
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
951
|
+
setupTransitionRef.current = true;
|
|
952
|
+
try {
|
|
953
|
+
setProviderStep({ kind: 'list' });
|
|
954
|
+
setProviderSecret('');
|
|
955
|
+
setAppState((current) => closeOverlay(current));
|
|
956
|
+
await loadModels(row.id);
|
|
957
|
+
}
|
|
958
|
+
finally {
|
|
959
|
+
setupTransitionRef.current = false;
|
|
960
|
+
}
|
|
961
|
+
};
|
|
962
|
+
/* P17: the setup starts at the first missing step; it ends when its windows close (a model chosen, or Esc), and says
|
|
963
|
+
whether TALOS is ready — checked, not assumed: a provider, its key and the model's readiness. */
|
|
964
|
+
const startSetup = async (from) => {
|
|
965
|
+
setupRef.current = true;
|
|
966
|
+
setSetupActive(true);
|
|
967
|
+
setupTransitionRef.current = true;
|
|
968
|
+
try {
|
|
969
|
+
if (from === 'model' && chosenProvider) {
|
|
970
|
+
await loadModels(chosenProvider);
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
await loadProviders();
|
|
974
|
+
/* The key step opens at once for the chosen provider (the consent first when its key is in the environment): no
|
|
975
|
+
probe of a provider that has no usable key yet. */
|
|
976
|
+
if (from === 'key' && chosenProvider) {
|
|
977
|
+
const row = (await catalog.listProviders()).find((entry) => entry.id === chosenProvider);
|
|
978
|
+
if (row) {
|
|
979
|
+
const preliminary = providerPickerEnter({ selectedProvider: row.id, configured: row.configured, requiresKey: row.requiresKey, keySource: row.keySource });
|
|
980
|
+
setProviderSecret('');
|
|
981
|
+
setProviderStep(preliminary.kind === 'environment-consent' ? { kind: 'consent', provider: row, choice: 'no' } : keyStepFor(row));
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.providerPicker')));
|
|
987
|
+
}
|
|
988
|
+
finally {
|
|
989
|
+
setupTransitionRef.current = false;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
const finishSetup = async () => {
|
|
993
|
+
if (!setupRef.current)
|
|
994
|
+
return;
|
|
995
|
+
setupRef.current = false;
|
|
996
|
+
setSetupActive(false);
|
|
997
|
+
const model = controller.model();
|
|
998
|
+
const pending = setupPendingProviderRef.current;
|
|
999
|
+
setupPendingProviderRef.current = null;
|
|
1000
|
+
if (pending && providerOfModel(model) === pending.id) {
|
|
1001
|
+
try {
|
|
1002
|
+
await catalog.chooseProvider(pending.id);
|
|
1003
|
+
setChosenProvider(pending.id);
|
|
1004
|
+
showModel(pending.id, model);
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
addLocal(screenErrorText(codeOnly(error), act('screen.action.providerChoice')));
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
let ready = false;
|
|
1011
|
+
try {
|
|
1012
|
+
const provider = typeof catalog?.chosenProvider === 'function' ? await catalog.chosenProvider() : null;
|
|
1013
|
+
ready = Boolean(provider && providerOfModel(model) === provider && (await readinessFor(model)).ready);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
ready = false;
|
|
1017
|
+
}
|
|
1018
|
+
addLocal(setupEndLines(ready ? { ready: true, model } : { ready: false }).join('\n'));
|
|
891
1019
|
};
|
|
1020
|
+
React.useEffect(() => {
|
|
1021
|
+
if (!setupRef.current || appState.overlay || setupTransitionRef.current)
|
|
1022
|
+
return;
|
|
1023
|
+
void finishSetup();
|
|
1024
|
+
}, [appState.overlay]);
|
|
1025
|
+
const setupAskedRef = React.useRef(false);
|
|
1026
|
+
React.useEffect(() => {
|
|
1027
|
+
if (!invocation.setup || !selectionLoaded || setupAskedRef.current)
|
|
1028
|
+
return;
|
|
1029
|
+
setupAskedRef.current = true;
|
|
1030
|
+
void startSetup('provider');
|
|
1031
|
+
}, [selectionLoaded]);
|
|
892
1032
|
const loadProviders = async () => {
|
|
893
1033
|
applyOverlay({ kind: 'provider' });
|
|
894
1034
|
setPickerQuery('');
|
|
@@ -905,6 +1045,63 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
905
1045
|
addLocal(screenErrorText(error, act('screen.action.providerPicker')));
|
|
906
1046
|
}
|
|
907
1047
|
};
|
|
1048
|
+
const keyStepFor = (row, remediation) => row.id === 'openrouter' && typeof catalog?.startOpenRouterLogin === 'function'
|
|
1049
|
+
? { kind: 'login-choice', provider: row, choice: 'browser', ...(remediation ? { remediation } : {}) }
|
|
1050
|
+
: { kind: 'key', provider: row, testing: false, result: null, ...(remediation ? { remediation } : {}) };
|
|
1051
|
+
const oauthErrorText = (provider, error) => {
|
|
1052
|
+
const code = String(error?.code ?? '');
|
|
1053
|
+
const status = error?.status ?? '?';
|
|
1054
|
+
const known = ['OAUTH_TIMEOUT', 'OAUTH_SCAMBIO_RIFIUTATO', 'OAUTH_RETE', 'OAUTH_RISPOSTA_INATTESA', 'OAUTH_DENIED', 'OAUTH_CODE_MISSING'];
|
|
1055
|
+
return known.includes(code) ? t(`screen.provider.oauthError.${code}`, { provider: provider.label, status }) : t('screen.provider.oauthError.other');
|
|
1056
|
+
};
|
|
1057
|
+
/* P21 (owner decisions 2026-09-26): the browser opens (the address is shown anyway), the loopback waits, a code can be
|
|
1058
|
+
pasted instead; the key that comes back is tested and saved like a pasted one, then the models open. */
|
|
1059
|
+
const completeOAuth = async (provider, key) => {
|
|
1060
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'exchanging', message: null } : current);
|
|
1061
|
+
let result;
|
|
1062
|
+
try {
|
|
1063
|
+
result = await catalog.setProviderKey(provider.id, key);
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'failed', message: screenErrorText(codeOnly(error), act('screen.action.keyTest')) } : current);
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
finally {
|
|
1070
|
+
key = '';
|
|
1071
|
+
}
|
|
1072
|
+
if (!result.saved) {
|
|
1073
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'failed', message: result.message } : current);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
oauthLoginRef.current = null;
|
|
1077
|
+
addLocal(t('screen.provider.oauthSaved', { provider: provider.label }));
|
|
1078
|
+
const refreshed = (await catalog.listProviders()).find((entry) => entry.id === provider.id) ?? { ...provider, configured: true };
|
|
1079
|
+
await chooseProviderAndOpenModels(refreshed);
|
|
1080
|
+
};
|
|
1081
|
+
const startOAuth = async (provider) => {
|
|
1082
|
+
oauthLoginRef.current?.cancel();
|
|
1083
|
+
oauthLoginRef.current = null;
|
|
1084
|
+
setProviderSecret('');
|
|
1085
|
+
let login;
|
|
1086
|
+
try {
|
|
1087
|
+
login = await catalog.startOpenRouterLogin();
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
setProviderStep({ kind: 'oauth', provider, url: '', mode: 'browser', opened: false, phase: 'failed', message: oauthErrorText(provider, error) });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
oauthLoginRef.current = login;
|
|
1094
|
+
/* The address also goes to the conversation, unboxed: a person copies it there without the window's borders. */
|
|
1095
|
+
addLocal(t('screen.provider.oauthAddress', { provider: provider.label, url: login.url }));
|
|
1096
|
+
setProviderStep({ kind: 'oauth', provider, url: login.url, mode: login.mode, opened: login.opened, phase: login.mode === 'paste' ? 'paste' : 'waiting', message: null });
|
|
1097
|
+
login.key.then((key) => { if (oauthLoginRef.current === login)
|
|
1098
|
+
void completeOAuth(provider, key); }, (error) => {
|
|
1099
|
+
if (oauthLoginRef.current !== login || error?.code === 'OAUTH_CANCELLED')
|
|
1100
|
+
return;
|
|
1101
|
+
oauthLoginRef.current = null;
|
|
1102
|
+
setProviderStep((current) => current.kind === 'oauth' ? { ...current, phase: 'failed', message: oauthErrorText(provider, error) } : current);
|
|
1103
|
+
});
|
|
1104
|
+
};
|
|
908
1105
|
const routeProviderSelection = async (row, { allowEnvironmentConsent = true } = {}) => {
|
|
909
1106
|
const originalIndex = providerRows.findIndex((provider) => provider.id === row.id);
|
|
910
1107
|
if (originalIndex >= 0)
|
|
@@ -927,10 +1124,25 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
927
1124
|
const next = providerPickerEnter({ selectedProvider: row.id, configured: row.configured, requiresKey: row.requiresKey, keySource: row.keySource, probe });
|
|
928
1125
|
if (next.kind === 'provider-setup') {
|
|
929
1126
|
setProviderSecret('');
|
|
930
|
-
setProviderStep(
|
|
1127
|
+
setProviderStep(keyStepFor(row, next.remediation ?? undefined));
|
|
931
1128
|
return;
|
|
932
1129
|
}
|
|
933
1130
|
if (next.kind === 'provider-remediation') {
|
|
1131
|
+
/* P17 (owner decision 8): in the setup, a local engine that is down opens its model window, which says how to start it. */
|
|
1132
|
+
if (setupRef.current && row.local && !row.requiresKey) {
|
|
1133
|
+
setupPendingProviderRef.current = row;
|
|
1134
|
+
setupTransitionRef.current = true;
|
|
1135
|
+
try {
|
|
1136
|
+
setProviderStep({ kind: 'list' });
|
|
1137
|
+
setProviderSecret('');
|
|
1138
|
+
setAppState((current) => closeOverlay(current));
|
|
1139
|
+
await loadModels(row.id);
|
|
1140
|
+
}
|
|
1141
|
+
finally {
|
|
1142
|
+
setupTransitionRef.current = false;
|
|
1143
|
+
}
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
934
1146
|
addLocal(next.remediation?.message || probe?.detail || t('screen.provider.notReady', { provider: row.label }));
|
|
935
1147
|
return;
|
|
936
1148
|
}
|
|
@@ -1246,6 +1458,11 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
1246
1458
|
addLocal(parsed.suggestions.length ? t('screen.slash.unknownSuggest', { name: parsed.name, suggestions: parsed.suggestions.map(value => `/${value}`).join(', ') }) : t('screen.slash.unknown', { name: parsed.name }));
|
|
1247
1459
|
return;
|
|
1248
1460
|
}
|
|
1461
|
+
/* P17 (owner decision 4): /setup runs the guided setup again, from the provider. */
|
|
1462
|
+
if (parsed.name === 'setup') {
|
|
1463
|
+
await startSetup('provider');
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1249
1466
|
const intent = slashCommandIntent(text);
|
|
1250
1467
|
if (intent?.kind === 'open-overlay') {
|
|
1251
1468
|
if (intent.overlay.kind === 'model') {
|
|
@@ -2188,7 +2405,7 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
2188
2405
|
commandMenuSelectionStore.reset();
|
|
2189
2406
|
return;
|
|
2190
2407
|
}
|
|
2191
|
-
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'key')
|
|
2408
|
+
if (appState.focus.current === 'provider-picker' && (providerStep.kind === 'key' || providerStep.kind === 'oauth'))
|
|
2192
2409
|
setProviderSecret((value) => value.slice(0, -1));
|
|
2193
2410
|
else if (appState.focus.current === 'provider-picker' && providerStep.kind === 'consent') { /* nothing to erase */ }
|
|
2194
2411
|
else
|
|
@@ -2255,6 +2472,37 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
2255
2472
|
return;
|
|
2256
2473
|
}
|
|
2257
2474
|
if (appState.focus.current === 'provider-picker') {
|
|
2475
|
+
if (providerStep.kind === 'login-choice') {
|
|
2476
|
+
const step = providerStep;
|
|
2477
|
+
if (step.choice === 'browser') {
|
|
2478
|
+
await startOAuth(step.provider);
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
setProviderSecret('');
|
|
2482
|
+
setProviderStep({ kind: 'key', provider: step.provider, testing: false, result: null, ...(step.remediation ? { remediation: step.remediation } : {}) });
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2485
|
+
if (providerStep.kind === 'oauth') {
|
|
2486
|
+
const step = providerStep;
|
|
2487
|
+
if (step.phase === 'failed') {
|
|
2488
|
+
await startOAuth(step.provider);
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (step.phase !== 'paste' || !oauthLoginRef.current)
|
|
2492
|
+
return;
|
|
2493
|
+
const pasted = providerSecret;
|
|
2494
|
+
setProviderSecret('');
|
|
2495
|
+
if (!pasted.trim())
|
|
2496
|
+
return;
|
|
2497
|
+
try {
|
|
2498
|
+
await oauthLoginRef.current.submit(pasted);
|
|
2499
|
+
}
|
|
2500
|
+
catch (error) {
|
|
2501
|
+
if (error?.code === 'OAUTH_CODE_MISSING')
|
|
2502
|
+
setProviderStep({ ...step, message: oauthErrorText(step.provider, error) });
|
|
2503
|
+
}
|
|
2504
|
+
return;
|
|
2505
|
+
}
|
|
2258
2506
|
if (providerStep.kind === 'consent') {
|
|
2259
2507
|
const step = providerStep;
|
|
2260
2508
|
try {
|
|
@@ -2429,6 +2677,13 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
2429
2677
|
closeCurrentOverlay();
|
|
2430
2678
|
return;
|
|
2431
2679
|
}
|
|
2680
|
+
if (action === 'picker-cancel' && setupRef.current && appState.focus.current === 'model-picker' && modelList.engineDown && !modelEffortStep) {
|
|
2681
|
+
modelPickerRef.current?.cancel();
|
|
2682
|
+
setupTransitionRef.current = true;
|
|
2683
|
+
setupPendingProviderRef.current = null;
|
|
2684
|
+
void loadProviders().finally(() => { setupTransitionRef.current = false; });
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2432
2687
|
if (action === 'picker-cancel') {
|
|
2433
2688
|
if (appState.focus.current === 'provider-picker')
|
|
2434
2689
|
providerPickerRef.current?.cancel();
|
|
@@ -2439,6 +2694,14 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
2439
2694
|
closeCurrentOverlay();
|
|
2440
2695
|
return;
|
|
2441
2696
|
}
|
|
2697
|
+
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'login-choice' && (action === 'picker-up' || action === 'picker-home')) {
|
|
2698
|
+
setProviderStep({ ...providerStep, choice: 'browser' });
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'login-choice' && (action === 'picker-down' || action === 'picker-end')) {
|
|
2702
|
+
setProviderStep({ ...providerStep, choice: 'paste-key' });
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2442
2705
|
if (appState.focus.current === 'provider-picker' && providerStep.kind === 'consent' && (action === 'picker-up' || action === 'picker-home')) {
|
|
2443
2706
|
setProviderStep({ ...providerStep, choice: 'no' });
|
|
2444
2707
|
return;
|
|
@@ -2719,7 +2982,7 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
2719
2982
|
const pasteOwner = composerOwnsPaste({
|
|
2720
2983
|
bootPhase: boot.phase,
|
|
2721
2984
|
focus: appState.focus.current,
|
|
2722
|
-
modalOwner: Boolean(appState.overlay || queueEditor || transcriptSearch || contextInspector || themePicker || undoPicker || historyPicker || agentTreeOverlay || trustCenter || mcpCenter || hookCenter || pluginCenter || memoryCenter || notesCenter || tasksCenter || libraryCenter || researchCenter || automationCenter || forgeCenter || pending),
|
|
2985
|
+
modalOwner: Boolean(appState.overlay || queueEditor || transcriptSearch || contextInspector || themePicker || undoPicker || historyPicker || agentTreeOverlay || agentView || trustCenter || mcpCenter || hookCenter || pluginCenter || memoryCenter || notesCenter || tasksCenter || libraryCenter || researchCenter || automationCenter || forgeCenter || pending),
|
|
2723
2986
|
vimMode: vim.enabled ? vim.mode : 'disabled',
|
|
2724
2987
|
auxCount,
|
|
2725
2988
|
});
|
|
@@ -2781,13 +3044,18 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
2781
3044
|
const focusNow = graceComposer && appState.focus.current === 'approval' ? (appState.focus.stack.at(-1) ?? 'composer') : appState.focus.current;
|
|
2782
3045
|
if (graceComposer && focusNow !== 'composer')
|
|
2783
3046
|
return;
|
|
2784
|
-
const modalOwner = Boolean((appState.overlay && !(graceComposer && appState.overlay.kind === 'approval')) || queueEditor || transcriptSearch || contextInspector || themePicker || undoPicker || historyPicker || agentTreeOverlay || trustCenter || mcpCenter || hookCenter || pluginCenter || memoryCenter || notesCenter || tasksCenter || libraryCenter || researchCenter || automationCenter || forgeCenter || (pending && !graceComposer));
|
|
3047
|
+
const modalOwner = Boolean((appState.overlay && !(graceComposer && appState.overlay.kind === 'approval')) || queueEditor || transcriptSearch || contextInspector || themePicker || undoPicker || historyPicker || agentTreeOverlay || agentView || trustCenter || mcpCenter || hookCenter || pluginCenter || memoryCenter || notesCenter || tasksCenter || libraryCenter || researchCenter || automationCenter || forgeCenter || (pending && !graceComposer));
|
|
2785
3048
|
/* R1 E15 / Q-R1-7 (no mouse capture): Ctrl+Home and Ctrl+End move the transcript to its first and newest rows, as in
|
|
2786
3049
|
Claude Code fullscreen and Qwen Code. The keymap resolver drops Ctrl on Home/End, so they are read here first. */
|
|
2787
3050
|
if (key?.ctrl && (key.home || key.end) && !modalOwner && boot.phase === 'ready') {
|
|
2788
3051
|
scrollTranscript(key.home ? 'top' : 'jump-tail');
|
|
2789
3052
|
return;
|
|
2790
3053
|
}
|
|
3054
|
+
/* P13: the watched agent's transcript scrolls the same way (the view owns the composer, not the transcript). */
|
|
3055
|
+
if (agentView && !agentTreeOverlay && !appState.overlay && key?.ctrl && (key.home || key.end) && boot.phase === 'ready') {
|
|
3056
|
+
scrollTranscript(key.home ? 'top' : 'jump-tail');
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
2791
3059
|
/* Lane O: the help overlay scrolls inside its budget; the keymap has no help-context scroll keys, so they are read here. */
|
|
2792
3060
|
if (appState.overlay?.kind === 'help' && (key.pageUp || key.pageDown || key.upArrow || key.downArrow)) {
|
|
2793
3061
|
const step = key.pageUp || key.pageDown ? helpPageRef.current : 1, delta = key.pageUp || key.upArrow ? -step : step;
|
|
@@ -3352,6 +3620,18 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3352
3620
|
setAgentTreeOverlay(null);
|
|
3353
3621
|
return;
|
|
3354
3622
|
}
|
|
3623
|
+
/* P13: Enter watches the selected agent live; on the session in use it goes back to it. */
|
|
3624
|
+
if (key.return) {
|
|
3625
|
+
const row = selectedAgentRow(agentTreeOverlay);
|
|
3626
|
+
setAgentTreeOverlay(null);
|
|
3627
|
+
if (!row || row.id === agentView?.sessionId)
|
|
3628
|
+
return;
|
|
3629
|
+
if (row.id === controller.current())
|
|
3630
|
+
closeAgentView();
|
|
3631
|
+
else
|
|
3632
|
+
openAgentView(row);
|
|
3633
|
+
return;
|
|
3634
|
+
}
|
|
3355
3635
|
if (key.upArrow) {
|
|
3356
3636
|
setAgentTreeOverlay((current) => current ? moveAgentTreeSelection(current, -1) : current);
|
|
3357
3637
|
return;
|
|
@@ -3509,6 +3789,21 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3509
3789
|
}
|
|
3510
3790
|
return;
|
|
3511
3791
|
}
|
|
3792
|
+
/* P13 (owner decisions 2 and 7): watching an agent is read-only. Esc goes back to the main session and does NOT
|
|
3793
|
+
interrupt the turn (Claude Code fixed exactly that); Ctrl+C still stops the whole turn; the agent tree, scrolling,
|
|
3794
|
+
redraw, reasoning and tool details still work; typing does nothing. */
|
|
3795
|
+
if (agentView) {
|
|
3796
|
+
if (key.escape) {
|
|
3797
|
+
closeAgentView();
|
|
3798
|
+
return;
|
|
3799
|
+
}
|
|
3800
|
+
const viewAction = resolveKeybinding(ch, key, ['global', 'transcript', 'composer'], effectiveKeymap);
|
|
3801
|
+
if (viewAction && AGENT_VIEW_ACTIONS.has(viewAction)) {
|
|
3802
|
+
handleSemanticAction(viewAction, key);
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
return;
|
|
3806
|
+
}
|
|
3512
3807
|
const vimOwnsInput = boot.phase === 'ready' && !trustCenter && !mcpCenter && !hookCenter && !pluginCenter && !memoryCenter && !notesCenter && !tasksCenter && !libraryCenter && !researchCenter && !automationCenter && !forgeCenter && focusNow === 'composer' && vim.enabled && (vim.mode === 'normal' || Boolean(key.escape));
|
|
3513
3808
|
if (vimOwnsInput) {
|
|
3514
3809
|
const vimResult = handleVimInput({ state: vim, editor: currentEditor(), ch, key });
|
|
@@ -3551,6 +3846,20 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3551
3846
|
}
|
|
3552
3847
|
return;
|
|
3553
3848
|
}
|
|
3849
|
+
if (focusNow === 'provider-picker' && providerStep.kind === 'login-choice')
|
|
3850
|
+
return;
|
|
3851
|
+
if (focusNow === 'provider-picker' && providerStep.kind === 'oauth') {
|
|
3852
|
+
if (providerStep.phase === 'waiting') {
|
|
3853
|
+
if (routed.text.trim().toLowerCase() === 'c')
|
|
3854
|
+
setProviderStep({ ...providerStep, phase: 'paste', message: null });
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3857
|
+
if (providerStep.phase === 'paste') {
|
|
3858
|
+
setProviderSecret((value) => value + routed.text);
|
|
3859
|
+
return;
|
|
3860
|
+
}
|
|
3861
|
+
return;
|
|
3862
|
+
}
|
|
3554
3863
|
if (focusNow === 'provider-picker' && providerStep.kind === 'consent') {
|
|
3555
3864
|
const letter = routed.text.trim().toLowerCase();
|
|
3556
3865
|
if (letter === 'y' || letter === 'n')
|
|
@@ -3559,6 +3868,10 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3559
3868
|
}
|
|
3560
3869
|
if (focusNow === 'model-picker' && modelEffortStep)
|
|
3561
3870
|
return;
|
|
3871
|
+
if (focusNow === 'model-picker' && setupRef.current && modelList.engineDown && !pickerQuery && (routed.text === 'r' || routed.text === 'R') && appState.overlay?.kind === 'model') {
|
|
3872
|
+
void loadModels(appState.overlay.provider);
|
|
3873
|
+
return;
|
|
3874
|
+
}
|
|
3562
3875
|
if (focusNow === 'model-picker' || focusNow === 'provider-picker' || focusNow === 'session-picker') {
|
|
3563
3876
|
setPickerQuery((value) => value + routed.text);
|
|
3564
3877
|
setPickerSelection((current) => ({ ...current, selected: 0 }));
|
|
@@ -3591,13 +3904,18 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3591
3904
|
});
|
|
3592
3905
|
const { columns, activity, shortcutLine, rosterLine, queueLine, footerLine, statusBar: statusRow } = layout;
|
|
3593
3906
|
/* R3 Q-R3-9: no `Run · …` rows; R5a S8: a collapsed reasoning block is drawn once it has ended (thinking-row.ts). */
|
|
3594
|
-
|
|
3907
|
+
/* P13: the watched agent's transcript takes the main view; the main session's is kept as it was. */
|
|
3908
|
+
const shownTranscriptItems = (agentView ? agentView.transcript.items : state.transcript.items);
|
|
3909
|
+
/* In an agent's transcript its prompt is the task the main session gave it, not the person's words, and the answers are
|
|
3910
|
+
the agent's: "You" and "TALOS" would say otherwise. */
|
|
3911
|
+
const watchingAgent = Boolean(agentView);
|
|
3912
|
+
const visibleTranscriptItems = React.useMemo(() => visibleItemsOf(shownTranscriptItems, appState.reasoningVisible), [shownTranscriptItems, appState.reasoningVisible]);
|
|
3595
3913
|
const renderTranscriptItem = React.useCallback((item) => {
|
|
3596
3914
|
const selected = item.id === transcriptSelectedId, prefix = selected ? '› ' : '';
|
|
3597
3915
|
if (transcriptRaw)
|
|
3598
3916
|
return h(Ink.Text, { key: item.id, inverse: selected, dimColor: !selected }, prefix + transcriptItemRaw(item));
|
|
3599
3917
|
if (item.kind === 'message')
|
|
3600
|
-
return h(Ink.Box, { key: item.id, flexDirection: 'column' }, h(Ink.Text, { bold: item.role === 'user', color: transcriptColor, inverse: selected }, prefix + t(item.role === 'user' ? 'screen.transcript.you' : 'screen.transcript.assistant')), h(MarkdownView, { text: item.text, width: columns }));
|
|
3918
|
+
return h(Ink.Box, { key: item.id, flexDirection: 'column' }, h(Ink.Text, { bold: item.role === 'user', color: transcriptColor, inverse: selected }, prefix + t(item.role === 'user' ? (watchingAgent ? 'screen.agentView.task' : 'screen.transcript.you') : (watchingAgent ? 'screen.agentView.agent' : 'screen.transcript.assistant'))), h(MarkdownView, { text: item.text, width: columns }));
|
|
3601
3919
|
if (item.kind === 'reasoning')
|
|
3602
3920
|
return appState.reasoningVisible ? h(Ink.Box, { key: item.id, borderStyle: 'single', flexDirection: 'column' }, h(Ink.Text, { dimColor: true, inverse: selected }, prefix + t('screen.transcript.reasoning')), h(Ink.Text, { dimColor: true }, item.text || ' ')) : h(Ink.Text, { key: item.id, dimColor: true, inverse: selected }, prefix + thoughtLine(item));
|
|
3603
3921
|
/* The selected row carries a two-cell prefix: its header is drawn two cells narrower so it never wraps (lane RD). */
|
|
@@ -3608,7 +3926,7 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3608
3926
|
if (item.kind === 'warning')
|
|
3609
3927
|
return h(Ink.Text, { key: item.id, dimColor: true, inverse: selected }, prefix + item.text);
|
|
3610
3928
|
return null;
|
|
3611
|
-
}, [transcriptSelectedId, transcriptRaw, columns, appState.expandedTools, appState.reasoningVisible, toolNow]);
|
|
3929
|
+
}, [transcriptSelectedId, transcriptRaw, columns, appState.expandedTools, appState.reasoningVisible, toolNow, watchingAgent]);
|
|
3612
3930
|
const diffView = diff ? renderDiffModel(parseUnifiedDiff(diff), columns) : null;
|
|
3613
3931
|
const vimHint = vimModeHint(vim);
|
|
3614
3932
|
React.useEffect(() => {
|
|
@@ -3637,11 +3955,11 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3637
3955
|
const statusStartedAt = activity.startedAt ?? busySince;
|
|
3638
3956
|
const statusElapsed = typeof activity.frozenMs === 'number' ? activity.frozenMs : Math.max(0, motionNow - statusStartedAt);
|
|
3639
3957
|
const statusFrame = frameIndex(statusElapsed, MOTION_FRAME_MS, theme.glyphs.spinner.length);
|
|
3640
|
-
const statusText = exitHint ? exitHintText(exitHintKey) : busyIndicatorText({ kind: activity.kind, elapsedMs: statusElapsed, frame: statusFrame, unicode: capabilities.unicode, frames: theme.glyphs.spinner, ...(activity.tool ? { tool: activity.tool } : {}), retry: state.retry, nowMs: Math.max(toolNow, state.retry?.at ?? 0) });
|
|
3958
|
+
const statusText = exitHint ? exitHintText(exitHintKey) : busyIndicatorText({ kind: activity.kind, elapsedMs: statusElapsed, frame: statusFrame, unicode: capabilities.unicode, frames: theme.glyphs.spinner, ...(activity.tool ? { tool: activity.tool } : {}), ...(activity.agents ? { agents: activity.agents } : {}), retry: state.retry, nowMs: Math.max(toolNow, state.retry?.at ?? 0) });
|
|
3641
3959
|
/* R3 gate E7: the main roster row says what the CLI knows while a turn is open (the kernel list says `unknown` or `running`),
|
|
3642
3960
|
and after the turn done / interrupted / failed like the status row (owner decision, R3 follow-up: one vocabulary). */
|
|
3643
3961
|
const turnOpen = state.running || typeof state.sentAt === 'number';
|
|
3644
|
-
const mainRowStatus = turnOpen ? activityWord(activity.kind, activity.tool) : state.lastTurn ? activityWord(state.lastTurn.outcome) : undefined;
|
|
3962
|
+
const mainRowStatus = turnOpen ? activityWord(activity.kind, activity.tool, activity.agents) : state.lastTurn ? activityWord(state.lastTurn.outcome) : undefined;
|
|
3645
3963
|
/*
|
|
3646
3964
|
* R1 gate E1: one height budget per frame. The composer's row count comes from its store as a primitive, so the
|
|
3647
3965
|
* fast input path still re-renders only the composer on a key and the whole screen only when the composer gains or
|
|
@@ -3674,6 +3992,18 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3674
3992
|
*/
|
|
3675
3993
|
const fittedPanel = (fitted) => ({ element: renderFittedOverlay(h, Ink, fitted, { accentColor: pickerColor }), rows: fitted.rows.length + (fitted.border ? 2 : 0) });
|
|
3676
3994
|
const overlayOf = (content) => fittedPanel(fitOverlay(content, { maxRows, columns }));
|
|
3995
|
+
/* P17: in the guided setup, each window says its step; a local engine that is down offers R and Esc (decision 8). */
|
|
3996
|
+
const setupWrap = (content, step) => {
|
|
3997
|
+
if (!setupActive)
|
|
3998
|
+
return content;
|
|
3999
|
+
/* On a local engine that is down, Esc goes back to the providers: the step line does not also say it skips. */
|
|
4000
|
+
if (step === 'model' && modelList.engineDown) {
|
|
4001
|
+
const wrapped = setupOverlay(content, step, { escSkips: false });
|
|
4002
|
+
return { ...wrapped, foot: [...(wrapped.foot ?? []), { text: t('firstrun.setup.retryHint'), tone: 'dim' }] };
|
|
4003
|
+
}
|
|
4004
|
+
/* The key and consent windows say what Esc does in their own line ("Esc cancels"): one wording per window. */
|
|
4005
|
+
return setupOverlay(content, step, { escSkips: step !== 'key' });
|
|
4006
|
+
};
|
|
3677
4007
|
let overlayNode = null;
|
|
3678
4008
|
if (historyPicker)
|
|
3679
4009
|
overlayNode = overlayOf(historyPickerOverlay({ rows: history, picker: historyPicker, width: columns }));
|
|
@@ -3684,7 +4014,7 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3684
4014
|
else if (contextInspector)
|
|
3685
4015
|
overlayNode = overlayOf(contextInspectorOverlay(contextInspector));
|
|
3686
4016
|
else if (agentTreeOverlay)
|
|
3687
|
-
overlayNode = overlayOf(agentTreeOverlayContent(agentTreeOverlay, { width: Math.max(1, columns - 2), nowMs: motionNow, currentSessionId: controller.current(), running: state.running }));
|
|
4017
|
+
overlayNode = overlayOf(agentTreeOverlayContent(agentTreeOverlay, { width: Math.max(1, columns - 2), nowMs: motionNow, currentSessionId: controller.current(), running: state.running, viewingId: agentView?.sessionId ?? null }));
|
|
3688
4018
|
else if (trustCenter)
|
|
3689
4019
|
overlayNode = overlayOf(trustCenterOverlay(trustCenter));
|
|
3690
4020
|
else if (mcpCenter)
|
|
@@ -3726,9 +4056,13 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3726
4056
|
else if (appState.overlay?.kind === 'model')
|
|
3727
4057
|
overlayNode = modelEffortStep
|
|
3728
4058
|
? overlayOf(effortLineOverlay({ name: modelEffortStep.name, choices: effortRows, selected: pickerSelection.selected, current: state.status.model === modelEffortStep.id ? (reasoningEffort ?? 'default') : null, width: Math.max(20, columns - 6), unicode: capabilities.unicode }))
|
|
3729
|
-
: overlayOf(modelPickerOverlay({ label: modelList.label, provider: appState.overlay.provider, loading: modelList.loading, verified: modelList.verified, notice: modelList.notice ?? '', query: pickerQuery, rows: filteredModels, selected: pickerSelection.selected }));
|
|
4059
|
+
: overlayOf(setupWrap(modelPickerOverlay({ label: modelList.label, provider: appState.overlay.provider, loading: modelList.loading, verified: modelList.verified, notice: modelList.notice ?? '', query: pickerQuery, rows: filteredModels, selected: pickerSelection.selected }), 'model'));
|
|
4060
|
+
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'login-choice')
|
|
4061
|
+
overlayNode = overlayOf(setupWrap(providerLoginChoiceOverlay({ label: providerStep.provider.label, choice: providerStep.choice, notice: providerStep.remediation?.message ?? (providerStep.provider.benched ? t('screen.provider.benchedRetry', { benched: benchedKeyText(providerStep.provider.label, providerStep.provider.benched) }) : null) }), 'key'));
|
|
4062
|
+
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'oauth')
|
|
4063
|
+
overlayNode = overlayOf(setupWrap(providerOAuthOverlay({ label: providerStep.provider.label, url: providerStep.url, mode: providerStep.mode, opened: providerStep.opened, phase: providerStep.phase, masked: maskedSecret(providerSecret), message: providerStep.message }), 'key'));
|
|
3730
4064
|
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'consent')
|
|
3731
|
-
overlayNode = overlayOf(providerConsentOverlay({ provider: providerStep.provider, choice: providerStep.choice }));
|
|
4065
|
+
overlayNode = overlayOf(setupWrap(providerConsentOverlay({ provider: providerStep.provider, choice: providerStep.choice }), 'key'));
|
|
3732
4066
|
else if (appState.overlay?.kind === 'provider' && providerStep.kind === 'key') {
|
|
3733
4067
|
const label = providerStep.provider.label;
|
|
3734
4068
|
/* B1 slice 18: a one-token key test is stated on this step, before Enter sends it; no further question is asked. */
|
|
@@ -3736,10 +4070,10 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3736
4070
|
/* B1 slice 25: a provider whose saved key is benched says so here, with the cause and until when, before another key is typed. */
|
|
3737
4071
|
const remediation = providerStep.remediation?.message ?? null;
|
|
3738
4072
|
const benched = !remediation && providerStep.provider.benched ? t('screen.provider.benchedRetry', { benched: benchedKeyText(label, providerStep.provider.benched) }) : null;
|
|
3739
|
-
overlayNode = overlayOf(providerKeyOverlay({ label, remediation, benched, masked: maskedSecret(providerSecret), keyTestNotice, testing: providerStep.testing, result: providerStep.result?.message ?? null }));
|
|
4073
|
+
overlayNode = overlayOf(setupWrap(providerKeyOverlay({ label, remediation, benched, masked: maskedSecret(providerSecret), keyTestNotice, testing: providerStep.testing, result: providerStep.result?.message ?? null }), 'key'));
|
|
3740
4074
|
}
|
|
3741
4075
|
else if (appState.overlay?.kind === 'provider')
|
|
3742
|
-
overlayNode = overlayOf(providerPickerOverlay({ query: pickerQuery, rows: filteredProviders, selected: pickerSelection.selected, chosen: chosenProvider, reachable: id => providerPickerRef.current?.reachable(id) ?? null }));
|
|
4076
|
+
overlayNode = overlayOf(setupWrap(providerPickerOverlay({ query: pickerQuery, rows: filteredProviders, selected: pickerSelection.selected, chosen: chosenProvider, reachable: id => providerPickerRef.current?.reachable(id) ?? null }), 'provider'));
|
|
3743
4077
|
else if (appState.overlay?.kind === 'session')
|
|
3744
4078
|
overlayNode = overlayOf(sessionPickerOverlay({ action: appState.overlay.action, query: pickerQuery, rows: filteredSessions, selected: pickerSelection.selected }));
|
|
3745
4079
|
const diffPanel = diffView ? overlayOf({ head: [{ text: t('screen.diff.title', { mode: diffView.mode }), tone: 'title' }], sections: [{ lines: diffView.lines.map(line => ({ text: line.text, tone: line.kind === 'file' || line.kind === 'hunk' || line.kind === 'add' || line.kind === 'remove' ? 'strong' : 'dim' })), offset: 0 }] }) : null;
|
|
@@ -3751,11 +4085,14 @@ export function createTuiAppComponent(React, Ink, baseCapabilities = { interacti
|
|
|
3751
4085
|
const auxNodes = completionPanel ? [completionPanel.element] : auxLines;
|
|
3752
4086
|
const b = layoutBudget({ ...budgetInput, overlay: overlayNode?.rows ?? null, menu: menuRows || null, diff: diffPanel?.rows ?? null, aux: completionPanel?.rows ?? (auxLines.length || null) });
|
|
3753
4087
|
/* R5a S9 (review D-14): the header names the folder once; `interactive` said nothing, `degraded` still does. */
|
|
3754
|
-
const
|
|
4088
|
+
const agentViewTask = agentView ? (agentView.task ?? t('screen.agentView.unnamed')) : '';
|
|
4089
|
+
const headerStatus = agentView ? t(agentView.done ? 'screen.agentView.done' : 'screen.agentView.watching', { task: agentViewTask }) : boot.phase === 'error' ? t('screen.header.degraded') : '';
|
|
3755
4090
|
const header = headerLine({ workspace: truncateMiddle(projectRoot, Math.max(1, columns - displayWidth(headerLine({ workspace: '', status: headerStatus })) - 3)), status: headerStatus });
|
|
3756
4091
|
/* E1/E2: the root is exactly the frame; every region below is a box of the height the budget gave it, in this order,
|
|
3757
4092
|
so the composer, the status row and the chrome sit at the bottom of the screen (D-03) and nothing overwrites
|
|
3758
4093
|
anything (D-02). */
|
|
3759
|
-
return h(Ink.Box, { key: `main-${appState.redrawNonce}-${live.id}`, flexDirection: 'column', height: b.frameRows, flexShrink: 0, overflowY: 'hidden' }, h(Regions.Header, { key: 'header', rows: b.header, text: header, color: bootColor }), h(TranscriptView, { key: 'transcript', items: visibleTranscriptItems, scrollRows: viewport.offset, rowBudget: b.transcript, width: columns, raw: transcriptRaw, expandedTools: appState.expandedTools, reasoningVisible: appState.reasoningVisible, now: toolNow, revealId: transcriptRevealId, measurementStore: transcriptMeasurementStore, renderItem: renderTranscriptItem, onPlan: onTranscriptPlan, dimColor: footerColor }), h(Regions.Panels, { key: 'panels', budget: b, diff: diffPanel?.element ?? null, overlay: overlayNode?.element ?? null, aux: auxNodes, menu: menuOpen ? h(CommandMenuView, { composerStore, selectionStore: commandMenuSelectionStore, maxRows: b.menu, columns }) : null }), b.status ? h(Ink.Text, { key: 'status', color: footerColor, dimColor: exitHint, wrap: 'truncate-end' }, statusText) : null, b.composer ?
|
|
4094
|
+
return h(Ink.Box, { key: `main-${appState.redrawNonce}-${live.id}`, flexDirection: 'column', height: b.frameRows, flexShrink: 0, overflowY: 'hidden' }, h(Regions.Header, { key: 'header', rows: b.header, text: header, color: bootColor }), h(TranscriptView, { key: 'transcript', items: visibleTranscriptItems, scrollRows: viewport.offset, rowBudget: b.transcript, width: columns, raw: transcriptRaw, expandedTools: appState.expandedTools, reasoningVisible: appState.reasoningVisible, now: toolNow, revealId: transcriptRevealId, measurementStore: transcriptMeasurementStore, renderItem: renderTranscriptItem, onPlan: onTranscriptPlan, dimColor: footerColor }), h(Regions.Panels, { key: 'panels', budget: b, diff: diffPanel?.element ?? null, overlay: overlayNode?.element ?? null, aux: auxNodes, menu: menuOpen ? h(CommandMenuView, { composerStore, selectionStore: commandMenuSelectionStore, maxRows: b.menu, columns }) : null }), b.status ? h(Ink.Text, { key: 'status', color: footerColor, dimColor: exitHint, wrap: 'truncate-end' }, statusText) : null, b.composer ? (agentView
|
|
4095
|
+
? h(Ink.Box, { key: 'composer', borderStyle: 'single', flexDirection: 'column', flexShrink: 0, height: 2 + b.composerContent + b.composerHints, overflowY: 'hidden' }, h(Ink.Text, { dimColor: true, wrap: 'truncate-end' }, t('screen.agentView.readOnly')))
|
|
4096
|
+
: h(ComposerView, { key: 'composer', store: composerStore, vimHint, contentRows: b.composerContent, hintRows: b.composerHints, columns })) : null, h(Regions.StatusBar, { key: 'status-bar', budget: b, shortcutLine: agentView ? truncateDisplay(t('screen.agentView.shortcuts'), Math.max(1, columns)) : shortcutLine, rosterLines, queueLine, reasoningLine, footerLine, statusSegments: statusRow.segments, toneColors: { warn: theme.colors.warning, high: theme.colors.danger, critical: theme.colors.danger }, color: footerColor }));
|
|
3760
4097
|
};
|
|
3761
4098
|
}
|