@golba98/codexa 1.0.3 → 1.0.5

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 (125) hide show
  1. package/README.md +17 -18
  2. package/bin/codexa.js +62 -144
  3. package/package.json +4 -3
  4. package/src/app.tsx +642 -303
  5. package/src/commands/handler.ts +7 -18
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/runtimeConfig.ts +1 -1
  9. package/src/config/settings.ts +1 -1
  10. package/src/config/trustStore.ts +1 -1
  11. package/src/core/README.md +52 -0
  12. package/src/core/agent/loop.ts +282 -0
  13. package/src/core/agent/protocol.ts +211 -0
  14. package/src/core/agent/tools.ts +414 -0
  15. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  16. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  17. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  18. package/src/core/debug/modelStateDebug.ts +34 -0
  19. package/src/core/executables/antigravityExecutable.ts +48 -0
  20. package/src/core/executables/executableResolver.ts +5 -1
  21. package/src/core/models/codexModelCapabilities.ts +57 -4
  22. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  23. package/src/core/models/providerModelCache.ts +106 -0
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/CommandRunner.ts +12 -1
  26. package/src/core/providerLauncher/registry.ts +64 -18
  27. package/src/core/providerLauncher/types.ts +12 -9
  28. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  29. package/src/core/providerRuntime/anthropic.ts +10 -6
  30. package/src/core/providerRuntime/antigravity.ts +461 -0
  31. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
  32. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  33. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  34. package/src/core/providerRuntime/local.ts +129 -51
  35. package/src/core/providerRuntime/mistralVibe.ts +663 -0
  36. package/src/core/providerRuntime/models.ts +40 -15
  37. package/src/core/providerRuntime/reasoning.ts +9 -6
  38. package/src/core/providerRuntime/registry.ts +76 -4
  39. package/src/core/providerRuntime/types.ts +20 -14
  40. package/src/core/providers/codexSubprocess.ts +2 -2
  41. package/src/core/providers/types.ts +1 -1
  42. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  43. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  44. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  45. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  46. package/src/core/terminal/frameLock.ts +109 -0
  47. package/src/core/terminal/inkRenderReset.ts +123 -0
  48. package/src/core/terminal/terminalControl.ts +22 -0
  49. package/src/core/terminal/terminalTitle.ts +16 -102
  50. package/src/core/{channel.ts → version/channel.ts} +1 -1
  51. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  52. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  53. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  54. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  55. package/src/headless/execRunner.ts +2 -2
  56. package/src/index.tsx +66 -98
  57. package/src/session/appSession.ts +10 -7
  58. package/src/session/chatLifecycle.ts +1 -1
  59. package/src/session/liveRenderScheduler.ts +1 -1
  60. package/src/session/types.ts +1 -1
  61. package/src/test/runtimeTestUtils.ts +90 -0
  62. package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
  63. package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
  64. package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
  65. package/src/ui/chrome/AppShell.tsx +672 -0
  66. package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
  67. package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
  68. package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
  69. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  70. package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
  71. package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
  72. package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
  73. package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
  74. package/src/ui/{focus.ts → input/focus.ts} +1 -1
  75. package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
  76. package/src/ui/layout.ts +298 -24
  77. package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
  78. package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
  79. package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
  80. package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
  81. package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
  82. package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
  83. package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
  84. package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
  85. package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
  86. package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
  87. package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
  88. package/src/ui/panels/ProviderPicker.tsx +737 -0
  89. package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
  90. package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
  91. package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
  92. package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
  93. package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
  94. package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
  95. package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
  96. package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
  97. package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
  98. package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
  99. package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
  100. package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
  101. package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
  102. package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
  103. package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
  104. package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
  105. package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
  106. package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
  107. package/src/ui/timeline/TranscriptShell.tsx +322 -0
  108. package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
  109. package/src/ui/timeline/layoutListWindow.ts +145 -0
  110. package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
  111. package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
  112. package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
  113. package/src/core/codex.ts +0 -124
  114. package/src/ui/AppShell.tsx +0 -706
  115. package/src/ui/ProviderPicker.tsx +0 -321
  116. package/src/ui/StaticTranscriptItem.tsx +0 -56
  117. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  118. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  119. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  120. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  121. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  122. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
  123. /package/src/ui/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
  124. /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
  125. /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
