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.
Files changed (40) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +9 -4
  3. package/dist/args.js +4 -1
  4. package/dist/commands/provider-cli.js +146 -87
  5. package/dist/config/types.js +1 -1
  6. package/dist/i18n/en/approval.js +3 -0
  7. package/dist/i18n/en/credentials.js +10 -0
  8. package/dist/i18n/en/errors.js +21 -0
  9. package/dist/i18n/en/firstrun.js +12 -0
  10. package/dist/i18n/en/screen.js +38 -0
  11. package/dist/i18n/error-view.js +1 -1
  12. package/dist/main.js +2 -1
  13. package/dist/provider/openrouter-login.js +172 -0
  14. package/dist/runtime/context-archive.js +69 -0
  15. package/dist/runtime/repo.js +11 -0
  16. package/dist/runtime/supervisor.js +5 -2
  17. package/dist/runtime/talos-composition.js +9 -3
  18. package/dist/tui/agent-view.js +51 -0
  19. package/dist/tui/app.js +364 -27
  20. package/dist/tui/catalog-service.js +2 -1
  21. package/dist/tui/components/status-indicator.js +10 -4
  22. package/dist/tui/components/terminal-shell.js +10 -1
  23. package/dist/tui/descendant-approvals.js +104 -0
  24. package/dist/tui/launch-state.js +9 -0
  25. package/dist/tui/live-activity.js +13 -2
  26. package/dist/tui/overlays/agent-tree.js +7 -3
  27. package/dist/tui/overlays/approval-dialog.js +3 -1
  28. package/dist/tui/overlays/model-picker.js +5 -0
  29. package/dist/tui/overlays/provider-picker.js +26 -0
  30. package/dist/tui/project-trust-prompt.js +5 -3
  31. package/dist/tui/session-controller.js +25 -12
  32. package/dist/tui/setup-wizard.js +46 -0
  33. package/dist/tui/slash-commands.js +3 -1
  34. package/dist/tui/theme-catalog.js +3 -1
  35. package/dist/version.js +1 -1
  36. package/dist/workspace/checkpoint-store.js +32 -1
  37. package/dist/workspace/checkpoint.js +40 -12
  38. package/package.json +3 -1
  39. package/vendor/harness-ui/src/kernel/talosHarness.mjs +149 -2
  40. 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 ?? 'calm');
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), todo = steps.filter(step => step.kind !== 'info');
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
- setProviderStep({ kind: 'list' });
888
- setProviderSecret('');
889
- setAppState((current) => closeOverlay(current));
890
- await loadModels(row.id);
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({ kind: 'key', provider: row, testing: false, result: null, ...(next.remediation ? { remediation: next.remediation } : {}) });
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
- const visibleTranscriptItems = React.useMemo(() => visibleItemsOf(state.transcript.items, appState.reasoningVisible), [state.transcript.items, appState.reasoningVisible]);
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 headerStatus = boot.phase === 'error' ? t('screen.header.degraded') : '';
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 ? h(ComposerView, { key: 'composer', store: composerStore, vimHint, contentRows: b.composerContent, hintRows: b.composerHints, columns }) : null, h(Regions.StatusBar, { key: 'status-bar', budget: b, shortcutLine, rosterLines, queueLine, reasoningLine, footerLine, statusSegments: statusRow.segments, toneColors: { warn: theme.colors.warning, high: theme.colors.danger, critical: theme.colors.danger }, color: footerColor }));
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
  }