@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,24 +1,24 @@
1
1
  import React, { memo } from "react";
2
2
  import { Box, Text } from "ink";
3
- import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
4
- import { formatCodexaBrandLabel } from "../core/channel.js";
5
- import type { RuntimeSummary } from "../config/runtimeConfig.js";
6
- import type { CodexAuthState } from "../core/auth/codexAuth.js";
7
- import { getAuthStateLabel } from "../core/auth/codexAuth.js";
8
- import * as renderDebug from "../core/perf/renderDebug.js";
9
- import { useTheme } from "./theme.js";
10
- import { clampVisualText, type Layout } from "./layout.js";
11
- import { getTextWidth } from "./textLayout.js";
3
+ import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
4
+ import { formatCodexaBrandLabel } from "../../core/version/channel.js";
5
+ import { formatVersionLabel } from "../../core/version/updateCheck.js";
6
+ import type { RuntimeSummary } from "../../config/runtimeConfig.js";
7
+ import type { CodexAuthState } from "../../core/auth/codexAuth.js";
8
+ import { getAuthStateLabel } from "../../core/auth/codexAuth.js";
9
+ import * as renderDebug from "../../core/perf/renderDebug.js";
10
+ import { useTheme } from "../theme.js";
11
+ import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "../layout.js";
12
+ import { getTextWidth } from "../render/textLayout.js";
12
13
  import { UPDATE_CARD_ROWS, UpdateAvailableCard } from "./UpdateAvailableCard.js";
13
14
  import {
14
15
  LOGO_LARGE,
16
+ LOGO_COMPACT,
15
17
  LOGO_COMPACT_MIN_COLS,
16
18
  LOGO_LARGE_MIN_COLS,
17
19
  LOGO_LARGE_MIN_ROWS,
18
- LOGO_MEDIUM_MIN_COLS,
19
- selectLogoVariantForViewport,
20
20
  getLogoWidth,
21
- } from "./logoVariants.js";
21
+ } from "../render/logoVariants.js";
22
22
 
23
23
  // Re-exported for backward compatibility with existing tests.
24
24
  export const HEADER_WORDMARK_LINES = LOGO_LARGE;
@@ -27,9 +27,8 @@ const HEADER_PADDING_COLUMNS = 2;
27
27
  const SHELL_GUTTER_COLUMNS = 1;
28
28
  // Require 130+ cols for wide side-by-side so the UpdateAvailableCard has room.
29
29
  const WIDE_HEADER_MIN_COLUMNS = 130;
30
- // Require 72+ cols (= LOGO_MEDIUM_MIN_COLS) for medium side-by-side both
31
- // LOGO_LARGE (≥100) and LOGO_MEDIUM (72–99) fit cleanly in side-by-side layout.
32
- const MEDIUM_HEADER_MIN_COLUMNS = LOGO_MEDIUM_MIN_COLS; // 72
30
+ // Require 72+ cols for medium side-by-side canonical-logo layout.
31
+ const MEDIUM_HEADER_MIN_COLUMNS = LOGO_LARGE_MIN_COLS;
33
32
  const MIN_SIDE_BY_SIDE_METADATA_WIDTH = 18;
34
33
  const STACKED_METADATA_GAP_ROWS = 1;
35
34
  // Gap row between the UpdateAvailableCard and the metadata lines.
@@ -84,7 +83,7 @@ function getMetadataRowCount(headerConfig: HeaderConfig): number {
84
83
  }
85
84
 