@@ -1,9 +1,9 @@
1
1
  import React, { memo, useEffect, useMemo, useRef, useState } from "react";
2
2
  import { Box, Text, useFocus, useInput, useStdin } from "ink";
3
- import { formatContextCompact } from "../core/providerRuntime/contextMetadata.js";
4
- import type { ModelSpec } from "../core/models/modelSpecs.js";
5
- import type { ExternalCliStatus, UIState } from "../session/types.js";
6
- import { FOCUS_IDS } from "./focus.js";
3
+ import { formatContextCompact } from "../../core/providerRuntime/contextMetadata.js";
4
+ import type { ModelSpec } from "../../core/models/modelSpecs.js";
5
+ import type { ExternalCliStatus, UIState } from "../../session/types.js";
6
+ import { FOCUS_IDS } from "../input/focus.js";
7
7
  import {
8
8
  createInputViewport,
9
9
  deleteInputBackward,
@@ -14,22 +14,22 @@ import {
14
14
  moveCursorRight,
15
15
  normalizeInputText,
16
16
  normalizeCursorOffset,
17
- } from "./inputBuffer.js";
18
- import { getModeDisplaySpec } from "./modeDisplay.js";
17
+ } from "../input/inputBuffer.js";
18
+ import { getModeDisplaySpec } from "../render/modeDisplay.js";
19
19
  import { ActivityIndicator } from "./ActivityIndicator.js";
20
20
  import { measureRunFooterRows, MemoizedRunFooter } from "./RunFooter.js";
21
- import { useTheme } from "./theme.js";
22
- import { clampVisualText, getShellWidth, type Layout } from "./layout.js";
23
- import { getTextWidth, splitTextAtColumn } from "./textLayout.js";
24
- import { useThrottledValue } from "./useThrottledValue.js";
25
- import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
26
- import { getStdinDebugState, traceInputDebug } from "../core/inputDebug.js";
27
- import * as renderDebug from "../core/perf/renderDebug.js";
21
+ import { useTheme } from "../theme.js";
22
+ import { clampVisualText, getShellWidth, type Layout } from "../layout.js";
23
+ import { getTextWidth, splitTextAtColumn } from "../render/textLayout.js";
24
+ import { useThrottledValue } from "../useThrottledValue.js";
25
+ import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
26
+ import { getStdinDebugState, traceInputDebug } from "../../core/debug/inputDebug.js";
27
+ import * as renderDebug from "../../core/perf/renderDebug.js";
28
28
  import { AnimatedStatusText } from "./AnimatedStatusText.js";
29
29
  import { isAnimatedBusyState } from "./busyStatusAnimation.js";
30
30
  import { Spinner } from "./Spinner.js";
31
- import type { TerminalSelectionProfile } from "../core/terminal/terminalSelection.js";
32
- import { getSlashCommandSuggestions, type CommandSuggestion } from "./slashCommands.js";
31
+ import type { TerminalSelectionProfile } from "../../core/terminal/terminalSelection.js";
32
+ import { getSlashCommandSuggestions, type CommandSuggestion } from "../input/slashCommands.js";
33
33
 
34
34
  // ─── Types & constants ────────────────────────────────────────────────────────
35
35
 
