@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.
- package/README.md +17 -18
- package/bin/codexa.js +62 -144
- package/package.json +4 -3
- package/src/app.tsx +533 -275
- package/src/commands/handler.ts +2 -2
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +1 -1
- package/src/config/trustStore.ts +1 -1
- package/src/core/README.md +52 -0
- package/src/core/agent/loop.ts +282 -0
- package/src/core/agent/protocol.ts +211 -0
- package/src/core/agent/tools.ts +414 -0
- package/src/core/{codexExecArgs.ts → codex/codexExecArgs.ts} +2 -2
- package/src/core/{codexLaunch.ts → codex/codexLaunch.ts} +3 -3
- package/src/core/{codexPrompt.ts → codex/codexPrompt.ts} +3 -3
- package/src/core/debug/modelStateDebug.ts +34 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/executableResolver.ts +5 -1
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/providerLauncher/registry.ts +30 -14
- package/src/core/providerLauncher/types.ts +11 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +7 -1
- package/src/core/providerRuntime/antigravity.ts +305 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +268 -22
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/contextMetadata.ts +12 -24
- package/src/core/providerRuntime/local.ts +129 -51
- package/src/core/providerRuntime/models.ts +22 -11
- package/src/core/providerRuntime/registry.ts +58 -31
- package/src/core/providerRuntime/types.ts +19 -14
- package/src/core/providers/codexSubprocess.ts +2 -2
- package/src/core/providers/types.ts +1 -1
- package/src/core/{attachments.ts → shared/attachments.ts} +27 -4
- package/src/core/{cleanupFastFail.ts → shared/cleanupFastFail.ts} +1 -1
- package/src/core/{hollowResponseFormat.ts → shared/hollowResponseFormat.ts} +1 -1
- package/src/core/terminal/clearFrameBoundary.ts +814 -0
- package/src/core/terminal/frameLock.ts +109 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/terminalControl.ts +22 -0
- package/src/core/terminal/terminalTitle.ts +16 -102
- package/src/core/{channel.ts → version/channel.ts} +1 -1
- package/src/core/{updateCheck.ts → version/updateCheck.ts} +10 -5
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +9 -16
- package/src/core/{planStorage.ts → workspace/planStorage.ts} +2 -2
- package/src/core/{workspaceGuard.ts → workspace/workspaceGuard.ts} +97 -13
- package/src/headless/execRunner.ts +2 -2
- package/src/index.tsx +43 -89
- package/src/session/appSession.ts +10 -7
- package/src/session/chatLifecycle.ts +1 -1
- package/src/session/liveRenderScheduler.ts +1 -1
- package/src/session/types.ts +1 -1
- package/src/ui/AppShell.tsx +672 -706
- package/src/ui/AttachmentImportPanel.tsx +2 -2
- package/src/ui/BottomComposer.tsx +145 -144
- package/src/ui/ModelPickerScreen.tsx +216 -36
- package/src/ui/ModelReasoningPicker.tsx +1 -1
- package/src/ui/PlanReviewPanel.tsx +1 -1
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +4 -0
- package/src/ui/SettingsPanel.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +1 -1
- package/src/ui/Timeline.tsx +1619 -1470
- package/src/ui/TopHeader.tsx +46 -30
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +2 -2
- package/src/ui/UpdateAvailableCard.tsx +3 -3
- package/src/ui/UpdatePromptPanel.tsx +16 -22
- package/src/ui/layout.ts +298 -24
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +4 -8
- package/src/ui/runtimeDisplay.ts +15 -3
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/timelineMeasure.ts +194 -122
- package/src/core/codex.ts +0 -124
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- /package/src/core/{inputDebug.ts → debug/inputDebug.ts} +0 -0
- /package/src/core/{clipboard.ts → shared/clipboard.ts} +0 -0
- /package/src/core/{githubDiagnostics.ts → shared/githubDiagnostics.ts} +0 -0
- /package/src/core/{projectInstructions.ts → workspace/projectInstructions.ts} +0 -0
- /package/src/core/{workspaceActivity.ts → workspace/workspaceActivity.ts} +0 -0
- /package/src/core/{workspaceRoot.ts → workspace/workspaceRoot.ts} +0 -0
package/src/ui/TopHeader.tsx
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import React, { memo } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../config/settings.js";
|
|
4
|
-
import { formatCodexaBrandLabel } from "../core/channel.js";
|
|
4
|
+
import { formatCodexaBrandLabel } from "../core/version/channel.js";
|
|
5
|
+
import { formatVersionLabel } from "../core/version/updateCheck.js";
|
|
5
6
|
import type { RuntimeSummary } from "../config/runtimeConfig.js";
|
|
6
7
|
import type { CodexAuthState } from "../core/auth/codexAuth.js";
|
|
7
8
|
import { getAuthStateLabel } from "../core/auth/codexAuth.js";
|
|
8
9
|
import * as renderDebug from "../core/perf/renderDebug.js";
|
|
9
10
|
import { useTheme } from "./theme.js";
|
|
10
|
-
import { clampVisualText, type Layout } from "./layout.js";
|
|
11
|
+
import { clampVisualText, isDecorativeLayoutMode, type Layout, useAppLayoutBudget } from "./layout.js";
|
|
11
12
|
import { getTextWidth } from "./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
21
|
} from "./logoVariants.js";
|
|
22
22
|
|
|
@@ -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
|
|
31
|
-
|
|
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
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 (
|
|
121
|
-
// Compact / text-only header.
|
|
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:
|
|
133
|
-
totalRows: topMarginRows + 1 +
|
|
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 =
|
|
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">{
|
|
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 "./logoVariants.js";
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import React, { memo, useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { Box, Static } from "ink";
|
|
3
|
+
import type { RuntimeSummary } from "../config/runtimeConfig.js";
|
|
4
|
+
import type { CodexAuthState } from "../core/auth/codexAuth.js";
|
|
5
|
+
import * as renderDebug from "../core/perf/renderDebug.js";
|
|
6
|
+
import type { TimelineEvent, UIState } from "../session/types.js";
|
|
7
|
+
import {
|
|
8
|
+
buildActiveRenderItems,
|
|
9
|
+
buildIntroRenderItem,
|
|
10
|
+
buildStaticRenderItems,
|
|
11
|
+
buildTimelineItems,
|
|
12
|
+
TimelineRowView,
|
|
13
|
+
type TimelineItem,
|
|
14
|
+
} from "./Timeline.js";
|
|
15
|
+
import { getShellHeight, getShellWidth, resolveStartupHeaderMode, type TerminalViewport } from "./layout.js";
|
|
16
|
+
import {
|
|
17
|
+
buildNativeTranscriptParts,
|
|
18
|
+
type NativeTranscriptRowItem,
|
|
19
|
+
type TimelineRow,
|
|
20
|
+
} from "./timelineMeasure.js";
|
|
21
|
+
import { LOGO_COMPACT, LOGO_COMPACT_MIN_COLS, LOGO_LARGE, LOGO_MEDIUM, selectLogoVariant } from "./logoVariants.js";
|
|
22
|
+
|
|
23
|
+
type TranscriptStaticItem = NativeTranscriptRowItem & { type: "rows" };
|
|
24
|
+
type StaticRenderItem = TranscriptStaticItem;
|
|
25
|
+
|
|
26
|
+
export interface TranscriptShellProps {
|
|
27
|
+
layout: TerminalViewport;
|
|
28
|
+
authState: CodexAuthState;
|
|
29
|
+
workspaceLabel: string;
|
|
30
|
+
workspaceRoot?: string | null;
|
|
31
|
+
runtimeSummary?: RuntimeSummary | null;
|
|
32
|
+
staticEvents: TimelineEvent[];
|
|
33
|
+
activeEvents: TimelineEvent[];
|
|
34
|
+
uiState: UIState;
|
|
35
|
+
composer: React.ReactNode;
|
|
36
|
+
composerRows?: number;
|
|
37
|
+
verboseMode?: boolean;
|
|
38
|
+
clearCount?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Bumped whenever a width-changing resize forces the terminal's app.tsx-owned
|
|
41
|
+
* clear boundary to physically wipe the screen. Folded into <Static>'s key
|
|
42
|
+
* (not the whole component's) so already-flushed content reprints at the
|
|
43
|
+
* new width — without remounting the composer or anything else.
|
|
44
|
+
*/
|
|
45
|
+
repaintGeneration?: number;
|
|
46
|
+
visible?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function RowsBlock({ rows }: { rows: TimelineRow[] }) {
|
|
50
|
+
return (
|
|
51
|
+
<Box flexDirection="column">
|
|
52
|
+
{rows.map((row) => (
|
|
53
|
+
<TimelineRowView key={row.key} row={row} />
|
|
54
|
+
))}
|
|
55
|
+
</Box>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isTranscriptEvent(event: TimelineEvent): boolean {
|
|
60
|
+
return event.type === "user" || event.type === "assistant" || event.type === "run" || event.type === "shell";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isHomeScreenState({
|
|
64
|
+
staticEvents,
|
|
65
|
+
activeEvents,
|
|
66
|
+
uiState,
|
|
67
|
+
}: {
|
|
68
|
+
staticEvents: TimelineEvent[];
|
|
69
|
+
activeEvents: TimelineEvent[];
|
|
70
|
+
uiState: UIState;
|
|
71
|
+
}): boolean {
|
|
72
|
+
return uiState.kind === "IDLE"
|
|
73
|
+
&& !staticEvents.some(isTranscriptEvent)
|
|
74
|
+
&& !activeEvents.some(isTranscriptEvent);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getLogoVariantName(rows: readonly string[]): "large" | "medium" | "compact" | "wordmark" | "none" {
|
|
78
|
+
if (rows.length === 0) return process.env["CODEXA_NO_ASCII_LOGO"] === "1" ? "none" : "wordmark";
|
|
79
|
+
if (rows === LOGO_LARGE || rows.join("\n") === LOGO_LARGE.join("\n")) return "large";
|
|
80
|
+
if (rows === LOGO_MEDIUM || rows.join("\n") === LOGO_MEDIUM.join("\n")) return "medium";
|
|
81
|
+
if (rows === LOGO_COMPACT || rows.join("\n") === LOGO_COMPACT.join("\n")) return "compact";
|
|
82
|
+
return "wordmark";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getLogoHiddenReason({
|
|
86
|
+
startupHeaderMode,
|
|
87
|
+
logoVariant,
|
|
88
|
+
width,
|
|
89
|
+
}: {
|
|
90
|
+
startupHeaderMode: ReturnType<typeof resolveStartupHeaderMode>;
|
|
91
|
+
logoVariant: ReturnType<typeof getLogoVariantName>;
|
|
92
|
+
width: number;
|
|
93
|
+
}): string | null {
|
|
94
|
+
if (startupHeaderMode === "tiny") return "terminal-too-small";
|
|
95
|
+
if (process.env["CODEXA_NO_ASCII_LOGO"] === "1") return "CODEXA_NO_ASCII_LOGO";
|
|
96
|
+
if (logoVariant === "wordmark") return `no-ascii-variant-fits-width-${width}`;
|
|
97
|
+
if (logoVariant === "none") return `no-logo-variant-fits-width-${width}`;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function buildTranscriptItems({
|
|
102
|
+
layout,
|
|
103
|
+
authState,
|
|
104
|
+
workspaceLabel,
|
|
105
|
+
workspaceRoot,
|
|
106
|
+
runtimeSummary,
|
|
107
|
+
staticEvents,
|
|
108
|
+
activeEvents,
|
|
109
|
+
uiState,
|
|
110
|
+
composerRows,
|
|
111
|
+
verboseMode,
|
|
112
|
+
}: Pick<
|
|
113
|
+
TranscriptShellProps,
|
|
114
|
+
| "layout"
|
|
115
|
+
| "authState"
|
|
116
|
+
| "workspaceLabel"
|
|
117
|
+
| "workspaceRoot"
|
|
118
|
+
| "runtimeSummary"
|
|
119
|
+
| "staticEvents"
|
|
120
|
+
| "activeEvents"
|
|
121
|
+
| "uiState"
|
|
122
|
+
| "composerRows"
|
|
123
|
+
| "verboseMode"
|
|
124
|
+
>): { staticItems: TranscriptStaticItem[]; liveRows: TimelineRow[]; startupHeaderMode: ReturnType<typeof resolveStartupHeaderMode> } {
|
|
125
|
+
const staticItems = buildTimelineItems(staticEvents);
|
|
126
|
+
const activeItems = buildTimelineItems(activeEvents);
|
|
127
|
+
const turnIds = [...staticItems, ...activeItems]
|
|
128
|
+
.filter((item): item is Extract<TimelineItem, { type: "turn" }> => item.type === "turn")
|
|
129
|
+
.map((item) => item.turnId);
|
|
130
|
+
const startupHeaderMode = resolveStartupHeaderMode({
|
|
131
|
+
cols: layout.cols,
|
|
132
|
+
rows: layout.rows,
|
|
133
|
+
introRows: 8,
|
|
134
|
+
composerRows: composerRows ?? 5,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const parts = buildNativeTranscriptParts(
|
|
138
|
+
[
|
|
139
|
+
buildIntroRenderItem({
|
|
140
|
+
authState,
|
|
141
|
+
workspaceLabel,
|
|
142
|
+
layout,
|
|
143
|
+
providerLabel: runtimeSummary?.providerLabel ?? null,
|
|
144
|
+
startupHeaderMode,
|
|
145
|
+
}),
|
|
146
|
+
...buildStaticRenderItems(staticItems, turnIds, null, null, null),
|
|
147
|
+
...buildActiveRenderItems(activeItems, turnIds, uiState),
|
|
148
|
+
],
|
|
149
|
+
{
|
|
150
|
+
totalWidth: getShellWidth(layout.cols),
|
|
151
|
+
verboseMode,
|
|
152
|
+
debugLabel: "transcript-shell",
|
|
153
|
+
workspaceRoot,
|
|
154
|
+
},
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
staticItems: parts.staticItems.map((item) => ({ ...item, type: "rows" as const })),
|
|
159
|
+
liveRows: parts.liveRows,
|
|
160
|
+
startupHeaderMode,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function TranscriptShellInner({
|
|
165
|
+
layout,
|
|
166
|
+
authState,
|
|
167
|
+
workspaceLabel,
|
|
168
|
+
workspaceRoot = null,
|
|
169
|
+
runtimeSummary = null,
|
|
170
|
+
staticEvents,
|
|
171
|
+
activeEvents,
|
|
172
|
+
uiState,
|
|
173
|
+
composer,
|
|
174
|
+
composerRows,
|
|
175
|
+
verboseMode = false,
|
|
176
|
+
clearCount = 0,
|
|
177
|
+
visible = true,
|
|
178
|
+
}: TranscriptShellProps) {
|
|
179
|
+
const { staticItems, liveRows, startupHeaderMode } = useMemo(
|
|
180
|
+
() => buildTranscriptItems({
|
|
181
|
+
layout,
|
|
182
|
+
authState,
|
|
183
|
+
workspaceLabel,
|
|
184
|
+
workspaceRoot,
|
|
185
|
+
runtimeSummary,
|
|
186
|
+
staticEvents,
|
|
187
|
+
activeEvents,
|
|
188
|
+
uiState,
|
|
189
|
+
composerRows,
|
|
190
|
+
verboseMode,
|
|
191
|
+
}),
|
|
192
|
+
[activeEvents, authState, composerRows, layout, runtimeSummary, staticEvents, uiState, verboseMode, workspaceLabel, workspaceRoot],
|
|
193
|
+
);
|
|
194
|
+
const homeScreenActive = visible && isHomeScreenState({ staticEvents, activeEvents, uiState });
|
|
195
|
+
const visibleStaticItemsRef = useRef(staticItems);
|
|
196
|
+
|
|
197
|
+
useEffect(() => {
|
|
198
|
+
if (visible) {
|
|
199
|
+
visibleStaticItemsRef.current = staticItems;
|
|
200
|
+
}
|
|
201
|
+
}, [staticItems, visible]);
|
|
202
|
+
|
|
203
|
+
const displayedStaticItems = visible ? staticItems : visibleStaticItemsRef.current;
|
|
204
|
+
|
|
205
|
+
const staticRenderItems = useMemo<StaticRenderItem[]>(() => {
|
|
206
|
+
return displayedStaticItems.map((item) => ({
|
|
207
|
+
...item,
|
|
208
|
+
key: `clear-${clearCount}-${item.key}`,
|
|
209
|
+
}));
|
|
210
|
+
}, [clearCount, displayedStaticItems]);
|
|
211
|
+
|
|
212
|
+
const displayedLiveRows = visible ? liveRows : [];
|
|
213
|
+
const staticRowCount = useMemo(
|
|
214
|
+
() => displayedStaticItems.reduce((rowCount, item) => rowCount + item.rows.length, 0),
|
|
215
|
+
[displayedStaticItems],
|
|
216
|
+
);
|
|
217
|
+
const liveBottomSpacerRows = visible
|
|
218
|
+
? Math.max(0, getShellHeight(layout.rows) - staticRowCount - displayedLiveRows.length - (composerRows ?? 0))
|
|
219
|
+
: 0;
|
|
220
|
+
const spacerRows = useMemo<TimelineRow[]>(
|
|
221
|
+
() => Array.from({ length: liveBottomSpacerRows }, (_, index) => ({
|
|
222
|
+
key: `live-bottom-spacer-${clearCount}-${index}`,
|
|
223
|
+
spans: [{ text: " ".repeat(getShellWidth(layout.cols)) }],
|
|
224
|
+
})),
|
|
225
|
+
[clearCount, layout.cols, liveBottomSpacerRows],
|
|
226
|
+
);
|
|
227
|
+
const shellWidth = getShellWidth(layout.cols);
|
|
228
|
+
const introInnerWidth = Math.max(10, shellWidth - 2);
|
|
229
|
+
const selectedLogoRows = startupHeaderMode === "tiny"
|
|
230
|
+
? []
|
|
231
|
+
: startupHeaderMode === "large"
|
|
232
|
+
? selectLogoVariant(introInnerWidth)
|
|
233
|
+
: introInnerWidth >= LOGO_COMPACT_MIN_COLS ? LOGO_COMPACT : [];
|
|
234
|
+
const selectedLogoVariant = getLogoVariantName(selectedLogoRows);
|
|
235
|
+
const logoHiddenReason = getLogoHiddenReason({
|
|
236
|
+
startupHeaderMode,
|
|
237
|
+
logoVariant: selectedLogoVariant,
|
|
238
|
+
width: introInnerWidth,
|
|
239
|
+
});
|
|
240
|
+
const startupTraceKeyRef = useRef<string | null>(null);
|
|
241
|
+
|
|
242
|
+
useEffect(() => {
|
|
243
|
+
const nextKey = [
|
|
244
|
+
layout.cols,
|
|
245
|
+
layout.rows,
|
|
246
|
+
layout.mode,
|
|
247
|
+
startupHeaderMode,
|
|
248
|
+
homeScreenActive ? "home" : "transcript",
|
|
249
|
+
staticEvents.length,
|
|
250
|
+
activeEvents.length,
|
|
251
|
+
uiState.kind,
|
|
252
|
+
selectedLogoVariant,
|
|
253
|
+
clearCount,
|
|
254
|
+
].join("|");
|
|
255
|
+
if (startupTraceKeyRef.current === nextKey) return;
|
|
256
|
+
startupTraceKeyRef.current = nextKey;
|
|
257
|
+
renderDebug.traceEvent("startup", "homeRender", {
|
|
258
|
+
cols: layout.cols,
|
|
259
|
+
rows: layout.rows,
|
|
260
|
+
layoutMode: layout.mode,
|
|
261
|
+
activeRoot: "TranscriptShell",
|
|
262
|
+
messageCount: [...staticEvents, ...activeEvents].filter(isTranscriptEvent).length,
|
|
263
|
+
staticEventsLength: staticEvents.length,
|
|
264
|
+
activeEventsLength: activeEvents.length,
|
|
265
|
+
uiStateKind: uiState.kind,
|
|
266
|
+
selectedLayoutMode: startupHeaderMode,
|
|
267
|
+
selectedLogoVariant,
|
|
268
|
+
logoBranchSelected: selectedLogoVariant !== "none" && selectedLogoVariant !== "wordmark",
|
|
269
|
+
logoHiddenReason,
|
|
270
|
+
composerCount: visible ? 1 : 0,
|
|
271
|
+
footerCount: visible ? 1 : 0,
|
|
272
|
+
homeScreenRendererUsed: homeScreenActive,
|
|
273
|
+
staticItemCount: staticRenderItems.length,
|
|
274
|
+
liveRowCount: displayedLiveRows.length,
|
|
275
|
+
clearCount,
|
|
276
|
+
});
|
|
277
|
+
}, [
|
|
278
|
+
activeEvents,
|
|
279
|
+
clearCount,
|
|
280
|
+
displayedLiveRows.length,
|
|
281
|
+
homeScreenActive,
|
|
282
|
+
layout.cols,
|
|
283
|
+
layout.mode,
|
|
284
|
+
layout.rows,
|
|
285
|
+
logoHiddenReason,
|
|
286
|
+
selectedLogoVariant,
|
|
287
|
+
startupHeaderMode,
|
|
288
|
+
staticEvents,
|
|
289
|
+
staticRenderItems.length,
|
|
290
|
+
uiState.kind,
|
|
291
|
+
visible,
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<Box flexDirection="column" width="100%" display={visible ? "flex" : "none"}>
|
|
296
|
+
<Static key={`static-${clearCount}`} items={staticRenderItems}>
|
|
297
|
+
{(item) => <RowsBlock key={item.key} rows={item.rows} />}
|
|
298
|
+
</Static>
|
|
299
|
+
|
|
300
|
+
{displayedLiveRows.length > 0 && <RowsBlock rows={displayedLiveRows} />}
|
|
301
|
+
{spacerRows.length > 0 && <RowsBlock rows={spacerRows} />}
|
|
302
|
+
|
|
303
|
+
{visible && composer}
|
|
304
|
+
</Box>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export const TranscriptShell = memo(function TranscriptShell(props: TranscriptShellProps) {
|
|
309
|
+
// repaintGeneration is folded in here (not just <Static>'s own key) because
|
|
310
|
+
// Ink only reliably re-flushes <Static> content on a genuine fresh mount of
|
|
311
|
+
// the whole subtree — its "capture before delete" escape hatch
|
|
312
|
+
// (reconciler.js's isStaticDirty/onImmediateRender) does not fire the same
|
|
313
|
+
// way when only the inner <Static> node is keyed away and remounted on its
|
|
314
|
+
// own. This does remount the composer too, but only on the (already
|
|
315
|
+
// disruptive) event of a real terminal resize, not during normal typing.
|
|
316
|
+
return (
|
|
317
|
+
<TranscriptShellInner
|
|
318
|
+
key={`clear-${props.clearCount ?? 0}-repaint-${props.repaintGeneration ?? 0}`}
|
|
319
|
+
{...props}
|
|
320
|
+
/>
|
|
321
|
+
);
|
|
322
|
+
});
|
package/src/ui/TurnGroup.tsx
CHANGED
|
@@ -18,7 +18,7 @@ import { useTheme } from "./theme.js";
|
|
|
18
18
|
import { sanitizeTerminalOutput } from "../core/terminal/terminalSanitize.js";
|
|
19
19
|
import { wrapPlainText, wrapCommandText } from "./textLayout.js";
|
|
20
20
|
import { selectVisibleRunActivity } from "./runActivityView.js";
|
|
21
|
-
import type { RunFileActivity } from "../core/workspaceActivity.js";
|
|
21
|
+
import type { RunFileActivity } from "../core/workspace/workspaceActivity.js";
|
|
22
22
|
import { RUN_OUTPUT_TRUNCATION_NOTICE } from "../session/chatLifecycle.js";
|
|
23
23
|
import { formatProgressBlockBodyLines } from "./progressEntries.js";
|
|
24
24
|
import { getUsableShellWidth, transcriptContentIndent } from "./layout.js";
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "./outputPipeline.js";
|
|
33
33
|
import { normalizeCommand, getFriendlyActionLabel } from "./commandNormalize.js";
|
|
34
34
|
import * as renderDebug from "../core/perf/renderDebug.js";
|
|
35
|
-
import { normalizePlanReviewMarkdown } from "../core/planStorage.js";
|
|
35
|
+
import { normalizePlanReviewMarkdown } from "../core/workspace/planStorage.js";
|
|
36
36
|
|
|
37
37
|
export type TurnOpacity = "active" | "recent" | "dim";
|
|
38
38
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { CODEXA_UPDATE_COMMAND } from "../core/updateCheck.js";
|
|
3
|
+
import { CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
|
|
4
4
|
import { clampVisualText } from "./layout.js";
|
|
5
5
|
import { useTheme } from "./theme.js";
|
|
6
6
|
|
|
@@ -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}
|
|
37
|
-
<Text color={theme.textMuted}>{clamp(`
|
|
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
|
);
|
|
@@ -2,7 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { Box, Text, useFocus, useInput } from "ink";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { useTheme } from "./theme.js";
|
|
5
|
-
import { CODEXA_NPM_PACKAGE, CODEXA_UPDATE_COMMAND } from "../core/updateCheck.js";
|
|
5
|
+
import { CODEXA_NPM_PACKAGE, CODEXA_UPDATE_COMMAND, formatVersionLabel } from "../core/version/updateCheck.js";
|
|
6
6
|
|
|
7
7
|
type Phase = "menu" | "running" | "done" | "error";
|
|
8
8
|
|
|
@@ -38,6 +38,10 @@ export function UpdatePromptPanel({
|
|
|
38
38
|
const spawnStartedRef = useRef(false);
|
|
39
39
|
|
|
40
40
|
useInput((input, key) => {
|
|
41
|
+
if (key.escape) {
|
|
42
|
+
onSkip();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
41
45
|
if (phase === "menu") {
|
|
42
46
|
if (key.upArrow || input === "k") {
|
|
43
47
|
setSelectedIndex((i) => Math.max(0, i - 1));
|
|
@@ -57,12 +61,8 @@ export function UpdatePromptPanel({
|
|
|
57
61
|
}
|
|
58
62
|
return;
|
|
59
63
|
}
|
|
60
|
-
if (key.escape) {
|
|
61
|
-
onSkip();
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
64
|
} else if (phase === "done" || phase === "error") {
|
|
65
|
-
if (key.return
|
|
65
|
+
if (key.return) {
|
|
66
66
|
onSkip();
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -107,9 +107,9 @@ export function UpdatePromptPanel({
|
|
|
107
107
|
};
|
|
108
108
|
}, [phase]);
|
|
109
109
|
|
|
110
|
-
const
|
|
111
|
-
? "
|
|
112
|
-
: "
|
|
110
|
+
const footerText = phase === "menu"
|
|
111
|
+
? "Esc to close · Enter to confirm"
|
|
112
|
+
: "Esc to close";
|
|
113
113
|
|
|
114
114
|
return (
|
|
115
115
|
<Box flexDirection="column" width="100%" marginTop={1}>
|
|
@@ -122,11 +122,10 @@ export function UpdatePromptPanel({
|
|
|
122
122
|
flexDirection="column"
|
|
123
123
|
>
|
|
124
124
|
<Box>
|
|
125
|
-
<Text color={theme.accent} bold>Update available
|
|
126
|
-
<Text color={theme.textMuted}>{hintText}</Text>
|
|
125
|
+
<Text color={theme.accent} bold>{`Update available: Codexa ${formatVersionLabel(latestVersion)}`}</Text>
|
|
127
126
|
</Box>
|
|
128
127
|
<Box marginTop={1}>
|
|
129
|
-
<Text color={theme.text}>{
|
|
128
|
+
<Text color={theme.text}>{`${formatVersionLabel(currentVersion)} -> ${formatVersionLabel(latestVersion)}`}</Text>
|
|
130
129
|
</Box>
|
|
131
130
|
<Box>
|
|
132
131
|
<Text color={theme.textMuted}>{`Package: ${CODEXA_NPM_PACKAGE}`}</Text>
|
|
@@ -138,7 +137,7 @@ export function UpdatePromptPanel({
|
|
|
138
137
|
|
|
139
138
|
<Box
|
|
140
139
|
borderStyle="round"
|
|
141
|
-
borderColor={theme.borderFocused}
|
|
140
|
+
borderColor={phase === "menu" ? theme.borderFocused : theme.border}
|
|
142
141
|
paddingX={2}
|
|
143
142
|
paddingY={1}
|
|
144
143
|
marginTop={1}
|
|
@@ -160,9 +159,6 @@ export function UpdatePromptPanel({
|
|
|
160
159
|
</Text>
|
|
161
160
|
</Box>
|
|
162
161
|
))}
|
|
163
|
-
<Box marginTop={1}>
|
|
164
|
-
<Text color={theme.textDim}>Press enter to continue</Text>
|
|
165
|
-
</Box>
|
|
166
162
|
</>
|
|
167
163
|
)}
|
|
168
164
|
|
|
@@ -179,9 +175,6 @@ export function UpdatePromptPanel({
|
|
|
179
175
|
<>
|
|
180
176
|
<Text color={theme.success}>{"Codexa was updated successfully."}</Text>
|
|
181
177
|
<Text color={theme.textMuted}>{"Restart Codexa to use the new version."}</Text>
|
|
182
|
-
<Box marginTop={1}>
|
|
183
|
-
<Text color={theme.textDim}>Press Enter to close</Text>
|
|
184
|
-
</Box>
|
|
185
178
|
</>
|
|
186
179
|
)}
|
|
187
180
|
|
|
@@ -192,11 +185,12 @@ export function UpdatePromptPanel({
|
|
|
192
185
|
{outputLines.slice(-5).map((line, i) => (
|
|
193
186
|
<Text key={i} color={theme.textDim}>{line}</Text>
|
|
194
187
|
))}
|
|
195
|
-
<Box marginTop={1}>
|
|
196
|
-
<Text color={theme.textDim}>Press Enter to close</Text>
|
|
197
|
-
</Box>
|
|
198
188
|
</>
|
|
199
189
|
)}
|
|
190
|
+
|
|
191
|
+
<Box marginTop={1}>
|
|
192
|
+
<Text color={theme.textDim}>{footerText}</Text>
|
|
193
|
+
</Box>
|
|
200
194
|
</Box>
|
|
201
195
|
</Box>
|
|
202
196
|
);
|