86
85
  function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bottomMarginRows: number } {
87
- if (layout.mode !== "full") {
86
+ if (!isDecorativeLayoutMode(layout.mode)) {
88
87
  return {
89
88
  topMarginRows: 0,
90
89
  bottomMarginRows: 0,
@@ -101,6 +100,19 @@ function getHeaderVerticalMargins(layout: Layout): { topMarginRows: number; bott
101
100
  };
102
101
  }
103
102
 
103
+ export function selectHeaderLogo(layout: Layout): readonly string[] {
104
+ if (process.env["CODEXA_NO_ASCII_LOGO"] === "1") return [];
105
+ const showNormalLogo =
106
+ process.env["CODEXA_NO_ASCII_LOGO"] !== "1" && (
107
+ layout.mode === "regular" ||
108
+ layout.mode === "expanded" ||
109
+ (layout.mode === "compact" && layout.cols >= 72)
110
+ );
111
+ if (!showNormalLogo) return [];
112
+ if (process.env["CODEXA_COMPACT_LOGO"] === "1") return LOGO_COMPACT;
113
+ return LOGO_LARGE;
114
+ }
115
+
104
116
  export function getHeaderHeroLayout(
105
117
  layout: Layout,
106
118
  headerConfig: HeaderConfig = HEADER_CONFIG_DEFAULTS,
@@ -110,17 +122,29 @@ export function getHeaderHeroLayout(
110
122
  const metadataRows = getMetadataRowCount(headerConfig);
111
123
  const contentWidth = getHeaderContentWidth(layout.cols);
112
124
 
113
- // Pick the largest logo that fits both the columns AND the rows. This degrades
114
- // large medium → compact on a short terminal instead of collapsing straight
115
- // to the flat text-only header.
116
- const logo = selectLogoVariantForViewport(layout.cols, layout.rows);
125
+ const showNormalLogo =
126
+ process.env["CODEXA_NO_ASCII_LOGO"] !== "1" && (
127
+ layout.mode === "regular" ||
128
+ layout.mode === "expanded" ||
129
+ (layout.mode === "compact" && layout.cols >= 72)
130
+ );
131
+
132
+ const placeMetadataBesideLogo = showNormalLogo && layout.cols >= 95;
133
+
134
+ let mode: HeaderHeroMode;
135
+ if (!showNormalLogo) {
136
+ mode = "compact";
137
+ } else if (placeMetadataBesideLogo) {
138
+ mode = layout.cols >= 130 ? "wide" : "medium";
139
+ } else {
140
+ mode = "narrow";
141
+ }
142
+
143
+ const logo = selectHeaderLogo(layout);
117
144
  const logoWidth = logo.length > 0 ? getLogoWidth(logo) : 0;
118
- const canRenderLogo = logo.length > 0;
119
145
 
120
- if (!canRenderLogo) {
121
- // Compact / text-only header. Show a recommended-size hint when the logo was
122
- // dropped purely for size (not because the user disabled ASCII art).
123
- const sizeHintRows = process.env["CODEXA_NO_ASCII_LOGO"] === "1" ? 0 : 1;
146
+ if (mode === "compact") {
147
+ // Compact / micro text-only header.
124
148
  return {
125
149
  mode: "compact",
126
150
  topMarginRows,
@@ -129,21 +153,13 @@ export function getHeaderHeroLayout(
129
153
  metadataGapRows: 0,
130
154
  logoRows: 1,
131
155
  metadataRows: 0,
132
- compactHintRows: sizeHintRows,
133
- totalRows: topMarginRows + 1 + sizeHintRows + bottomMarginRows,
156
+ compactHintRows: 0,
157
+ totalRows: topMarginRows + 1 + bottomMarginRows,
134
158
  };
135
159
  }
136
160
 
137
161
  const logoRowCount = logo.length;
138
162
  const metadataGapColumns = layout.cols >= WIDE_HEADER_MIN_COLUMNS ? 4 : 2;
139
- const metadataColumnWidth = contentWidth - logoWidth - metadataGapColumns;
140
- const canUseSideBySide = metadataRows === 0
141
- || metadataColumnWidth >= MIN_SIDE_BY_SIDE_METADATA_WIDTH;
142
- const mode: HeaderHeroMode = canUseSideBySide && layout.cols >= WIDE_HEADER_MIN_COLUMNS
143
- ? "wide"
144
- : canUseSideBySide && layout.cols >= MEDIUM_HEADER_MIN_COLUMNS
145
- ? "medium"
146
- : "narrow";
147
163
  const metadataGapRows = mode === "narrow" && metadataRows > 0 ? STACKED_METADATA_GAP_ROWS : 0;
148
164
 
149
165
  const isSideBySide = mode === "wide" || mode === "medium";
@@ -244,7 +260,7 @@ export function TopHeader({
244
260
  : authLabelRaw;
245
261
 
246
262
  const heroLayout = getHeaderHeroLayout(layout, headerConfig, !!updateAvailable);
247
- const selectedLogo = selectLogoVariantForViewport(layout.cols, layout.rows);
263
+ const selectedLogo = selectHeaderLogo(layout);
248
264
  const selectedLogoWidth = selectedLogo.length > 0 ? getLogoWidth(selectedLogo) : 0;
249
265
 
250
266
  const contentWidth = getHeaderContentWidth(layout.cols);
@@ -343,7 +359,7 @@ export function TopHeader({
343
359
  )}
344
360
  {metadataColumn}
345
361
  {updateAvailable && (
346
- <Text color={theme.warning} wrap="truncate">{`↑ Update: Codexa ${updateAvailable.latestVersion} is available run: npm install -g @golba98/codexa@latest`}</Text>
362
+ <Text color={theme.warning} wrap="truncate">{`Update available: Codexa ${formatVersionLabel(updateAvailable.latestVersion)} — Run: npm install -g @golba98/codexa@latest`}</Text>
347
363
  )}
348
364
  </Box>
349
365
  )}
@@ -418,4 +434,4 @@ export const MemoizedTopHeader = memo(TopHeader, (prev, next) => {
418
434
  export const MIN_LOGO_TERMINAL_WIDTH = LOGO_COMPACT_MIN_COLS;
419
435
 
420
436
  // Re-export for consumers that reference these constants directly.
421
- export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS };
437
+ export { LOGO_LARGE_MIN_COLS, LOGO_MEDIUM_MIN_COLS, LOGO_COMPACT_MIN_COLS } from "../render/logoVariants.js";
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import { Box, Text } from "ink";
3
- import { CODEXA_UPDATE_COMMAND } from "../core/updateCheck.js";
4
- import { clampVisualText } from "./layout.js";
5
- import { useTheme } from "./theme.js";
3
+ import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../../core/version/updateCheck.js";
4
+ import { clampVisualText } from "../layout.js";
5
+ import { useTheme } from "../theme.js";
6
6
 
7
7
  export const UPDATE_CARD_CONTENT_ROWS = 4; // title + available + using + command
8
8
  export const UPDATE_CARD_ROWS = UPDATE_CARD_CONTENT_ROWS + 2; // +2 for top/bottom border rows
@@ -33,8 +33,8 @@ export function UpdateAvailableCard({ latestVersion, currentVersion, width }: Up
33
33
  flexShrink={0}
34
34
  >
35
35
  <Text color={theme.text} bold>{clamp("Update available")}</Text>
36
- <Text color={theme.textMuted}>{clamp(`Codexa ${latestVersion} is available`)}</Text>
37
- <Text color={theme.textMuted}>{clamp(`You are using ${currentVersion}`)}</Text>
36
+ <Text color={theme.textMuted}>{clamp(`Codexa ${formatVersionLabel(latestVersion)}`)}</Text>
37
+ <Text color={theme.textMuted}>{clamp(`Using ${formatVersionLabel(currentVersion)}`)}</Text>
38
38
  <Text color={theme.textDim}>{clamp(command)}</Text>
39
39
  </Box>
40
40
  );
@@ -1,4 +1,4 @@
1
- import { formatTerminalAnswerInline } from "./terminalAnswerFormat.js";
1
+ import { formatTerminalAnswerInline } from "../render/terminalAnswerFormat.js";
2
2
 
3
3
  function stripOuterQuotes(s: string): string {
4
4
  if ((s.startsWith("'") && s.endsWith("'")) || (s.startsWith('"') && s.endsWith('"'))) {
@@ -1,4 +1,4 @@
1
- import type { Screen } from "../session/types.js";
1
+ import type { Screen } from "../../session/types.js";
2
2
 
3
3
  export const FOCUS_IDS = {
4
4
  composer: "composer",
@@ -1,6 +1,6 @@
1
- import type { WrappedTextRow } from "./textLayout.js";
2
- import { getTextWidth, normalizeLineBreaks, wrapTextRows } from "./textLayout.js";
3
- import { sanitizeTerminalInput } from "../core/terminal/terminalSanitize.js";
1
+ import type { WrappedTextRow } from "../render/textLayout.js";
2
+ import { getTextWidth, normalizeLineBreaks, wrapTextRows } from "../render/textLayout.js";
3
+ import { sanitizeTerminalInput } from "../../core/terminal/terminalSanitize.js";
4
4
 
5
5
  export type WrappedInputRow = WrappedTextRow;
6
6
 
package/src/ui/layout.ts CHANGED
@@ -1,23 +1,35 @@
1
1
  /**
2
2
  * Responsive layout constants and hook.
3
3
  *
4
- * Three modes based purely on terminal column count:
4
+ * Five modes based on both terminal columns and rows:
5
5
  *
6
- * full 110 cols full banner, side-by-side header info
7
- * compact 60–109 → single-line mini logo, stacked info, condensed composer
8
- * micro < 60 no logo, one-line header, ultra-compact composer
6
+ * micro very small rows/cols one-line shell
7
+ * compact normal short terminal compact shell, no large logo
8
+ * normal standard terminal compact header, roomy content
9
+ * wide large terminal → decorative header allowed
10
+ * max maximized terminal → full decorative layout
9
11
  */
10
12
 
11
- import { useEffect, useRef, useState } from "react";
13
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
12
14
  import { useStdout } from "ink";
13
15
  import stringWidth from "string-width";
14
16
  import * as renderDebug from "../core/perf/renderDebug.js";
15
-
16
- export const BREAKPOINT_FULL = 110; // ≥ this → full
17
- export const BREAKPOINT_COMPACT = 60; // ≥ this → compact; below → micro
17
+ import { setTerminalResizing, isTerminalResizing } from "../core/terminal/terminalControl.js";
18
+
19
+ export const BREAKPOINT_MAX = 180;
20
+ export const BREAKPOINT_WIDE = 140;
21
+ export const BREAKPOINT_NORMAL = 90;
22
+ export const BREAKPOINT_COMPACT = 60;
23
+ export const ROW_BREAKPOINT_MAX = 40;
24
+ export const ROW_BREAKPOINT_WIDE = 30;
25
+ export const ROW_BREAKPOINT_NORMAL = 20;
26
+ export const ROW_BREAKPOINT_COMPACT = 14;
27
+ export const MAX_CONTENT_WIDTH = 220;
28
+ export const MIN_TERMINAL_COLS = 20;
29
+ export const MIN_TERMINAL_ROWS = 10;
18
30
  export const MIN_VIEWPORT_COLS = 20;
19
31
  export const MIN_VIEWPORT_ROWS = 10;
20
- export const RESTORE_SETTLE_MS = 100;
32
+ export const RESTORE_SETTLE_MS = process.env.NODE_ENV === "test" ? 0 : 100;
21
33
  export const STARTUP_TINY_MIN_COLS = 40;
22
34
  export const STARTUP_TINY_MIN_ROWS = 14;
23
35
  export const STARTUP_FULL_MIN_COLS = 100; // matches LOGO_LARGE_MIN_COLS in logoVariants.ts
@@ -29,7 +41,7 @@ export const transcriptContentIndent = 4; // 2 for DashCard border + 2 for promp
29
41
  const DEFAULT_COLUMNS = 120;
30
42
  const DEFAULT_ROWS = 24;
31
43
 
32
- export type LayoutMode = "full" | "compact" | "micro";
44
+ export type LayoutMode = "compact" | "regular" | "expanded";
33
45
  export type StartupHeaderMode = "large" | "compact" | "tiny";
34
46
 
35
47
  export interface Layout {
@@ -38,11 +50,85 @@ export interface Layout {
38
50
  mode: LayoutMode;
39
51
  }
40
52
 
53
+ export type PanelLayout = {
54
+ mode: "compact" | "regular" | "expanded";
55
+ availableRows: number;
56
+ availableCols: number;
57
+ };
58
+
59
+ export type BottomChromeBudget = {
60
+ runtimeMetadataRows: number;
61
+ composerRows: number;
62
+ transientStatusRows: number;
63
+ bottomPaddingRows: number;
64
+ totalRows: number;
65
+ };
66
+
67
+ export type AppLayoutBudget = {
68
+ mode: LayoutMode;
69
+ rows: number;
70
+ cols: number;
71
+
72
+ headerRows: number;
73
+ headerGapRows: number;
74
+ panelStagePaddingY: number;
75
+
76
+ activePanelRows: number;
77
+ activePanelCols: number;
78
+
79
+ bottomChromeBudget: BottomChromeBudget;
80
+ composerRows: number;
81
+
82
+ showNormalLogo: boolean;
83
+ showCompactHeader: boolean;
84
+ placeMetadataBesideLogo: boolean;
85
+ placeMetadataBelowLogo: boolean;
86
+
87
+ // Backward compatibility fields:
88
+ transcriptRows: number;
89
+ panelRows: number;
90
+ showLargeLogo: boolean;
91
+ showPanelSeparators: boolean;
92
+ showPanelColumnHeaders: boolean;
93
+ };
94
+
95
+ export const PanelAvailableRowsContext = createContext<number | undefined>(undefined);
96
+ export const AppLayoutBudgetContext = createContext<AppLayoutBudget | undefined>(undefined);
97
+ export const PanelLayoutContext = createContext<PanelLayout | undefined>(undefined);
98
+
99
+ export interface ActivePanelLayout {
100
+ width: number;
101
+ height: number;
102
+ availableRows: number;
103
+ availableCols: number;
104
+ }
105
+
106
+ export const ActivePanelLayoutContext = createContext<ActivePanelLayout | undefined>(undefined);
107
+
108
+ export function usePanelAvailableRows(): number | undefined {
109
+ return useContext(PanelAvailableRowsContext);
110
+ }
111
+
112
+ export function useAppLayoutBudget(): AppLayoutBudget | undefined {
113
+ return useContext(AppLayoutBudgetContext);
114
+ }
115
+
116
+ export function useActivePanelLayout(): ActivePanelLayout | undefined {
117
+ return useContext(ActivePanelLayoutContext);
118
+ }
119
+
120
+ export function usePanelLayout(): PanelLayout | undefined {
121
+ return useContext(PanelLayoutContext);
122
+ }
123
+
41
124
  export interface TerminalViewport extends Layout {
42
125
  rawCols?: number;
43
126
  rawRows?: number;
127
+ contentWidth: number;
128
+ isCramped: boolean;
44
129
  unstable: boolean;
45
130
  layoutEpoch: number;
131
+ isResizing: boolean;
46
132
  }
47
133
 
48
134
  // ─── Dimension helpers ────────────────────────────────────────────────────────
@@ -67,6 +153,13 @@ export function isRenderableViewport(cols: number | undefined, rows: number | un
67
153
  && Math.floor(rows) >= MIN_VIEWPORT_ROWS;
68
154
  }
69
155
 
156
+ /** Returns true if the terminal is below the minimum supported size for a full UI. */
157
+ export function isCrampedTerminal(cols: number | undefined, rows: number | undefined): boolean {
158
+ const safeCols = normalizeDimension(cols, DEFAULT_COLUMNS);
159
+ const safeRows = normalizeDimension(rows, DEFAULT_ROWS);
160
+ return safeCols < MIN_TERMINAL_COLS || safeRows < MIN_TERMINAL_ROWS;
161
+ }
162
+
70
163
  /**
71
164
  * Leave a 1-column gutter so box-drawing borders never land exactly on the
72
165
  * terminal edge, which can trigger a horizontal scrollbar in some Windows hosts.
@@ -75,6 +168,20 @@ export function getShellWidth(cols: number | undefined): number {
75
168
  return Math.max(20, (cols ?? 120) - 1);
76
169
  }
77
170
 
171
+ /**
172
+ * Returns the width of the main content area, capped at MAX_CONTENT_WIDTH.
173
+ * This is used to center the UI in large terminals.
174
+ */
175
+ export function getContentWidth(cols: number | undefined): number {
176
+ const shellWidth = getShellWidth(cols);
177
+
178
+ if (shellWidth < 100) return shellWidth;
179
+ if (shellWidth < 150) return shellWidth - 4;
180
+ if (shellWidth < 200) return shellWidth - 8;
181
+
182
+ return Math.min(shellWidth - 12, MAX_CONTENT_WIDTH);
183
+ }
184
+
78
185
  export function getUsableShellWidth(cols: number | undefined, reservedColumns = 0): number {
79
186
  return Math.max(1, getShellWidth(cols) - reservedColumns);
80
187
  }
@@ -142,10 +249,118 @@ export function clampVisualText(text: string, maxWidth: number): string {
142
249
  return output + ellipsis;
143
250
  }
144
251
 
145
- function computeMode(cols: number): LayoutMode {
146
- if (cols >= BREAKPOINT_FULL) return "full";
147
- if (cols >= BREAKPOINT_COMPACT) return "compact";
148
- return "micro";
252
+ export function computeMode(cols: number, rows: number): LayoutMode {
253
+ if (rows <= 24 || cols <= 100) {
254
+ return "compact";
255
+ }
256
+ if (cols >= 140 && rows >= 30) {
257
+ return "expanded";
258
+ }
259
+ return "regular";
260
+ }
261
+
262
+ export function isDecorativeLayoutMode(mode: LayoutMode): boolean {
263
+ return mode === "expanded";
264
+ }
265
+
266
+ export function isCompactShellMode(mode: LayoutMode): boolean {
267
+ return mode === "compact" || mode === "regular";
268
+ }
269
+
270
+ export interface AppLayoutBudgetParams {
271
+ cols: number | undefined;
272
+ rows: number | undefined;
273
+ composerRows?: number;
274
+ panelHintRows?: number;
275
+ headerRows?: number;
276
+ headerGapRows?: number;
277
+ }
278
+
279
+ export function computeAppLayoutBudget({
280
+ cols,
281
+ rows,
282
+ composerRows = 4,
283
+ panelHintRows = 0,
284
+ headerRows,
285
+ headerGapRows,
286
+ }: AppLayoutBudgetParams): AppLayoutBudget {
287
+ const safeCols = normalizeDimension(cols, DEFAULT_COLUMNS);
288
+ const safeRows = normalizeDimension(rows, DEFAULT_ROWS);
289
+ const mode = computeMode(safeCols, safeRows);
290
+ const shellHeight = getShellHeight(safeRows);
291
+
292
+ const showNormalLogo =
293
+ process.env["CODEXA_NO_ASCII_LOGO"] !== "1" && (
294
+ mode === "regular" ||
295
+ mode === "expanded" ||
296
+ (mode === "compact" && safeCols >= 72)
297
+ );
298
+
299
+ const showCompactHeader = !showNormalLogo;
300
+
301
+ const placeMetadataBesideLogo =
302
+ showNormalLogo && safeCols >= 95;
303
+
304
+ const placeMetadataBelowLogo =
305
+ showNormalLogo && !placeMetadataBesideLogo;
306
+
307
+ const resolvedHeaderRows = headerRows ?? (showNormalLogo ? 6 : 1);
308
+ const resolvedHeaderGapRows = headerGapRows ?? (mode === "compact" ? 0 : 1);
309
+ const panelStagePaddingY = mode === "compact" ? 0 : 1;
310
+ const resolvedComposerRows = mode === "compact" ? 3 : Math.max(0, composerRows);
311
+ const runtimeMetadataRows = 1;
312
+ const transientStatusRows = 0;
313
+ const bottomPaddingRows = mode === "compact" ? 0 : 1;
314
+ const bottomChromeBudget: BottomChromeBudget = {
315
+ runtimeMetadataRows,
316
+ composerRows: resolvedComposerRows,
317
+ transientStatusRows,
318
+ bottomPaddingRows,
319
+ totalRows: runtimeMetadataRows + resolvedComposerRows + transientStatusRows + bottomPaddingRows,
320
+ };
321
+ const baseReservedRows =
322
+ resolvedHeaderRows +
323
+ resolvedHeaderGapRows +
324
+ panelStagePaddingY * 2 +
325
+ bottomChromeBudget.totalRows +
326
+ Math.max(0, panelHintRows);
327
+
328
+ const activePanelRows = Math.max(1, shellHeight - baseReservedRows);
329
+ const contentWidth = getContentWidth(safeCols);
330
+
331
+ const isCompact = mode === "compact";
332
+ const borderRows = 2;
333
+ const titleRows = 1;
334
+ const headerRowsInPanel = isCompact ? 0 : 1;
335
+ const panelChromeRows = borderRows + titleRows + headerRowsInPanel;
336
+ const innerAvailableRows = Math.max(1, activePanelRows - panelChromeRows);
337
+
338
+ const borderCols = 4;
339
+ const innerAvailableCols = Math.max(20, contentWidth - borderCols);
340
+
341
+ return {
342
+ mode,
343
+ rows: safeRows,
344
+ cols: safeCols,
345
+ headerRows: resolvedHeaderRows,
346
+ headerGapRows: resolvedHeaderGapRows,
347
+ panelStagePaddingY,
348
+ activePanelRows: innerAvailableRows,
349
+ activePanelCols: innerAvailableCols,
350
+ bottomChromeBudget,
351
+ composerRows: resolvedComposerRows,
352
+ showNormalLogo,
353
+ showCompactHeader,
354
+ placeMetadataBesideLogo,
355
+ placeMetadataBelowLogo,
356
+
357
+ // Backward compatibility fields:
358
+ transcriptRows: activePanelRows,
359
+ panelRows: innerAvailableRows,
360
+ showLargeLogo: mode === "expanded",
361
+ showPanelSeparators: mode === "expanded",
362
+ showPanelColumnHeaders: mode === "expanded",
363
+ };
149
364
  }
150
365
 
151
366
  export function createLayoutSnapshot(
@@ -154,16 +369,28 @@ export function createLayoutSnapshot(
154
369
  fallback: Layout = {
155
370
  cols: DEFAULT_COLUMNS,
156
371
  rows: DEFAULT_ROWS,
157
- mode: computeMode(DEFAULT_COLUMNS),
372
+ mode: computeMode(DEFAULT_COLUMNS, DEFAULT_ROWS),
158
373
  },
159
- ): Layout {
374
+ ): TerminalViewport {
160
375
  const nextCols = normalizeDimension(cols, fallback.cols);
161
376
  const nextRows = normalizeDimension(rows, fallback.rows);
162
377
 
163
- return {
378
+ const stableLayout = {
164
379
  cols: nextCols,
165
380
  rows: nextRows,
166
- mode: computeMode(nextCols),
381
+ mode: computeMode(nextCols, nextRows),
382
+ };
383
+
384
+ const isCramped = isCrampedTerminal(nextCols, nextRows);
385
+ const contentWidth = getContentWidth(nextCols);
386
+
387
+ return {
388
+ ...stableLayout,
389
+ contentWidth,
390
+ isCramped,
391
+ unstable: false,
392
+ layoutEpoch: 0,
393
+ isResizing: false,
167
394
  };
168
395
  }
169
396
 
@@ -175,6 +402,7 @@ export function createTerminalViewport(
175
402
  cols: number | undefined,
176
403
  rows: number | undefined,
177
404
  fallback?: TerminalViewport,
405
+ isResizing = false,
178
406
  ): TerminalViewport {
179
407
  const fallbackLayout = fallback
180
408
  ? { cols: fallback.cols, rows: fallback.rows, mode: fallback.mode }
@@ -184,12 +412,18 @@ export function createTerminalViewport(
184
412
  ? fallbackLayout
185
413
  : createLayoutSnapshot(cols, rows, fallbackLayout);
186
414
 
415
+ const isCramped = isCrampedTerminal(cols, rows);
416
+ const contentWidth = getContentWidth(stableLayout.cols);
417
+
187
418
  return {
188
419
  ...stableLayout,
189
420
  rawCols: cols,
190
421
  rawRows: rows,
422
+ contentWidth,
423
+ isCramped,
191
424
  unstable,
192
425
  layoutEpoch: fallback?.layoutEpoch ?? 0,
426
+ isResizing,
193
427
  };
194
428
  }
195
429
 
@@ -197,8 +431,21 @@ export function advanceTerminalViewport(
197
431
  current: TerminalViewport,
198
432
  cols: number | undefined,
199
433
  rows: number | undefined,
434
+ isResizing = false,
200
435
  ): TerminalViewport {
201
- const next = createTerminalViewport(cols, rows, current);
436
+ const next = createTerminalViewport(cols, rows, current, isResizing);
437
+
438
+ if (process.env.CODEXA_LAYOUT_DEBUG === "1") {
439
+ renderDebug.traceEvent("layout", "advanceViewport", {
440
+ cols: next.cols,
441
+ rows: next.rows,
442
+ contentWidth: next.contentWidth,
443
+ isCramped: next.isCramped,
444
+ isResizing: next.isResizing,
445
+ mode: next.mode,
446
+ });
447
+ }
448
+
202
449
  if (!next.unstable && current.unstable) {
203
450
  return {
204
451
  ...next,
@@ -221,9 +468,9 @@ export function useTerminalViewport(): TerminalViewport {
221
468
  const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
222
469
 
223
470
  useEffect(() => {
224
- const commit = () => {
471
+ const commit = (isResizing = false) => {
225
472
  setViewport((current) => {
226
- const nextViewport = advanceTerminalViewport(current, stdout.columns, stdout.rows);
473
+ const nextViewport = advanceTerminalViewport(current, stdout.columns, stdout.rows, isResizing);
227
474
  if (
228
475
  current.cols === nextViewport.cols &&
229
476
  current.rows === nextViewport.rows &&
@@ -231,7 +478,8 @@ export function useTerminalViewport(): TerminalViewport {
231
478
  current.unstable === nextViewport.unstable &&
232
479
  current.layoutEpoch === nextViewport.layoutEpoch &&
233
480
  current.rawCols === nextViewport.rawCols &&
234
- current.rawRows === nextViewport.rawRows
481
+ current.rawRows === nextViewport.rawRows &&
482
+ current.isResizing === nextViewport.isResizing
235
483
  ) {
236
484
  renderDebug.traceFlickerEvent("measurementUpdate", {
237
485
  result: "skipped",
@@ -239,6 +487,7 @@ export function useTerminalViewport(): TerminalViewport {
239
487
  rows: nextViewport.rows,
240
488
  mode: nextViewport.mode,
241
489
  unstable: nextViewport.unstable,
490
+ isResizing: nextViewport.isResizing,
242
491
  });
243
492
  return current;
244
493
  }
@@ -249,6 +498,7 @@ export function useTerminalViewport(): TerminalViewport {
249
498
  rows: nextViewport.rows,
250
499
  mode: nextViewport.mode,
251
500
  unstable: nextViewport.unstable,
501
+ isResizing: nextViewport.isResizing,
252
502
  });
253
503
  return nextViewport;
254
504
  });
@@ -264,13 +514,22 @@ export function useTerminalViewport(): TerminalViewport {
264
514
  rawCols: stdout.columns,
265
515
  rawRows: stdout.rows,
266
516
  });
267
- commit();
517
+
518
+ // Leading edge: immediately enter isResizing state but do NOT commit
519
+ // new dimensions yet. This freezes the layout to prevent tearing while
520
+ // dragging, and signals animations to pause.
521
+ setTerminalResizing(true);
522
+ setViewport((current) => ({ ...current, isResizing: true }));
523
+
268
524
  if (settleTimerRef.current) {
269
525
  clearTimeout(settleTimerRef.current);
270
526
  }
527
+
271
528
  settleTimerRef.current = setTimeout(() => {
272
529
  settleTimerRef.current = null;
273
- commit();
530
+ setTerminalResizing(false);
531
+ // Trailing edge: commit final dimensions and exit isResizing state.
532
+ commit(false);
274
533
  }, RESTORE_SETTLE_MS);
275
534
  };
276
535
 
@@ -285,3 +544,18 @@ export function useTerminalViewport(): TerminalViewport {
285
544
 
286
545
  return viewport;
287
546
  }
547
+
548
+ /**
549
+ * Calculate available vertical rows for active panels, falling back to a layout-based
550
+ * budget if availableRows is not explicitly provided.
551
+ */
552
+ export function getAvailableRowsForPanel(
553
+ layout: Layout,
554
+ passedAvailableRows?: number
555
+ ): number {
556
+ if (passedAvailableRows !== undefined) {
557
+ return passedAvailableRows;
558
+ }
559
+
560
+ return computeAppLayoutBudget({ cols: layout.cols, rows: layout.rows }).panelRows;
561
+ }