@@ -98,13 +98,13 @@ export function getTokenBarDisplay(tokensUsed: number, modelSpec: ModelSpec) {
98
98
  interface BottomComposerProps {
99
99
  layout: Layout;
100
100
  uiState: UIState;
101
- themeName?: string;
102
- mode?: string;
101
+ themeName?: string;
102
+ mode?: string;
103
103
  model?: string;
104
104
  footerModelDisplay?: string;
105
105
  reasoningLevel?: string;
106
106
  contextDisplay?: string;
107
- planMode?: boolean;
107
+ planMode?: boolean;
108
108
  showBusyLoader?: boolean;
109
109
  tokensUsed?: number;
110
110
  modelSpec?: ModelSpec;
@@ -138,11 +138,11 @@ export interface BottomComposerMeasureParams {
138
138
  mode?: string;
139
139
  model?: string;
140
140
  reasoningLevel?: string;
141
- tokensUsed?: number;
142
- modelSpec?: ModelSpec;
143
- value: string;
144
- cursor: number;
145
- }
141
+ tokensUsed?: number;
142
+ modelSpec?: ModelSpec;
143
+ value: string;
144
+ cursor: number;
145
+ }
146
146
 
147
147
  export interface CommandSuggestionState {
148
148
  showSuggestions: boolean;
@@ -176,9 +176,9 @@ export function shouldRenderBusyFooter(layout: Layout, uiState: UIState): boolea
176
176
  return false;
177
177
  }
178
178
 
179
- export function getComposerToFooterGapRows(layout: Layout): number {
180
- return layout.mode === "micro" || layout.rows <= 24 ? 0 : 1;
181
- }
179
+ export function getComposerToFooterGapRows(layout: Layout): number {
180
+ return 0;
181
+ }
182
182
 
183
183
  export function getCommandSuggestionState({
184
184
  value,
@@ -206,11 +206,11 @@ export function getCommandSuggestionState({
206
206
  }
207
207
 
208
208
  export function measureBottomComposerRows({
209
- layout,
210
- uiState,
211
- value,
212
- cursor,
213
- }: BottomComposerMeasureParams): number {
209
+ layout,
210
+ uiState,
211
+ value,
212
+ cursor,
213
+ }: BottomComposerMeasureParams): number {
214
214
  if (shouldRenderBusyFooter(layout, uiState)) {
215
215
  return measureRunFooterRows();
216
216
  }
@@ -236,24 +236,27 @@ export function measureBottomComposerRows({
236
236
  inputLocked,
237
237
  });
238
238
 
239
- // ALWAYS reserve 1 row for the status line to prevent height shifting between idle and busy states.
240
- const statusLineReservedRows = 1;
241
- const showMetadata = layout.mode !== "micro";
242
- const bottomPadding = layout.mode === "micro" || layout.rows <= 24 ? 0 : 1;
243
- const footerGapRows = getComposerToFooterGapRows(layout);
244
-
245
- const visiblePromptRows = inputLocked ? 1 : promptViewport.visibleRows.length;
246
-
239
+ const bottomPadding = layout.mode === "compact" ? 0 : 1;
240
+ const footerGapRows = getComposerToFooterGapRows(layout);
241
+ const visibleStatusLine = getVisibleComposerStatusLine({
242
+ uiState,
243
+ value: normalizedValue,
244
+ allowCommands,
245
+ });
246
+ const transientStatusRows = visibleStatusLine.length > 0 ? 1 : 0;
247
+
248
+ const visiblePromptRows = inputLocked ? 1 : promptViewport.visibleRows.length;
249
+
247
250
  return (
248
251
  visiblePromptRows
249
- + 2
250
- + (commandSuggestionState.reserveSuggestionRow ? 1 : 0)
251
- + footerGapRows
252
- + statusLineReservedRows
253
- + (showMetadata ? 1 : 0)
254
- + bottomPadding
255
- );
256
- }
252
+ + 2
253
+ + (commandSuggestionState.reserveSuggestionRow ? 1 : 0)
254
+ + footerGapRows
255
+ + transientStatusRows
256
+ + 1
257
+ + bottomPadding
258
+ );
259
+ }
257
260
 
258
261
  function getExternalCliLabel(providerId: string): string | null {
259
262
  if (providerId === "google") return "Gemini CLI";
@@ -340,54 +343,54 @@ function getPlaceholder(persona: ComposerPersona): string {
340
343
 
341
344
  // ─── Component ────────────────────────────────────────────────────────────────
342
345
 
343
- function renderFooterRuntime(displayStr: string, theme: any) {
344
- // e.g. "Claude Code CLI / Sonnet 4.6 (Low)"
345
- const slashIndex = displayStr.indexOf("/");
346
- if (slashIndex === -1) {
347
- return <Text color={theme.model} wrap="truncate">{displayStr}</Text>;
348
- }
349
- const providerPart = displayStr.substring(0, slashIndex).trim();
350
- let remaining = displayStr.substring(slashIndex + 1).trim();
351
-
352
- const parenIndex = remaining.indexOf("(");
353
- if (parenIndex === -1) {
354
- return (
355
- <Box flexDirection="row" overflow="hidden">
356
- <Text color={theme.provider}>{providerPart}</Text>
357
- <Text color={theme.textMuted}>{" / "}</Text>
358
- <Text color={theme.model}>{remaining}</Text>
359
- </Box>
360
- );
361
- }
362
-
363
- const modelPart = remaining.substring(0, parenIndex).trim();
364
- let reasoningPart = remaining.substring(parenIndex + 1).trim();
365
- if (reasoningPart.endsWith(")")) {
366
- reasoningPart = reasoningPart.substring(0, reasoningPart.length - 1).trim();
367
- }
368
-
369
- return (
370
- <Box flexDirection="row" overflow="hidden">
371
- <Text color={theme.provider}>{providerPart}</Text>
372
- <Text color={theme.textMuted}>{" / "}</Text>
373
- <Text color={theme.model}>{modelPart}</Text>
374
- <Text color={theme.textMuted}>{" ("}</Text>
375
- <Text color={theme.accentMuted}>{reasoningPart}</Text>
376
- <Text color={theme.textMuted}>{")"}</Text>
377
- </Box>
378
- );
379
- }
380
-
346
+ function renderFooterRuntime(displayStr: string, theme: any) {
347
+ // e.g. "Claude Code CLI / Sonnet 4.6 (Low)"
348
+ const slashIndex = displayStr.indexOf("/");
349
+ if (slashIndex === -1) {
350
+ return <Text color={theme.model} wrap="truncate">{displayStr}</Text>;
351
+ }
352
+ const providerPart = displayStr.substring(0, slashIndex).trim();
353
+ let remaining = displayStr.substring(slashIndex + 1).trim();
354
+
355
+ const parenIndex = remaining.indexOf("(");
356
+ if (parenIndex === -1) {
357
+ return (
358
+ <Box flexDirection="row" overflow="hidden">
359
+ <Text color={theme.provider}>{providerPart}</Text>
360
+ <Text color={theme.textMuted}>{" / "}</Text>
361
+ <Text color={theme.model}>{remaining}</Text>
362
+ </Box>
363
+ );
364
+ }
365
+
366
+ const modelPart = remaining.substring(0, parenIndex).trim();
367
+ let reasoningPart = remaining.substring(parenIndex + 1).trim();
368
+ if (reasoningPart.endsWith(")")) {
369
+ reasoningPart = reasoningPart.substring(0, reasoningPart.length - 1).trim();
370
+ }
371
+
372
+ return (
373
+ <Box flexDirection="row" overflow="hidden">
374
+ <Text color={theme.provider}>{providerPart}</Text>
375
+ <Text color={theme.textMuted}>{" / "}</Text>
376
+ <Text color={theme.model}>{modelPart}</Text>
377
+ <Text color={theme.textMuted}>{" ("}</Text>
378
+ <Text color={theme.accentMuted}>{reasoningPart}</Text>
379
+ <Text color={theme.textMuted}>{")"}</Text>
380
+ </Box>
381
+ );
382
+ }
383
+
381
384
  export function BottomComposer({
382
385
  layout,
383
386
  uiState,
384
- themeName = "purple",
385
- mode = "",
387
+ themeName = "purple",
388
+ mode = "",
386
389
  model = "",
387
390
  footerModelDisplay,
388
391
  reasoningLevel = "",
389
392
  contextDisplay,
390
- planMode = false,
393
+ planMode = false,
391
394
  showBusyLoader = true,
392
395
  tokensUsed = 0,
393
396
  modelSpec = FALLBACK_MODEL_SPEC,
@@ -564,9 +567,10 @@ export function BottomComposer({
564
567
  .map((suggestion, index) => `${index === selectedIndex ? "›" : "·"} ${suggestion.cmd}`)
565
568
  .join(" ");
566
569
 
567
- const rawStatusLine = getVisibleComposerStatusLine({ uiState, value, allowCommands, activeProviderId, runElapsedSeconds, externalCliStatus });
568
- const showStatusLine = rawStatusLine.length > 0;
569
- const footerGapRows = getComposerToFooterGapRows(layout);
570
+ const rawStatusLine = getVisibleComposerStatusLine({ uiState, value, allowCommands, activeProviderId, runElapsedSeconds, externalCliStatus });
571
+ const showStatusLine = rawStatusLine.length > 0;
572
+ const showTransientStatusRow = showStatusLine || inputLocked || !!selectionProfile;
573
+ const footerGapRows = getComposerToFooterGapRows(layout);
570
574
 
571
575
  const promptViewport = useMemo(
572
576
  () => createInputViewport({
@@ -849,9 +853,9 @@ export function BottomComposer({
849
853
  }
850
854
  }, { isActive: isFocused });
851
855
 
852
- const tokenDisplay = getTokenBarDisplay(tokensUsed, modelSpec);
853
- const reasoningSuffix = reasoningLevel ? ` (${reasoningLevel})` : "";
854
- const footerRuntimeDisplay = footerModelDisplay ?? `${model}${reasoningSuffix}`;
856
+ const tokenDisplay = getTokenBarDisplay(tokensUsed, modelSpec);
857
+ const reasoningSuffix = reasoningLevel ? ` (${reasoningLevel})` : "";
858
+ const footerRuntimeDisplay = footerModelDisplay ?? `${model}${reasoningSuffix}`;
855
859
  const isAnswerMode = persona === "answer";
856
860
  const showBusyFooter = shouldRenderBusyFooter(layout, uiState);
857
861
  const promptPrefixColor = inputLocked ? theme.textDim : theme.text;
@@ -904,11 +908,11 @@ export function BottomComposer({
904
908
  return <MemoizedRunFooter uiState={uiState} showBusyLoader={showBusyLoader} onCancel={onCancel} onQuit={onQuit} />;
905
909
  }
906
910
 
907
- return (
908
- <Box flexDirection="column" paddingBottom={layoutMode === "micro" || crampedViewport ? 0 : 1} width="100%">
909
- {isAnswerMode ? (
910
- // Answer mode: Highlighted prompt
911
- <Box
911
+ return (
912
+ <Box flexDirection="column" paddingBottom={layoutMode === "compact" ? 0 : 1} width="100%">
913
+ {isAnswerMode ? (
914
+ // Answer mode: Highlighted prompt
915
+ <Box
912
916
  flexDirection="column"
913
917
  width="100%"
914
918
  paddingX={1}
@@ -918,47 +922,17 @@ export function BottomComposer({
918
922
  >
919
923
  {promptLine}
920
924
  </Box>
921
- ) : (
922
- // Normal mode: clean prompt in rounded border
923
- <Box
924
- flexDirection="column"
925
+ ) : (
926
+ // Normal mode: clean prompt in rounded border
927
+ <Box
928
+ flexDirection="column"
925
929
  width="100%"
926
930
  paddingX={1}
927
931
  paddingY={0}
928
932
  borderStyle="round"
929
933
  borderColor={theme.border}
930
934
  >
931
- {promptLine}
932
- </Box>
933
- )}
934
-
935
- {layoutMode !== "micro" && (
936
- <Box paddingLeft={1} paddingRight={1} marginTop={0} width="100%" justifyContent="space-between">
937
- <Box flexGrow={1} flexShrink={1} overflow="hidden">
938
- {renderFooterRuntime(footerRuntimeDisplay, theme)}
939
- </Box>
940
- <Box flexShrink={0}>
941
- {contextDisplay ? (
942
- <Box flexDirection="row">
943
- <Text color={theme.textMuted}>Context: </Text>
944
- <Text color={theme.context}>{contextDisplay}</Text>
945
- </Box>
946
- ) : tokenDisplay.hasKnownLimit ? (
947
- <Box flexDirection="row">
948
- <Text color={theme.textMuted}>Context: </Text>
949
- <Text color={theme.context}>{tokenDisplay.usedText}</Text>
950
- <Text color={theme.textDim}>
951
- {" / "}{tokenDisplay.limitText}
952
- {tokenDisplay.percentage !== null ? ` · ${tokenDisplay.isEstimatedLimit ? "~" : ""}${tokenDisplay.percentage}%` : ""}
953
- </Text>
954
- </Box>
955
- ) : (
956
- <Box flexDirection="row">
957
- <Text color={theme.textMuted}>Context: </Text>
958
- <Text color={theme.textDim}>Unknown</Text>
959
- </Box>
960
- )}
961
- </Box>
935
+ {promptLine}
962
936
  </Box>
963
937
  )}
964
938
 
@@ -972,22 +946,21 @@ export function BottomComposer({
972
946
  <Box height={footerGapRows} />
973
947
  )}
974
948
 
975
- {/* Always reserve the status line height */}
976
- <Box paddingX={1} marginTop={0} height={1} width="100%" justifyContent="space-between" overflow="hidden">
977
- {showStatusLine && (
978
- <>
979
- <Box flexShrink={1} flexGrow={1} overflow="hidden" flexDirection="row">
980
- {!!getExternalCliLabel(activeProviderId ?? "") && uiState.kind === "THINKING" && (
949
+ {showTransientStatusRow && (
950
+ <Box paddingX={1} marginTop={0} height={1} width="100%" justifyContent="space-between" overflow="hidden">
951
+ <>
952
+ <Box flexShrink={1} flexGrow={1} overflow="hidden" flexDirection="row">
953
+ {!!getExternalCliLabel(activeProviderId ?? "") && uiState.kind === "THINKING" && (
981
954
  <>
982
955
  <Spinner color={theme.accent} />
983
956
  <Text>{" "}</Text>
984
957
  </>
985
958
  )}
986
- <AnimatedStatusText
987
- baseText={rawStatusLine}
988
- isActive={!getExternalCliLabel(activeProviderId ?? "") && inputLocked && showBusyLoader}
989
- isError={persona === "error"}
990
- />
959
+ <AnimatedStatusText
960
+ baseText={rawStatusLine}
961
+ isActive={!getExternalCliLabel(activeProviderId ?? "") && inputLocked && showBusyLoader}
962
+ isError={persona === "error"}
963
+ />
991
964
  </Box>
992
965
  {inputLocked && (
993
966
  <Box flexShrink={0}>
@@ -997,15 +970,43 @@ export function BottomComposer({
997
970
  {!inputLocked && selectionProfile && (
998
971
  <Box flexShrink={0}>
999
972
  <Text color={theme.textDim}>{selectionProfile.shortHint}</Text>
1000
- </Box>
1001
- )}
1002
- </>
1003
- )}
1004
- </Box>
1005
-
973
+ </Box>
974
+ )}
975
+ </>
976
+ </Box>
977
+ )}
978
+
979
+ <Box paddingLeft={1} paddingRight={1} marginTop={0} width="100%" justifyContent="space-between">
980
+ <Box flexGrow={1} flexShrink={1} overflow="hidden">
981
+ {renderFooterRuntime(footerRuntimeDisplay, theme)}
982
+ </Box>
983
+ <Box flexShrink={0}>
984
+ {contextDisplay ? (
985
+ <Box flexDirection="row">
986
+ <Text color={theme.textMuted}>Context: </Text>
987
+ <Text color={theme.context}>{contextDisplay}</Text>
988
+ </Box>
989
+ ) : tokenDisplay.hasKnownLimit ? (
990
+ <Box flexDirection="row">
991
+ <Text color={theme.textMuted}>Context: </Text>
992
+ <Text color={theme.context}>{tokenDisplay.usedText}</Text>
993
+ <Text color={theme.textDim}>
994
+ {" / "}{tokenDisplay.limitText}
995
+ {tokenDisplay.percentage !== null ? ` · ${tokenDisplay.isEstimatedLimit ? "~" : ""}${tokenDisplay.percentage}%` : ""}
996
+ </Text>
997
+ </Box>
998
+ ) : (
999
+ <Box flexDirection="row">
1000
+ <Text color={theme.textMuted}>Context: </Text>
1001
+ <Text color={theme.textDim}>Unknown</Text>
1002
+ </Box>
1003
+ )}
1004
+ </Box>
1005
+ </Box>
1006
+
1006
1007
  </Box>
1007
1008
  );
1008
- }
1009
+ }
1009
1010
 
1010
1011
  // Helper to extract the relevant uiState kind for comparison
1011
1012
  function getUiStateKey(uiState: UIState): string {
@@ -1037,12 +1038,12 @@ export const MemoizedBottomComposer = memo(BottomComposer, (prev, next) => {
1037
1038
  if (prev.value !== next.value) return false;
1038
1039
  if (prev.cursor !== next.cursor) return false;
1039
1040
 
1040
- // Re-render if display props change
1041
- if (prev.mode !== next.mode) return false;
1042
- if (prev.model !== next.model) return false;
1043
- if (prev.footerModelDisplay !== next.footerModelDisplay) return false;
1044
- if (prev.reasoningLevel !== next.reasoningLevel) return false;
1045
- if (prev.contextDisplay !== next.contextDisplay) return false;
1041
+ // Re-render if display props change
1042
+ if (prev.mode !== next.mode) return false;
1043
+ if (prev.model !== next.model) return false;
1044
+ if (prev.footerModelDisplay !== next.footerModelDisplay) return false;
1045
+ if (prev.reasoningLevel !== next.reasoningLevel) return false;
1046
+ if (prev.contextDisplay !== next.contextDisplay) return false;
1046
1047
  if (prev.planMode !== next.planMode) return false;
1047
1048
  if (prev.showBusyLoader !== next.showBusyLoader) return false;
1048
1049
  if (prev.tokensUsed !== next.tokensUsed) return false;
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import type { ReactNode } from "react";
3
3
  import { Box, Text } from "ink";
4
- import { useTheme } from "./theme.js";
5
- import { getVisualWidth } from "./layout.js";
4
+ import { useTheme } from "../theme.js";
5
+ import { getVisualWidth } from "../layout.js";
6
6
 
7
7
  interface DashCardProps {
8
8
  cols: number;
@@ -1,8 +1,8 @@
1
1
  import React, { memo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import type { UIState } from "../session/types.js";
4
- import * as renderDebug from "../core/perf/renderDebug.js";
5
- import { useTheme } from "./theme.js";
3
+ import type { UIState } from "../../session/types.js";
4
+ import * as renderDebug from "../../core/perf/renderDebug.js";
5
+ import { useTheme } from "../theme.js";
6
6
  import { AnimatedStatusText } from "./AnimatedStatusText.js";
7
7
  import { isAnimatedBusyState } from "./busyStatusAnimation.js";
8
8
 
@@ -0,0 +1,108 @@
1
+ import React from "react";
2
+ import { Box, Text } from "ink";
3
+ import { useTheme } from "../theme.js";
4
+ import type { Layout } from "../layout.js";
5
+ import * as renderDebug from "../../core/perf/renderDebug.js";
6
+
7
+ export type RuntimeAvailability = "available" | "checking" | "reconnecting" | "unavailable" | "unknown";
8
+
9
+ export interface RuntimeStatusBarProps {
10
+ layout: Layout;
11
+ modelDisplay: string;
12
+ contextDisplay?: string;
13
+ availability?: RuntimeAvailability;
14
+ }
15
+
16
+ export function measureRuntimeStatusBarRows(): number {
17
+ return 1;
18
+ }
19
+
20
+ function getAvailabilityLabel(availability: RuntimeAvailability | undefined): string | null {
21
+ if (availability === "checking") return "checking";
22
+ if (availability === "reconnecting") return "reconnecting";
23
+ if (availability === "unavailable") return "unavailable";
24
+ if (availability === "unknown") return "Unknown";
25
+ return null;
26
+ }
27
+
28
+ function renderRuntimeDisplay(displayStr: string, theme: ReturnType<typeof useTheme>) {
29
+ const safeDisplay = displayStr.trim() || "Local / Detecting...";
30
+ const slashIndex = safeDisplay.indexOf("/");
31
+ if (slashIndex === -1) {
32
+ return <Text color={theme.model} wrap="truncate">{safeDisplay}</Text>;
33
+ }
34
+
35
+ const providerPart = safeDisplay.substring(0, slashIndex).trim() || "Local";
36
+ const remaining = safeDisplay.substring(slashIndex + 1).trim() || "Detecting...";
37
+ const parenIndex = remaining.indexOf("(");
38
+
39
+ if (parenIndex === -1) {
40
+ return (
41
+ <Box flexDirection="row" overflow="hidden">
42
+ <Text color={theme.provider}>{providerPart}</Text>
43
+ <Text color={theme.textMuted}>{" / "}</Text>
44
+ <Text color={theme.model}>{remaining}</Text>
45
+ </Box>
46
+ );
47
+ }
48
+
49
+ const modelPart = remaining.substring(0, parenIndex).trim();
50
+ let reasoningPart = remaining.substring(parenIndex + 1).trim();
51
+ if (reasoningPart.endsWith(")")) {
52
+ reasoningPart = reasoningPart.substring(0, reasoningPart.length - 1).trim();
53
+ }
54
+
55
+ return (
56
+ <Box flexDirection="row" overflow="hidden">
57
+ <Text color={theme.provider}>{providerPart}</Text>
58
+ <Text color={theme.textMuted}>{" / "}</Text>
59
+ <Text color={theme.model}>{modelPart}</Text>
60
+ <Text color={theme.textMuted}>{" ("}</Text>
61
+ <Text color={theme.accentMuted}>{reasoningPart}</Text>
62
+ <Text color={theme.textMuted}>{")"}</Text>
63
+ </Box>
64
+ );
65
+ }
66
+
67
+ export function RuntimeStatusBar({
68
+ layout,
69
+ modelDisplay,
70
+ contextDisplay,
71
+ availability = "available",
72
+ }: RuntimeStatusBarProps) {
73
+ const theme = useTheme();
74
+ const availabilityLabel = getAvailabilityLabel(availability);
75
+ const safeModelDisplay = modelDisplay.trim() || "Local / Detecting...";
76
+ const safeContextDisplay = contextDisplay?.trim() || "Unknown";
77
+ renderDebug.useRenderDebug("RuntimeStatusBar", {
78
+ modelDisplay: safeModelDisplay,
79
+ contextDisplay: safeContextDisplay,
80
+ availability,
81
+ cols: layout.cols,
82
+ });
83
+
84
+ return (
85
+ <Box
86
+ height={measureRuntimeStatusBarRows()}
87
+ width="100%"
88
+ paddingLeft={1}
89
+ paddingRight={1}
90
+ justifyContent="space-between"
91
+ overflow="hidden"
92
+ >
93
+ <Box flexGrow={1} flexShrink={1} overflow="hidden" flexDirection="row">
94
+ {renderRuntimeDisplay(safeModelDisplay, theme)}
95
+ {availabilityLabel && (
96
+ <>
97
+ <Text color={theme.textMuted}>{" · "}</Text>
98
+ <Text color={availability === "unavailable" ? theme.warning : theme.info}>{availabilityLabel}</Text>
99
+ </>
100
+ )}
101
+ </Box>
102
+ <Box flexShrink={0}>
103
+ <Text color={theme.textMuted}>{layout.cols >= 48 ? "Context: " : "Ctx: "}</Text>
104
+ <Text color={safeContextDisplay === "Unknown" ? theme.textDim : theme.context}>{safeContextDisplay}</Text>
105
+ </Box>
106
+ </Box>
107
+ );
108
+ }
@@ -1,6 +1,6 @@
1
1
  import React, { startTransition, useEffect, useState } from "react";
2
2
  import { Text } from "ink";
3
- import { useTheme } from "./theme.js";
3
+ import { useTheme } from "../theme.js";
4
4
 
5
5
  const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
6