@golba98/codexa 1.0.2 → 1.0.4

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 (206) hide show
  1. package/README.md +396 -100
  2. package/bin/codexa.js +62 -144
  3. package/package.json +14 -8
  4. package/src/app.tsx +596 -306
  5. package/src/commands/handler.ts +6 -6
  6. package/src/config/buildInfo.ts +2 -2
  7. package/src/config/layeredConfig.ts +1 -1
  8. package/src/config/persistence.ts +10 -0
  9. package/src/config/runtimeConfig.ts +1 -1
  10. package/src/config/settings.ts +8 -16
  11. package/src/config/trustStore.ts +1 -1
  12. package/src/config/updateCheckCache.ts +19 -1
  13. package/src/core/README.md +52 -0
  14. package/src/core/agent/loop.ts +282 -0
  15. package/src/core/agent/protocol.ts +211 -0
  16. package/src/core/agent/tools.ts +414 -0
  17. package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
  18. package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
  19. package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
  20. package/src/core/debug/modelStateDebug.ts +34 -0
  21. package/src/core/executables/antigravityExecutable.ts +48 -0
  22. package/src/core/executables/codexExecutable.ts +1 -0
  23. package/src/core/executables/executableResolver.ts +65 -43
  24. package/src/core/perf/renderDebug.ts +10 -6
  25. package/src/core/process/processValidation.ts +9 -5
  26. package/src/core/providerLauncher/launcher.ts +59 -42
  27. package/src/core/providerLauncher/registry.ts +30 -14
  28. package/src/core/providerLauncher/types.ts +11 -9
  29. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  30. package/src/core/providerRuntime/anthropic.ts +7 -1
  31. package/src/core/providerRuntime/antigravity.ts +305 -0
  32. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  33. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  34. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  35. package/src/core/providerRuntime/local.ts +129 -51
  36. package/src/core/providerRuntime/models.ts +22 -11
  37. package/src/core/providerRuntime/registry.ts +58 -31
  38. package/src/core/providerRuntime/types.ts +19 -14
  39. package/src/core/providers/codexSubprocess.ts +2 -2
  40. package/src/core/providers/types.ts +1 -1
  41. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  42. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  43. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  44. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  45. package/src/core/terminal/frameLock.ts +109 -0
  46. package/src/core/terminal/inkRenderReset.ts +123 -0
  47. package/src/core/terminal/terminalControl.ts +22 -0
  48. package/src/core/terminal/terminalTitle.ts +16 -102
  49. package/src/core/version/channel.ts +23 -0
  50. package/src/core/version/updateCheck.ts +193 -0
  51. package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
  52. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  53. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  54. package/src/headless/execRunner.ts +2 -2
  55. package/src/index.tsx +43 -89
  56. package/src/session/appSession.ts +10 -7
  57. package/src/session/chatLifecycle.ts +1 -1
  58. package/src/session/liveRenderScheduler.ts +1 -1
  59. package/src/session/types.ts +3 -2
  60. package/src/ui/ActionRequiredBlock.tsx +5 -5
  61. package/src/ui/ActivityBars.tsx +3 -3
  62. package/src/ui/ActivityIndicator.tsx +6 -6
  63. package/src/ui/AgentBlock.tsx +6 -6
  64. package/src/ui/AnimatedStatusText.tsx +1 -1
  65. package/src/ui/AppShell.tsx +670 -719
  66. package/src/ui/AttachmentImportPanel.tsx +8 -8
  67. package/src/ui/AuthPanel.tsx +20 -20
  68. package/src/ui/BottomComposer.tsx +158 -118
  69. package/src/ui/DashCard.tsx +3 -3
  70. package/src/ui/Markdown.tsx +17 -17
  71. package/src/ui/ModelPickerScreen.tsx +222 -42
  72. package/src/ui/ModelReasoningPicker.tsx +15 -15
  73. package/src/ui/Panel.tsx +3 -3
  74. package/src/ui/PlanActionPicker.tsx +6 -6
  75. package/src/ui/PlanReviewPanel.tsx +9 -9
  76. package/src/ui/ProviderPicker.tsx +735 -321
  77. package/src/ui/RunFooter.tsx +3 -3
  78. package/src/ui/RuntimeStatusBar.tsx +108 -0
  79. package/src/ui/SelectionPanel.tsx +8 -4
  80. package/src/ui/SettingsPanel.tsx +9 -9
  81. package/src/ui/Spinner.tsx +1 -1
  82. package/src/ui/TextEntryPanel.tsx +11 -11
  83. package/src/ui/ThinkingBlock.tsx +8 -8
  84. package/src/ui/Timeline.tsx +1625 -1472
  85. package/src/ui/TopHeader.tsx +437 -293
  86. package/src/ui/TranscriptShell.tsx +322 -0
  87. package/src/ui/TurnGroup.tsx +33 -33
  88. package/src/ui/UpdateAvailableCard.tsx +41 -0
  89. package/src/ui/UpdatePromptPanel.tsx +197 -0
  90. package/src/ui/focus.ts +3 -0
  91. package/src/ui/layout.ts +299 -25
  92. package/src/ui/layoutListWindow.ts +145 -0
  93. package/src/ui/logoVariants.ts +103 -0
  94. package/src/ui/modeDisplay.ts +12 -12
  95. package/src/ui/runtimeDisplay.ts +112 -0
  96. package/src/ui/textLayout.ts +15 -4
  97. package/src/ui/theme.tsx +274 -395
  98. package/src/ui/timelineMeasure.ts +218 -136
  99. package/scripts/audit-codexa-capabilities.mjs +0 -466
  100. package/scripts/gen-build-info.mjs +0 -33
  101. package/scripts/smoke-terminal-bench.mjs +0 -35
  102. package/src/appRenderStability.test.ts +0 -131
  103. package/src/commands/handler.test.ts +0 -655
  104. package/src/config/launchArgs.test.ts +0 -189
  105. package/src/config/layeredConfig.test.ts +0 -143
  106. package/src/config/persistence.test.ts +0 -114
  107. package/src/config/runtimeConfig.test.ts +0 -218
  108. package/src/config/settings.test.ts +0 -155
  109. package/src/config/trustStore.test.ts +0 -29
  110. package/src/core/attachments.test.ts +0 -155
  111. package/src/core/auth/codexAuth.test.ts +0 -68
  112. package/src/core/cleanupFastFail.test.ts +0 -76
  113. package/src/core/codex.ts +0 -124
  114. package/src/core/codexExecArgs.test.ts +0 -195
  115. package/src/core/codexLaunch.test.ts +0 -205
  116. package/src/core/codexPrompt.test.ts +0 -252
  117. package/src/core/executables/codexExecutable.test.ts +0 -212
  118. package/src/core/executables/executableResolver.test.ts +0 -129
  119. package/src/core/executables/geminiExecutable.test.ts +0 -116
  120. package/src/core/executables/pathSanityScan.test.ts +0 -47
  121. package/src/core/githubDiagnostics.test.ts +0 -92
  122. package/src/core/hollowResponseFormat.test.ts +0 -58
  123. package/src/core/launchContext.test.ts +0 -157
  124. package/src/core/models/codexCapabilities.test.ts +0 -45
  125. package/src/core/models/codexModelCapabilities.test.ts +0 -246
  126. package/src/core/models/modelSpecs.test.ts +0 -283
  127. package/src/core/perf/renderDebug.test.ts +0 -230
  128. package/src/core/planStorage.test.ts +0 -143
  129. package/src/core/process/CommandRunner.test.ts +0 -105
  130. package/src/core/projectInstructions.test.ts +0 -50
  131. package/src/core/providerLauncher/launcher.test.ts +0 -238
  132. package/src/core/providerLauncher/registry.test.ts +0 -324
  133. package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
  134. package/src/core/providerRuntime/anthropic.test.ts +0 -1120
  135. package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
  136. package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
  137. package/src/core/providerRuntime/gemini.test.ts +0 -437
  138. package/src/core/providerRuntime/lmstudio.test.ts +0 -168
  139. package/src/core/providerRuntime/local.test.ts +0 -787
  140. package/src/core/providerRuntime/registry.test.ts +0 -233
  141. package/src/core/providers/codexJsonStream.test.ts +0 -148
  142. package/src/core/providers/codexSubprocess.test.ts +0 -68
  143. package/src/core/providers/codexTranscript.test.ts +0 -284
  144. package/src/core/terminal/startupClear.test.ts +0 -55
  145. package/src/core/terminal/terminalCapabilities.test.ts +0 -93
  146. package/src/core/terminal/terminalControl.test.ts +0 -75
  147. package/src/core/terminal/terminalSanitize.test.ts +0 -22
  148. package/src/core/terminal/terminalSelection.test.ts +0 -42
  149. package/src/core/terminal/terminalTitle.test.ts +0 -328
  150. package/src/core/updateCheck.test.ts +0 -194
  151. package/src/core/updateCheck.ts +0 -172
  152. package/src/core/workspaceActivity.test.ts +0 -163
  153. package/src/core/workspaceGuard.test.ts +0 -151
  154. package/src/core/workspaceRoot.test.ts +0 -23
  155. package/src/exec.test.ts +0 -13
  156. package/src/headless/execArgs.test.ts +0 -147
  157. package/src/headless/execRunner.test.ts +0 -436
  158. package/src/index.test.tsx +0 -620
  159. package/src/session/appSession.test.ts +0 -897
  160. package/src/session/chatLifecycle.test.ts +0 -64
  161. package/src/session/liveRenderScheduler.test.ts +0 -201
  162. package/src/session/planFlow.test.ts +0 -103
  163. package/src/session/planTranscript.test.ts +0 -65
  164. package/src/session/promptRunSchedule.test.ts +0 -36
  165. package/src/ui/ActivityIndicator.test.tsx +0 -58
  166. package/src/ui/AgentBlock.test.ts +0 -6
  167. package/src/ui/AnimatedStatusText.test.ts +0 -16
  168. package/src/ui/AppShell.test.tsx +0 -1776
  169. package/src/ui/AttachmentImportPanel.test.tsx +0 -204
  170. package/src/ui/BottomComposer.test.ts +0 -674
  171. package/src/ui/CodexLogo.tsx +0 -55
  172. package/src/ui/Markdown.test.ts +0 -157
  173. package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
  174. package/src/ui/ModelPickerScreen.test.tsx +0 -99
  175. package/src/ui/ModelPickerState.test.tsx +0 -151
  176. package/src/ui/ModelReasoningPicker.test.tsx +0 -447
  177. package/src/ui/PlanReviewPanel.test.tsx +0 -267
  178. package/src/ui/PromptCardBorder.test.tsx +0 -161
  179. package/src/ui/ProviderPicker.test.tsx +0 -289
  180. package/src/ui/ProviderShortcut.test.tsx +0 -143
  181. package/src/ui/SettingsPanel.test.tsx +0 -233
  182. package/src/ui/StaticTranscriptItem.tsx +0 -56
  183. package/src/ui/Timeline.test.ts +0 -2067
  184. package/src/ui/TimelineNavigation.test.tsx +0 -201
  185. package/src/ui/TopHeader.test.tsx +0 -254
  186. package/src/ui/TurnGroup.test.tsx +0 -365
  187. package/src/ui/busyStatusAnimation.test.ts +0 -30
  188. package/src/ui/commandNormalize.test.ts +0 -142
  189. package/src/ui/diffRenderer.test.ts +0 -102
  190. package/src/ui/focusFlow.test.tsx +0 -1098
  191. package/src/ui/inputBuffer.test.ts +0 -151
  192. package/src/ui/layout.test.ts +0 -146
  193. package/src/ui/modeDisplay.test.ts +0 -42
  194. package/src/ui/runActivityView.test.ts +0 -89
  195. package/src/ui/runLifecycleView.test.tsx +0 -237
  196. package/src/ui/statusRenderIsolation.test.tsx +0 -654
  197. package/src/ui/terminalAnswerFormat.test.ts +0 -19
  198. package/src/ui/textLayout.test.ts +0 -18
  199. package/src/ui/themeFlow.test.ts +0 -53
  200. package/src/ui/timelineMeasureCache.test.ts +0 -986
  201. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  202. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  203. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  204. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  205. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  206. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
