@golba98/codexa 1.0.3 → 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 (85) 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 +533 -275
  5. package/src/commands/handler.ts +2 -2
  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/perf/renderDebug.ts +10 -6
  22. package/src/core/providerLauncher/registry.ts +30 -14
  23. package/src/core/providerLauncher/types.ts +11 -9
  24. package/src/core/providerLauncher/workspaceConfig.ts +41 -26
  25. package/src/core/providerRuntime/anthropic.ts +7 -1
  26. package/src/core/providerRuntime/antigravity.ts +305 -0
  27. package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
  28. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  29. package/src/core/providerRuntime/contextMetadata.ts +12 -24
  30. package/src/core/providerRuntime/local.ts +129 -51
  31. package/src/core/providerRuntime/models.ts +22 -11
  32. package/src/core/providerRuntime/registry.ts +58 -31
  33. package/src/core/providerRuntime/types.ts +19 -14
  34. package/src/core/providers/codexSubprocess.ts +2 -2
  35. package/src/core/providers/types.ts +1 -1
  36. package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
  37. package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
  38. package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
  39. package/src/core/terminal/clearFrameBoundary.ts +814 -0
  40. package/src/core/terminal/frameLock.ts +109 -0
  41. package/src/core/terminal/inkRenderReset.ts +123 -0
  42. package/src/core/terminal/terminalControl.ts +22 -0
  43. package/src/core/terminal/terminalTitle.ts +16 -102
  44. package/src/core/{channel.ts → version/channel.ts} +1 -1
  45. package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
  46. package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
  47. package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
  48. package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
  49. package/src/headless/execRunner.ts +2 -2
  50. package/src/index.tsx +43 -89
  51. package/src/session/appSession.ts +10 -7
  52. package/src/session/chatLifecycle.ts +1 -1
  53. package/src/session/liveRenderScheduler.ts +1 -1
  54. package/src/session/types.ts +1 -1
  55. package/src/ui/AppShell.tsx +672 -706
  56. package/src/ui/AttachmentImportPanel.tsx +2 -2
  57. package/src/ui/BottomComposer.tsx +145 -144
  58. package/src/ui/ModelPickerScreen.tsx +216 -36
  59. package/src/ui/ModelReasoningPicker.tsx +1 -1
  60. package/src/ui/PlanReviewPanel.tsx +1 -1
  61. package/src/ui/ProviderPicker.tsx +735 -321
  62. package/src/ui/RuntimeStatusBar.tsx +108 -0
  63. package/src/ui/SelectionPanel.tsx +4 -0
  64. package/src/ui/SettingsPanel.tsx +1 -1
  65. package/src/ui/TextEntryPanel.tsx +1 -1
  66. package/src/ui/Timeline.tsx +1619 -1470
  67. package/src/ui/TopHeader.tsx +46 -30
  68. package/src/ui/TranscriptShell.tsx +322 -0
  69. package/src/ui/TurnGroup.tsx +2 -2
  70. package/src/ui/UpdateAvailableCard.tsx +3 -3
  71. package/src/ui/UpdatePromptPanel.tsx +16 -22
  72. package/src/ui/layout.ts +298 -24
  73. package/src/ui/layoutListWindow.ts +145 -0
  74. package/src/ui/logoVariants.ts +4 -8
  75. package/src/ui/runtimeDisplay.ts +15 -3
  76. package/src/ui/textLayout.ts +15 -4
  77. package/src/ui/timelineMeasure.ts +194 -122
  78. package/src/core/codex.ts +0 -124
  79. package/src/ui/StaticTranscriptItem.tsx +0 -56
  80. /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
  81. /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
  82. /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
  83. /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
  84. /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
  85. /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
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
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Shared utility for windowing large lists in limited terminal rows.
3
+ */
4
+
5
+ export interface ListWindowOptions {
6
+ itemCount: number;
7
+ selectedIndex: number;
8
+ availableRows: number;
9
+ /** Fixed rows consumed by headers, borders, titles, etc. */
10
+ chromeRows: number;
11
+ /** Whether to show "↑ N more" and "↓ N more" indicators. */
12
+ showIndicators?: boolean;
13
+ /** Whether to show "Showing X-Y of N" range line. */
14
+ showRangeLine?: boolean;
15
+ }
16
+
17
+ export interface ListWindowResult {
18
+ start: number;
19
+ end: number;
20
+ visibleCount: number;
21
+ showAbove: boolean;
22
+ showBelow: boolean;
23
+ showRange: boolean;
24
+ hiddenAbove: number;
25
+ hiddenBelow: number;
26
+ /** Total rows consumed including chrome and indicators. */
27
+ totalRows: number;
28
+ }
29
+
30
+ /**
31
+ * Calculates a contiguous window of items that fits within available rows,
32
+ * ensuring the selected item is always visible.
33
+ */
34
+ export function calculateListWindow({
35
+ itemCount,
36
+ selectedIndex,
37
+ availableRows,
38
+ chromeRows,
39
+ showIndicators = true,
40
+ showRangeLine = false,
41
+ }: ListWindowOptions): ListWindowResult {
42
+ if (itemCount === 0) {
43
+ return {
44
+ start: 0,
45
+ end: 0,
46
+ visibleCount: 0,
47
+ showAbove: false,
48
+ showBelow: false,
49
+ showRange: false,
50
+ hiddenAbove: 0,
51
+ hiddenBelow: 0,
52
+ totalRows: chromeRows,
53
+ };
54
+ }
55
+
56
+ const rangeReserved = showRangeLine ? 1 : 0;
57
+
58
+ // If everything fits including the range line, just show everything.
59
+ if (itemCount + chromeRows + rangeReserved <= availableRows) {
60
+ return {
61
+ start: 0,
62
+ end: itemCount,
63
+ visibleCount: itemCount,
64
+ showAbove: false,
65
+ showBelow: false,
66
+ showRange: showRangeLine,
67
+ hiddenAbove: 0,
68
+ hiddenBelow: 0,
69
+ totalRows: itemCount + chromeRows + rangeReserved,
70
+ };
71
+ }
72
+
73
+ // Otherwise we must window.
74
+ // We reserve space for indicators if requested.
75
+ // We MUST fit within availableRows.
76
+ const indicatorReserved = showIndicators ? 2 : 0;
77
+ const baseReserved = chromeRows + indicatorReserved + rangeReserved;
78
+
79
+ let itemRows = Math.max(1, availableRows - baseReserved);
80
+ let start = 0;
81
+ let end = 0;
82
+
83
+ // Iteratively refine to account for dynamic indicators.
84
+ for (let attempt = 0; attempt < 3; attempt++) {
85
+ const visibleCount = Math.max(1, Math.min(itemCount, itemRows));
86
+
87
+ // Center selected item if possible
88
+ start = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
89
+ if (start + visibleCount > itemCount) {
90
+ start = Math.max(0, itemCount - visibleCount);
91
+ }
92
+ end = Math.min(itemCount, start + visibleCount);
93
+
94
+ const actualAbove = start > 0;
95
+ const actualBelow = end < itemCount;
96
+ const dynamicReserved = chromeRows
97
+ + (showRangeLine ? 1 : 0)
98
+ + (showIndicators && actualAbove ? 1 : 0)
99
+ + (showIndicators && actualBelow ? 1 : 0);
100
+
101
+ const nextItemRows = Math.max(1, availableRows - dynamicReserved);
102
+ if (nextItemRows === itemRows) break;
103
+ itemRows = nextItemRows;
104
+ }
105
+
106
+ // Final visibility check for selectedIndex
107
+ if (selectedIndex < start) {
108
+ start = selectedIndex;
109
+ end = Math.min(itemCount, start + itemRows);
110
+ } else if (selectedIndex >= end) {
111
+ end = selectedIndex + 1;
112
+ start = Math.max(0, end - itemRows);
113
+ }
114
+
115
+ // Final clamping to ensure we don't exceed availableRows if indicators are shown
116
+ let finalAbove = showIndicators && start > 0;
117
+ let finalBelow = showIndicators && end < itemCount;
118
+
119
+ while (end - start + chromeRows + (showRangeLine ? 1 : 0) + (finalAbove ? 1 : 0) + (finalBelow ? 1 : 0) > availableRows && end - start > 1) {
120
+ // Shrink window from the side further from selectedIndex
121
+ if (selectedIndex - start > end - 1 - selectedIndex) {
122
+ start++;
123
+ } else {
124
+ end--;
125
+ }
126
+ finalAbove = showIndicators && start > 0;
127
+ finalBelow = showIndicators && end < itemCount;
128
+ }
129
+
130
+ return {
131
+ start,
132
+ end,
133
+ visibleCount: end - start,
134
+ showAbove: finalAbove,
135
+ showBelow: finalBelow,
136
+ showRange: showRangeLine,
137
+ hiddenAbove: start,
138
+ hiddenBelow: itemCount - end,
139
+ totalRows: (end - start)
140
+ + chromeRows
141
+ + (showRangeLine ? 1 : 0)
142
+ + (finalAbove ? 1 : 0)
143
+ + (finalBelow ? 1 : 0),
144
+ };
145
+ }
@@ -10,8 +10,8 @@ import { getTextWidth } from "./textLayout.js";
10
10
  // The companion `wrap="truncate"` rule keeps each row on exactly one terminal