@@ -0,0 +1,197 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { Box, Text, useFocus, useInput } from "ink";
3
+ import { spawn } from "child_process";
4
+ import { useTheme } from "./theme.js";
5
+ import { CODEXA_NPM_PACKAGE, CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
6
+
7
+ type Phase = "menu" | "running" | "done" | "error";
8
+
9
+ const MENU_ITEMS = [
10
+ { label: "Update now" },
11
+ { label: "Skip" },
12
+ { label: "Skip until next version" },
13
+ ] as const;
14
+
15
+ interface UpdatePromptPanelProps {
16
+ focusId: string;
17
+ currentVersion: string;
18
+ latestVersion: string;
19
+ onSkip: () => void;
20
+ onSkipUntilNextVersion: (version: string) => void;
21
+ }
22
+
23
+ export function UpdatePromptPanel({
24
+ focusId,
25
+ currentVersion,
26
+ latestVersion,
27
+ onSkip,
28
+ onSkipUntilNextVersion,
29
+ }: UpdatePromptPanelProps) {
30
+ const theme = useTheme();
31
+ const { isFocused } = useFocus({ id: focusId, autoFocus: true });
32
+
33
+ const [phase, setPhase] = useState<Phase>("menu");
34
+ const [selectedIndex, setSelectedIndex] = useState(0);
35
+ const [outputLines, setOutputLines] = useState<string[]>([]);
36
+ const [errorMessage, setErrorMessage] = useState<string | null>(null);
37
+
38
+ const spawnStartedRef = useRef(false);
39
+
40
+ useInput((input, key) => {
41
+ if (key.escape) {
42
+ onSkip();
43
+ return;
44
+ }
45
+ if (phase === "menu") {
46
+ if (key.upArrow || input === "k") {
47
+ setSelectedIndex((i) => Math.max(0, i - 1));
48
+ return;
49
+ }
50
+ if (key.downArrow || input === "j") {
51
+ setSelectedIndex((i) => Math.min(MENU_ITEMS.length - 1, i + 1));
52
+ return;
53
+ }
54
+ if (key.return) {
55
+ if (selectedIndex === 0) {
56
+ setPhase("running");
57
+ } else if (selectedIndex === 1) {
58
+ onSkip();
59
+ } else {
60
+ onSkipUntilNextVersion(latestVersion);
61
+ }
62
+ return;
63
+ }
64
+ } else if (phase === "done" || phase === "error") {
65
+ if (key.return) {
66
+ onSkip();
67
+ }
68
+ }
69
+ }, { isActive: isFocused });
70
+
71
+ useEffect(() => {
72
+ if (phase !== "running") return;
73
+ if (spawnStartedRef.current) return;
74
+ spawnStartedRef.current = true;
75
+
76
+ const child = spawn("npm", ["install", "-g", `${CODEXA_NPM_PACKAGE}@latest`], {
77
+ shell: false,
78
+ stdio: ["ignore", "pipe", "pipe"],
79
+ });
80
+
81
+ const appendLines = (buf: Buffer) => {
82
+ const lines = buf.toString("utf8").split(/\r?\n/).filter(Boolean);
83
+ if (lines.length > 0) {
84
+ setOutputLines((prev) => [...prev, ...lines]);
85
+ }
86
+ };
87
+
88
+ child.stdout?.on("data", appendLines);
89
+ child.stderr?.on("data", appendLines);
90
+
91
+ child.once("error", (err: Error) => {
92
+ setErrorMessage(err.message);
93
+ setPhase("error");
94
+ });
95
+
96
+ child.once("close", (code: number | null) => {
97
+ if (code === 0) {
98
+ setPhase("done");
99
+ } else {
100
+ setErrorMessage(`npm exited with code ${code ?? "unknown"}.`);
101
+ setPhase("error");
102
+ }
103
+ });
104
+
105
+ return () => {
106
+ try { child.kill(); } catch { /* ignore */ }
107
+ };
108
+ }, [phase]);
109
+
110
+ const footerText = phase === "menu"
111
+ ? "Esc to close · Enter to confirm"
112
+ : "Esc to close";
113
+
114
+ return (
115
+ <Box flexDirection="column" width="100%" marginTop={1}>
116
+ <Box
117
+ borderStyle="round"
118
+ borderColor={theme.border}
119
+ paddingX={2}
120
+ paddingY={1}
121
+ width="100%"
122
+ flexDirection="column"
123
+ >
124
+ <Box>
125
+ <Text color={theme.accent} bold>{`Update available: Codexa ${formatVersionLabel(latestVersion)}`}</Text>
126
+ </Box>
127
+ <Box marginTop={1}>
128
+ <Text color={theme.text}>{`${formatVersionLabel(currentVersion)} -> ${formatVersionLabel(latestVersion)}`}</Text>
129
+ </Box>
130
+ <Box>
131
+ <Text color={theme.textMuted}>{`Package: ${CODEXA_NPM_PACKAGE}`}</Text>
132
+ </Box>
133
+ <Box>
134
+ <Text color={theme.textMuted}>{`Run: ${CODEXA_UPDATE_COMMAND}`}</Text>
135
+ </Box>
136
+ </Box>
137
+
138
+ <Box
139
+ borderStyle="round"
140
+ borderColor={phase === "menu" ? theme.borderFocused : theme.border}
141
+ paddingX={2}
142
+ paddingY={1}
143
+ marginTop={1}
144
+ width="100%"
145
+ flexDirection="column"
146
+ >
147
+ {phase === "menu" && (
148
+ <>
149
+ {MENU_ITEMS.map((item, index) => (
150
+ <Box key={item.label}>
151
+ <Text color={index === selectedIndex ? theme.accent : theme.textMuted}>
152
+ {index === selectedIndex ? "› " : " "}
153
+ </Text>
154
+ <Text
155
+ color={index === selectedIndex ? theme.text : theme.textMuted}
156
+ bold={index === selectedIndex}
157
+ >
158
+ {`${index + 1}. ${item.label}`}
159
+ </Text>
160
+ </Box>
161
+ ))}
162
+ </>
163
+ )}
164
+
165
+ {phase === "running" && (
166
+ <>
167
+ <Text color={theme.text}>{`Installing Codexa ${latestVersion}...`}</Text>
168
+ {outputLines.map((line, i) => (
169
+ <Text key={i} color={theme.textMuted}>{line}</Text>
170
+ ))}
171
+ </>
172
+ )}
173
+
174
+ {phase === "done" && (
175
+ <>
176
+ <Text color={theme.success}>{"Codexa was updated successfully."}</Text>
177
+ <Text color={theme.textMuted}>{"Restart Codexa to use the new version."}</Text>
178
+ </>
179
+ )}
180
+
181
+ {phase === "error" && (
182
+ <>
183
+ <Text color={theme.error}>{"Update failed."}</Text>
184
+ {errorMessage != null && <Text color={theme.textMuted}>{errorMessage}</Text>}
185
+ {outputLines.slice(-5).map((line, i) => (
186
+ <Text key={i} color={theme.textDim}>{line}</Text>
187
+ ))}
188
+ </>
189
+ )}
190
+
191
+ <Box marginTop={1}>
192
+ <Text color={theme.textDim}>{footerText}</Text>
193
+ </Box>
194
+ </Box>
195
+ </Box>
196
+ );
197
+ }
package/src/ui/focus.ts CHANGED
@@ -18,6 +18,7 @@ export const FOCUS_IDS = {
18
18
  permissionsAddWritableRoot: "permissions-add-writable-root",
19
19
  permissionsRemoveWritableRoot: "permissions-remove-writable-root",
20
20
  importConfirmationPanel: "import-confirmation",
21
+ updatePrompt: "update-prompt",
21
22
  } as const;
22
23
 
23
24
  export type FocusTargetId = (typeof FOCUS_IDS)[keyof typeof FOCUS_IDS];
@@ -54,6 +55,8 @@ export function getFocusTargetForScreen(screen: Screen): FocusTargetId {
54
55
  return FOCUS_IDS.permissionsRemoveWritableRoot;
55
56
  case "import-confirmation":
56
57
  return FOCUS_IDS.importConfirmationPanel;
58
+ case "update-prompt":
59
+ return FOCUS_IDS.updatePrompt;
57
60
  case "main":
58
61
  default:
59
62
  return FOCUS_IDS.composer;
package/src/ui/layout.ts CHANGED
@@ -1,26 +1,38 @@
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
- export const STARTUP_FULL_MIN_COLS = BREAKPOINT_FULL;
35
+ export const STARTUP_FULL_MIN_COLS = 100; // matches LOGO_LARGE_MIN_COLS in logoVariants.ts
24
36
  export const STARTUP_FULL_MIN_BODY_ROWS = 4;
25
37
  export const STARTUP_FULL_SAFE_PADDING_ROWS = 1;
26
38
  export const STARTUP_COMPACT_INTRO_ROWS = 4;
@@ -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
+ }