11
11
  // line regardless of the surrounding Ink flex layout.
12
12
 
13
- // Canonical Codexa brand wordmark — the ██ block art is the authoritative logo.
14
- // Always shown on any normal-width terminal (≥ LOGO_LARGE_MIN_COLS cols).
13
+ // Canonical Codexa brand wordmark — the ██ block art is the authoritative
14
+ // large logo for wide/max layouts.
15
15
  export const CODEXA_WORDMARK = [
16
16
  " ██████╗ ██████╗ ██████╗ ███████╗██╗ ██╗ █████╗ ",
17
17
  "██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝██╔══██╗",
@@ -48,16 +48,13 @@ export const LOGO_COMPACT_MIN_COLS = 48;
48
48
  // Minimum terminal rows each variant needs to render without crowding out the
49
49
  // metadata + composer. A wide-but-short terminal (e.g. VS Code's bottom panel)
50
50
  // must step DOWN to a smaller logo instead of dropping straight to text-only.
51
- export const LOGO_LARGE_MIN_ROWS = 24;
51
+ export const LOGO_LARGE_MIN_ROWS = 35;
52
52
  export const LOGO_MEDIUM_MIN_ROWS = 16;
53
53
  export const LOGO_COMPACT_MIN_ROWS = 12;
54
54
 
55
- // LOGO_MEDIUM is kept as an exported constant but intentionally omitted from
56
- // LOGO_VARIANTS. At 72+ cols, LOGO_LARGE_MIN_COLS = LOGO_MEDIUM_MIN_COLS = 72,
57
- // so LOGO_LARGE always wins. Below 72 cols the viewport is too narrow for the
58
- // thin ASCII art to add value over the compact single-line fallback.
59
55
  const LOGO_VARIANTS: readonly { logo: readonly string[]; minCols: number; minRows: number }[] = [
60
56
  { logo: LOGO_LARGE, minCols: LOGO_LARGE_MIN_COLS, minRows: LOGO_LARGE_MIN_ROWS },
57
+ { logo: LOGO_MEDIUM, minCols: LOGO_MEDIUM_MIN_COLS, minRows: LOGO_MEDIUM_MIN_ROWS },
61
58
  { logo: LOGO_COMPACT, minCols: LOGO_COMPACT_MIN_COLS, minRows: LOGO_COMPACT_MIN_ROWS },
62
59
  ];
63
60
 
@@ -92,7 +89,6 @@ export function selectLogoVariantForViewport(cols: number, rows: number): readon
92
89
  if (process.env["CODEXA_COMPACT_LOGO"] === "1") {
93
90
  return rows >= LOGO_COMPACT_MIN_ROWS ? LOGO_COMPACT : [];
94
91
  }
95
- if (cols >= LOGO_LARGE_MIN_COLS) return LOGO_LARGE;
96
92
  for (const variant of LOGO_VARIANTS) {
97
93
  if (cols >= variant.minCols && rows >= variant.minRows) {
98
94
  return variant.logo;
@@ -5,6 +5,7 @@ import type { ModelSpec } from "../core/models/modelSpecs.js";
5
5
  import type { ModelContextMetadata } from "../core/providerRuntime/contextMetadata.js";
6
6
  import { contextMetadataToModelSpec, formatContextCompact } from "../core/providerRuntime/contextMetadata.js";
7
7
  import type { ActiveProviderRoute } from "../core/providerRuntime/types.js";
8
+ import { getAntigravityModelLabel } from "../core/providerRuntime/antigravity.js";
8
9
 
9
10
  export interface ActiveRuntimeDisplayInput {
10
11
  route: ActiveProviderRoute;
@@ -29,6 +30,7 @@ const PROVIDER_DISPLAY: Record<string, string> = {
29
30
  anthropic: "Claude Code CLI",
30
31
  google: "Gemini CLI",
31
32
  local: "Local",
33
+ antigravity: "Antigravity CLI",
32
34
  };
33
35
 
34
36
  function formatContextLimit(value: number): string {
@@ -53,6 +55,9 @@ function getModelLabel(route: ActiveProviderRoute, capability?: CodexModelCapabi
53
55
  if (route.providerId === "anthropic") {
54
56
  return capability?.label ?? route.modelId;
55
57
  }
58
+ if (route.providerId === "antigravity") {
59
+ return getAntigravityModelLabel(route.modelId);
60
+ }
56
61
  return route.modelId;
57
62
  }
58
63
 
@@ -65,7 +70,10 @@ export function buildActiveRuntimeDisplay({
65
70
  contextMetadata = null,
66
71
  }: ActiveRuntimeDisplayInput): ActiveRuntimeDisplay {
67
72
  const providerLabel = PROVIDER_DISPLAY[route.providerId] ?? route.providerId;
68
- const reasoning = formatReasoningLabel(route.reasoning ?? reasoningLevel);
73
+ const rawReasoning = route.providerId === "antigravity"
74
+ ? route.reasoning
75
+ : route.reasoning ?? reasoningLevel;
76
+ const reasoning = rawReasoning ? formatReasoningLabel(rawReasoning) : null;
69
77
  const modelLabel = getModelLabel(route, modelCapability);
70
78
  const validContextMetadata = isContextForRoute(contextMetadata, route) ? contextMetadata : null;
71
79
  const contextDisplay = validContextMetadata?.contextLength != null
@@ -82,8 +90,12 @@ export function buildActiveRuntimeDisplay({
82
90
 
83
91
  return {
84
92
  providerLabel,
85
- modelDisplay: `${providerLabel} / ${modelLabel} / reasoning: ${reasoning}`,
86
- footerModelDisplay: `${providerLabel} / ${modelLabel} (${reasoning})`,
93
+ modelDisplay: reasoning
94
+ ? `${providerLabel} / ${modelLabel} / reasoning: ${reasoning}`
95
+ : `${providerLabel} / ${modelLabel}`,
96
+ footerModelDisplay: reasoning
97
+ ? `${providerLabel} / ${modelLabel} (${reasoning})`
98
+ : `${providerLabel} / ${modelLabel}`,
87
99
  contextDisplay,
88
100
  modeLabel: formatModeLabel(mode),
89
101
  modelSpec,