@golba98/codexa 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -18
- package/bin/codexa.js +62 -144
- package/package.json +4 -3
- package/src/app.tsx +642 -303
- package/src/commands/handler.ts +7 -18
- 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/models/codexModelCapabilities.ts +57 -4
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/providerModelCache.ts +106 -0
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/CommandRunner.ts +12 -1
- package/src/core/providerLauncher/registry.ts +64 -18
- package/src/core/providerLauncher/types.ts +12 -9
- package/src/core/providerLauncher/workspaceConfig.ts +41 -26
- package/src/core/providerRuntime/anthropic.ts +10 -6
- package/src/core/providerRuntime/antigravity.ts +461 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -446
- 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/mistralVibe.ts +663 -0
- package/src/core/providerRuntime/models.ts +40 -15
- package/src/core/providerRuntime/reasoning.ts +9 -6
- package/src/core/providerRuntime/registry.ts +76 -4
- package/src/core/providerRuntime/types.ts +20 -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 +66 -98
- 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/test/runtimeTestUtils.ts +90 -0
- package/src/ui/{ActivityBars.tsx → chrome/ActivityBars.tsx} +1 -1
- package/src/ui/{ActivityIndicator.tsx → chrome/ActivityIndicator.tsx} +3 -3
- package/src/ui/{AnimatedStatusText.tsx → chrome/AnimatedStatusText.tsx} +3 -3
- package/src/ui/chrome/AppShell.tsx +672 -0
- package/src/ui/{BottomComposer.tsx → chrome/BottomComposer.tsx} +159 -158
- package/src/ui/{DashCard.tsx → chrome/DashCard.tsx} +2 -2
- package/src/ui/{RunFooter.tsx → chrome/RunFooter.tsx} +3 -3
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/{Spinner.tsx → chrome/Spinner.tsx} +1 -1
- package/src/ui/{TopHeader.tsx → chrome/TopHeader.tsx} +54 -38
- package/src/ui/{UpdateAvailableCard.tsx → chrome/UpdateAvailableCard.tsx} +5 -5
- package/src/ui/{commandNormalize.ts → input/commandNormalize.ts} +1 -1
- package/src/ui/{focus.ts → input/focus.ts} +1 -1
- package/src/ui/{inputBuffer.ts → input/inputBuffer.ts} +3 -3
- package/src/ui/layout.ts +298 -24
- package/src/ui/{AttachmentImportPanel.tsx → panels/AttachmentImportPanel.tsx} +3 -3
- package/src/ui/{AuthPanel.tsx → panels/AuthPanel.tsx} +5 -5
- package/src/ui/{BackendPicker.tsx → panels/BackendPicker.tsx} +2 -2
- package/src/ui/{ModePicker.tsx → panels/ModePicker.tsx} +2 -2
- package/src/ui/{ModelPicker.tsx → panels/ModelPicker.tsx} +2 -2
- package/src/ui/{ModelPickerScreen.tsx → panels/ModelPickerScreen.tsx} +224 -42
- package/src/ui/{ModelReasoningPicker.tsx → panels/ModelReasoningPicker.tsx} +5 -5
- package/src/ui/{Panel.tsx → panels/Panel.tsx} +1 -1
- package/src/ui/{PermissionsPanel.tsx → panels/PermissionsPanel.tsx} +3 -3
- package/src/ui/{PlanActionPicker.tsx → panels/PlanActionPicker.tsx} +2 -2
- package/src/ui/{PlanReviewPanel.tsx → panels/PlanReviewPanel.tsx} +5 -5
- package/src/ui/panels/ProviderPicker.tsx +737 -0
- package/src/ui/{ReasoningPicker.tsx → panels/ReasoningPicker.tsx} +3 -3
- package/src/ui/{SelectionPanel.tsx → panels/SelectionPanel.tsx} +5 -1
- package/src/ui/{SettingsPanel.tsx → panels/SettingsPanel.tsx} +4 -4
- package/src/ui/{TextEntryPanel.tsx → panels/TextEntryPanel.tsx} +4 -4
- package/src/ui/{ThemePicker.tsx → panels/ThemePicker.tsx} +2 -2
- package/src/ui/{UpdatePromptPanel.tsx → panels/UpdatePromptPanel.tsx} +17 -23
- package/src/ui/{Markdown.tsx → render/Markdown.tsx} +2 -2
- package/src/ui/{diffRenderer.ts → render/diffRenderer.ts} +1 -1
- package/src/ui/{logoVariants.ts → render/logoVariants.ts} +4 -8
- package/src/ui/{modeDisplay.ts → render/modeDisplay.ts} +2 -2
- package/src/ui/{outputPipeline.ts → render/outputPipeline.ts} +2 -2
- package/src/ui/{runtimeDisplay.ts → render/runtimeDisplay.ts} +23 -10
- package/src/ui/{textLayout.ts → render/textLayout.ts} +15 -4
- package/src/ui/{ActionRequiredBlock.tsx → timeline/ActionRequiredBlock.tsx} +3 -3
- package/src/ui/{AgentBlock.tsx → timeline/AgentBlock.tsx} +10 -10
- package/src/ui/{StaticIntroItem.tsx → timeline/StaticIntroItem.tsx} +2 -2
- package/src/ui/{ThinkingBlock.tsx → timeline/ThinkingBlock.tsx} +4 -4
- package/src/ui/{Timeline.tsx → timeline/Timeline.tsx} +1620 -1471
- package/src/ui/timeline/TranscriptShell.tsx +322 -0
- package/src/ui/{TurnGroup.tsx → timeline/TurnGroup.tsx} +15 -15
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/{progressEntries.ts → timeline/progressEntries.ts} +3 -3
- package/src/ui/{runActivityView.ts → timeline/runActivityView.ts} +1 -1
- package/src/ui/{timelineMeasure.ts → timeline/timelineMeasure.ts} +206 -134
- package/src/core/codex.ts +0 -124
- package/src/ui/AppShell.tsx +0 -706
- package/src/ui/ProviderPicker.tsx +0 -321
- 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/{busyStatusAnimation.ts → chrome/busyStatusAnimation.ts} +0 -0
- /package/src/ui/{slashCommands.ts → input/slashCommands.ts} +0 -0
- /package/src/ui/{terminalAnswerFormat.ts → render/terminalAnswerFormat.ts} +0 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import React, { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Box, Text, useStdin } from "ink";
|
|
3
|
+
import { useTheme } from "../theme.js";
|
|
4
|
+
import type { RuntimeSummary } from "../../config/runtimeConfig.js";
|
|
5
|
+
import type { CodexAuthState } from "../../core/auth/codexAuth.js";
|
|
6
|
+
import { HEADER_CONFIG_DEFAULTS, type HeaderConfig } from "../../config/settings.js";
|
|
7
|
+
import * as renderDebug from "../../core/perf/renderDebug.js";
|
|
8
|
+
import type { Screen, TimelineEvent, UIState } from "../../session/types.js";
|
|
9
|
+
import { isBusy } from "../../session/types.js";
|
|
10
|
+
import {
|
|
11
|
+
ActivePanelLayoutContext,
|
|
12
|
+
type ActivePanelLayout,
|
|
13
|
+
AppLayoutBudgetContext,
|
|
14
|
+
computeAppLayoutBudget,
|
|
15
|
+
getContentWidth,
|
|
16
|
+
getShellHeight,
|
|
17
|
+
getShellWidth,
|
|
18
|
+
isCrampedTerminal,
|
|
19
|
+
PanelAvailableRowsContext,
|
|
20
|
+
type Layout,
|
|
21
|
+
type LayoutMode,
|
|
22
|
+
type PanelLayout,
|
|
23
|
+
PanelLayoutContext,
|
|
24
|
+
MIN_TERMINAL_COLS,
|
|
25
|
+
MIN_TERMINAL_ROWS,
|
|
26
|
+
type TerminalViewport,
|
|
27
|
+
} from "../layout.js";
|
|
28
|
+
import {
|
|
29
|
+
buildActiveRenderItems,
|
|
30
|
+
buildStaticRenderItems,
|
|
31
|
+
buildTimelineItems,
|
|
32
|
+
parseTimelineNavigationInput,
|
|
33
|
+
Timeline,
|
|
34
|
+
TimelineRowView,
|
|
35
|
+
type TimelineItem,
|
|
36
|
+
} from "../timeline/Timeline.js";
|
|
37
|
+
import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "../timeline/timelineMeasure.js";
|
|
38
|
+
import type { TerminalSelectionProfile } from "../../core/terminal/terminalSelection.js";
|
|
39
|
+
import { MemoizedTopHeader, measureTopHeaderRows, type UpdateAvailableInfo } from "./TopHeader.js";
|
|
40
|
+
|
|
41
|
+
const COMPACT_HEADER_TO_COMPOSER_GAP_ROWS = 1;
|
|
42
|
+
const MEDIUM_HEADER_TO_COMPOSER_GAP_ROWS = 1;
|
|
43
|
+
const TALL_HEADER_TO_COMPOSER_GAP_ROWS = 1;
|
|
44
|
+
|
|
45
|
+
// ─── Types & constants ────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
type AppShellLayout = TerminalViewport;
|
|
48
|
+
type NativeStaticItem =
|
|
49
|
+
{ type: "rows" } & NativeTranscriptRowItem;
|
|
50
|
+
|
|
51
|
+
export interface AppShellProps {
|
|
52
|
+
layout: AppShellLayout;
|
|
53
|
+
screen: Screen;
|
|
54
|
+
authState: CodexAuthState;
|
|
55
|
+
workspaceLabel: string;
|
|
56
|
+
workspaceRoot?: string | null;
|
|
57
|
+
runtimeSummary?: RuntimeSummary | null;
|
|
58
|
+
staticEvents: TimelineEvent[];
|
|
59
|
+
activeEvents: TimelineEvent[];
|
|
60
|
+
uiState: UIState;
|
|
61
|
+
panel: React.ReactNode;
|
|
62
|
+
mainPanel?: React.ReactNode;
|
|
63
|
+
mainPanelMode?: "viewport" | "full-output";
|
|
64
|
+
composer: React.ReactNode;
|
|
65
|
+
composerRows: number;
|
|
66
|
+
panelHint?: React.ReactNode;
|
|
67
|
+
verboseMode?: boolean;
|
|
68
|
+
mouseCapture?: boolean;
|
|
69
|
+
onMouseActivity?: () => void;
|
|
70
|
+
selectionProfile?: TerminalSelectionProfile;
|
|
71
|
+
clearCount?: number;
|
|
72
|
+
headerConfig?: HeaderConfig;
|
|
73
|
+
updateAvailable?: UpdateAvailableInfo | null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Helpers & subcomponents ─────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
export function isCrampedViewport(rows: number | undefined): boolean {
|
|
79
|
+
return (rows ?? 24) <= 24;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function calculateNativeSpacerRows({
|
|
83
|
+
shellRows,
|
|
84
|
+
introRows,
|
|
85
|
+
composerRows,
|
|
86
|
+
staticRows,
|
|
87
|
+
liveRows,
|
|
88
|
+
}: {
|
|
89
|
+
shellRows: number;
|
|
90
|
+
introRows: number;
|
|
91
|
+
composerRows: number;
|
|
92
|
+
staticRows: number;
|
|
93
|
+
liveRows: number;
|
|
94
|
+
}): number {
|
|
95
|
+
const availableBodyRows = Math.max(0, shellRows - introRows - composerRows);
|
|
96
|
+
const visibleBodyContentRows = Math.max(0, staticRows) + Math.max(0, liveRows);
|
|
97
|
+
return Math.max(0, availableBodyRows - visibleBodyContentRows);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function calculateColdStartSpacerRows({
|
|
101
|
+
shellRows,
|
|
102
|
+
headerRows,
|
|
103
|
+
composerRows,
|
|
104
|
+
layoutMode,
|
|
105
|
+
availableRows,
|
|
106
|
+
}: {
|
|
107
|
+
shellRows: number;
|
|
108
|
+
headerRows: number;
|
|
109
|
+
composerRows: number;
|
|
110
|
+
layoutMode: LayoutMode;
|
|
111
|
+
availableRows: number;
|
|
112
|
+
}): number {
|
|
113
|
+
if (availableRows <= 0) return 0;
|
|
114
|
+
|
|
115
|
+
const rowsAfterHeaderAndComposer = Math.max(0, shellRows - headerRows - composerRows);
|
|
116
|
+
const preferredRows = layoutMode === "compact" || shellRows <= 18
|
|
117
|
+
? 1
|
|
118
|
+
: shellRows >= 36
|
|
119
|
+
? TALL_HEADER_TO_COMPOSER_GAP_ROWS
|
|
120
|
+
: shellRows >= 28
|
|
121
|
+
? MEDIUM_HEADER_TO_COMPOSER_GAP_ROWS
|
|
122
|
+
: COMPACT_HEADER_TO_COMPOSER_GAP_ROWS;
|
|
123
|
+
|
|
124
|
+
return Math.max(0, Math.min(availableRows, rowsAfterHeaderAndComposer, preferredRows));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function calculateHeaderToContentGapRows(layout: Layout): number {
|
|
128
|
+
if (layout.mode === "compact" || layout.rows <= 18) return 0;
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function NativeRowsItem({ rows }: { rows: TimelineRow[] }) {
|
|
133
|
+
return (
|
|
134
|
+
<Box flexDirection="column">
|
|
135
|
+
{rows.map((row) => (
|
|
136
|
+
<TimelineRowView key={row.key} row={row} />
|
|
137
|
+
))}
|
|
138
|
+
</Box>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function NativePauseBar({ unseenRows }: { unseenRows: number }) {
|
|
143
|
+
return (
|
|
144
|
+
<Box width="100%" paddingX={1}>
|
|
145
|
+
<Text dimColor>
|
|
146
|
+
{unseenRows > 0
|
|
147
|
+
? `↓ ${unseenRows} new rows · End to follow`
|
|
148
|
+
: "↓ New output · End to follow"}
|
|
149
|
+
</Text>
|
|
150
|
+
</Box>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function CrampedView({ layout }: { layout: Layout }) {
|
|
155
|
+
const theme = useTheme();
|
|
156
|
+
return (
|
|
157
|
+
<Box
|
|
158
|
+
flexDirection="column"
|
|
159
|
+
width="100%"
|
|
160
|
+
height={getShellHeight(layout.rows)}
|
|
161
|
+
alignItems="center"
|
|
162
|
+
justifyContent="center"
|
|
163
|
+
>
|
|
164
|
+
<Box borderStyle="round" borderColor={theme.error} paddingX={2} paddingY={1}>
|
|
165
|
+
<Text color={theme.error} bold>
|
|
166
|
+
Terminal too small — resize to at least {MIN_TERMINAL_COLS} x {MIN_TERMINAL_ROWS}
|
|
167
|
+
</Text>
|
|
168
|
+
</Box>
|
|
169
|
+
<Box marginTop={1}>
|
|
170
|
+
<Text dimColor>
|
|
171
|
+
Current: {layout.cols} x {layout.rows}
|
|
172
|
+
</Text>
|
|
173
|
+
</Box>
|
|
174
|
+
</Box>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function injectPanelLayout(
|
|
179
|
+
element: React.ReactNode,
|
|
180
|
+
availableRows: number,
|
|
181
|
+
activePanelLayout: ActivePanelLayout,
|
|
182
|
+
panelLayout: PanelLayout
|
|
183
|
+
): React.ReactNode {
|
|
184
|
+
if (!React.isValidElement(element)) {
|
|
185
|
+
return element;
|
|
186
|
+
}
|
|
187
|
+
if (element.type === React.Fragment) {
|
|
188
|
+
const fragment = element as React.ReactElement<{ children?: React.ReactNode }>;
|
|
189
|
+
return React.cloneElement(
|
|
190
|
+
fragment,
|
|
191
|
+
fragment.props,
|
|
192
|
+
React.Children.map(fragment.props.children, (child) =>
|
|
193
|
+
injectPanelLayout(child, availableRows, activePanelLayout, panelLayout)
|
|
194
|
+
)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return React.cloneElement(
|
|
198
|
+
element as React.ReactElement<{
|
|
199
|
+
availableRows?: number;
|
|
200
|
+
activePanelLayout?: ActivePanelLayout;
|
|
201
|
+
panelLayout?: PanelLayout;
|
|
202
|
+
}>,
|
|
203
|
+
{ availableRows, activePanelLayout, panelLayout }
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
function AppShellInner({
|
|
210
|
+
layout,
|
|
211
|
+
screen,
|
|
212
|
+
authState,
|
|
213
|
+
workspaceLabel,
|
|
214
|
+
workspaceRoot = null,
|
|
215
|
+
runtimeSummary = null,
|
|
216
|
+
staticEvents,
|
|
217
|
+
activeEvents,
|
|
218
|
+
uiState,
|
|
219
|
+
panel,
|
|
220
|
+
mainPanel,
|
|
221
|
+
mainPanelMode = "viewport",
|
|
222
|
+
composer,
|
|
223
|
+
composerRows,
|
|
224
|
+
panelHint,
|
|
225
|
+
verboseMode = false,
|
|
226
|
+
mouseCapture = false,
|
|
227
|
+
onMouseActivity,
|
|
228
|
+
selectionProfile,
|
|
229
|
+
clearCount = 0,
|
|
230
|
+
headerConfig = HEADER_CONFIG_DEFAULTS,
|
|
231
|
+
updateAvailable = null,
|
|
232
|
+
}: AppShellProps) {
|
|
233
|
+
const theme = useTheme();
|
|
234
|
+
renderDebug.useRenderDebug("AppShell", {
|
|
235
|
+
cols: layout.cols,
|
|
236
|
+
rows: layout.rows,
|
|
237
|
+
mode: layout.mode,
|
|
238
|
+
layoutEpoch: layout.layoutEpoch,
|
|
239
|
+
screen,
|
|
240
|
+
authState,
|
|
241
|
+
workspaceLabel,
|
|
242
|
+
workspaceRoot,
|
|
243
|
+
runtimeSummary,
|
|
244
|
+
staticEvents,
|
|
245
|
+
activeEvents,
|
|
246
|
+
uiState,
|
|
247
|
+
composer,
|
|
248
|
+
composerRows,
|
|
249
|
+
verboseMode,
|
|
250
|
+
});
|
|
251
|
+
renderDebug.useLifecycleDebug("AppShell", {
|
|
252
|
+
screen,
|
|
253
|
+
cols: layout.cols,
|
|
254
|
+
rows: layout.rows,
|
|
255
|
+
mode: layout.mode,
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (layout.isCramped && !mouseCapture) {
|
|
259
|
+
return <CrampedView layout={layout} />;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const shellWidth = getShellWidth(layout.cols);
|
|
263
|
+
const shellHeight = getShellHeight(layout.rows);
|
|
264
|
+
const headerRows = measureTopHeaderRows(layout, headerConfig, !!updateAvailable);
|
|
265
|
+
|
|
266
|
+
const showComposer = true;
|
|
267
|
+
const showMainPanel = screen === "main" && mainPanel !== undefined && mainPanel !== null;
|
|
268
|
+
const showMainPanelFullOutput = showMainPanel && mainPanelMode === "full-output";
|
|
269
|
+
const showTimeline = screen === "main" && !showMainPanel;
|
|
270
|
+
const showPanelStage = screen !== "main";
|
|
271
|
+
const hasUserPrompt = useMemo(
|
|
272
|
+
() => staticEvents.some((e) => e.type === "user") || activeEvents.some((e) => e.type === "user"),
|
|
273
|
+
[staticEvents, activeEvents],
|
|
274
|
+
);
|
|
275
|
+
const previousMeasurements = useRef<{
|
|
276
|
+
timelineRows: number;
|
|
277
|
+
composerRows: number;
|
|
278
|
+
shellHeight: number;
|
|
279
|
+
shellWidth: number;
|
|
280
|
+
} | null>(null);
|
|
281
|
+
|
|
282
|
+
// ── Native mode scroll-pause state ────────────────────────────────────────
|
|
283
|
+
// When the user presses Page Up or Home during streaming, we freeze nativeAllRows
|
|
284
|
+
// so Ink's lastOutputHeight stays constant and the terminal stops auto-scrolling.
|
|
285
|
+
const [nativePaused, setNativePaused] = useState(false);
|
|
286
|
+
const nativePausedRef = useRef(false);
|
|
287
|
+
const frozenNativeRowsRef = useRef<TimelineRow[]>([]);
|
|
288
|
+
const frozenLiveRowCountRef = useRef(0);
|
|
289
|
+
const { stdin } = useStdin();
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
nativePausedRef.current = nativePaused;
|
|
293
|
+
}, [nativePaused]);
|
|
294
|
+
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
const handleRawInput = (chunk: Buffer | string) => {
|
|
297
|
+
if (!showTimeline || !isBusy(uiState)) return;
|
|
298
|
+
const raw = typeof chunk === "string" ? chunk : chunk.toString();
|
|
299
|
+
const actions = parseTimelineNavigationInput(raw);
|
|
300
|
+
if (actions.length === 0) return;
|
|
301
|
+
|
|
302
|
+
if (actions.some((action) => action === "pageUp" || action === "home" || action === "wheelUp")) {
|
|
303
|
+
setNativePaused(true);
|
|
304
|
+
} else if (actions.some((action) => action === "pageDown" || action === "end" || action === "wheelDown")) {
|
|
305
|
+
setNativePaused(false);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
if (stdin.isTTY) {
|
|
310
|
+
stdin.on("data", handleRawInput);
|
|
311
|
+
return () => {
|
|
312
|
+
stdin.off("data", handleRawInput);
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}, [stdin, uiState, showTimeline]);
|
|
316
|
+
|
|
317
|
+
// Auto-unpause when busy state ends.
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (!isBusy(uiState) && nativePaused) {
|
|
320
|
+
setNativePaused(false);
|
|
321
|
+
}
|
|
322
|
+
}, [uiState, nativePaused]);
|
|
323
|
+
|
|
324
|
+
// ── End native mode scroll-pause state ────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
const effectiveShowComposer = showComposer;
|
|
327
|
+
const panelHintRows = showPanelStage && panelHint ? 2 : 0;
|
|
328
|
+
|
|
329
|
+
// ─── App Layout Budget ────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
const appLayoutBudget = computeAppLayoutBudget({
|
|
332
|
+
cols: layout.cols,
|
|
333
|
+
rows: layout.rows,
|
|
334
|
+
composerRows,
|
|
335
|
+
panelHintRows,
|
|
336
|
+
headerRows,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const headerToContentGapRows = appLayoutBudget.headerGapRows;
|
|
340
|
+
const effectiveComposerRows = appLayoutBudget.composerRows;
|
|
341
|
+
const bottomChromeRows = appLayoutBudget.bottomChromeBudget.totalRows;
|
|
342
|
+
|
|
343
|
+
const finalTimelineRows = appLayoutBudget.transcriptRows;
|
|
344
|
+
const finalShellHeight = shellHeight;
|
|
345
|
+
const finalShellWidth = shellWidth;
|
|
346
|
+
|
|
347
|
+
const { finalTimelineRows: resolvedTimelineRows } = useMemo(() => {
|
|
348
|
+
const prev = previousMeasurements.current;
|
|
349
|
+
const isValid = shellHeight > 0
|
|
350
|
+
&& shellWidth > 0
|
|
351
|
+
&& Number.isFinite(shellHeight)
|
|
352
|
+
&& Number.isFinite(shellWidth)
|
|
353
|
+
&& Number.isFinite(finalTimelineRows)
|
|
354
|
+
&& finalTimelineRows >= 2;
|
|
355
|
+
|
|
356
|
+
if (!isValid && prev) {
|
|
357
|
+
return {
|
|
358
|
+
finalTimelineRows: prev.timelineRows,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
finalTimelineRows: Math.max(2, finalTimelineRows),
|
|
364
|
+
};
|
|
365
|
+
}, [shellHeight, shellWidth, finalTimelineRows]);
|
|
366
|
+
|
|
367
|
+
// ─── Native mode transcript ───────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
const nativeTranscriptParts = useMemo(() => {
|
|
370
|
+
if (mouseCapture) {
|
|
371
|
+
return { staticItems: [], liveRows: [] };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const staticItems = buildTimelineItems(staticEvents);
|
|
375
|
+
const activeItems = buildTimelineItems(activeEvents);
|
|
376
|
+
const turnIds = [...staticItems, ...activeItems]
|
|
377
|
+
.filter((item): item is Extract<TimelineItem, { type: "turn" }> => item.type === "turn")
|
|
378
|
+
.map((item) => item.turnId);
|
|
379
|
+
|
|
380
|
+
const parts = buildNativeTranscriptParts(
|
|
381
|
+
[
|
|
382
|
+
...buildStaticRenderItems(staticItems, turnIds, null, null, null),
|
|
383
|
+
...buildActiveRenderItems(activeItems, turnIds, uiState),
|
|
384
|
+
],
|
|
385
|
+
{
|
|
386
|
+
totalWidth: finalShellWidth,
|
|
387
|
+
verboseMode,
|
|
388
|
+
debugLabel: "app-shell-native",
|
|
389
|
+
workspaceRoot,
|
|
390
|
+
},
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
if (!showTimeline) {
|
|
394
|
+
return { ...parts, liveRows: [] };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return parts;
|
|
398
|
+
}, [activeEvents, finalShellWidth, mouseCapture, showTimeline, staticEvents, uiState, verboseMode, workspaceRoot]);
|
|
399
|
+
|
|
400
|
+
// ─── Spacer logic for native mode ─────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
const nativeStaticTranscriptRows = useMemo(
|
|
403
|
+
() => nativeTranscriptParts.staticItems.reduce((total, item) => total + item.rows.length, 0),
|
|
404
|
+
[nativeTranscriptParts.staticItems],
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const nativeSpacerRows = useMemo(() => {
|
|
408
|
+
if (mouseCapture || !effectiveShowComposer || showMainPanel) return 0;
|
|
409
|
+
const rows = calculateNativeSpacerRows({
|
|
410
|
+
shellRows: finalShellHeight,
|
|
411
|
+
introRows: headerRows + headerToContentGapRows,
|
|
412
|
+
composerRows: bottomChromeRows,
|
|
413
|
+
staticRows: nativeStaticTranscriptRows,
|
|
414
|
+
liveRows: nativeTranscriptParts.liveRows.length,
|
|
415
|
+
});
|
|
416
|
+
if (!hasUserPrompt) {
|
|
417
|
+
return calculateColdStartSpacerRows({
|
|
418
|
+
shellRows: finalShellHeight,
|
|
419
|
+
headerRows,
|
|
420
|
+
composerRows: bottomChromeRows,
|
|
421
|
+
layoutMode: layout.mode,
|
|
422
|
+
availableRows: rows,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return rows;
|
|
426
|
+
}, [mouseCapture, effectiveShowComposer, showMainPanel, finalShellHeight, headerRows, headerToContentGapRows, bottomChromeRows, layout.mode, nativeStaticTranscriptRows, nativeTranscriptParts.liveRows.length, hasUserPrompt]);
|
|
427
|
+
|
|
428
|
+
const nativeStaticAllItems = useMemo<NativeStaticItem[]>(
|
|
429
|
+
() => {
|
|
430
|
+
if (mouseCapture) return [];
|
|
431
|
+
return nativeTranscriptParts.staticItems.map((item) => ({ ...item, type: "rows" as const }));
|
|
432
|
+
},
|
|
433
|
+
[mouseCapture, nativeTranscriptParts.staticItems],
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const nativeAllRows = useMemo<TimelineRow[]>(
|
|
437
|
+
() => {
|
|
438
|
+
if (mouseCapture) return [];
|
|
439
|
+
|
|
440
|
+
const allStaticRows = nativeStaticAllItems.flatMap((item) => item.rows);
|
|
441
|
+
const maxStaticRows = finalShellHeight > 0 ? finalShellHeight * 2 : allStaticRows.length;
|
|
442
|
+
const trimmedStaticRows = allStaticRows.slice(Math.max(0, allStaticRows.length - maxStaticRows));
|
|
443
|
+
|
|
444
|
+
const liveRows = showTimeline ? nativeTranscriptParts.liveRows : [];
|
|
445
|
+
|
|
446
|
+
if (nativePaused) {
|
|
447
|
+
return frozenNativeRowsRef.current;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const rows = [
|
|
451
|
+
...trimmedStaticRows,
|
|
452
|
+
...liveRows,
|
|
453
|
+
];
|
|
454
|
+
frozenLiveRowCountRef.current = liveRows.length;
|
|
455
|
+
frozenNativeRowsRef.current = rows;
|
|
456
|
+
return rows;
|
|
457
|
+
},
|
|
458
|
+
[mouseCapture, nativePaused, nativeStaticAllItems, nativeTranscriptParts.liveRows, showTimeline, finalShellHeight],
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
const nativeUnseenRows = nativePaused
|
|
462
|
+
? Math.max(0, (showTimeline ? nativeTranscriptParts.liveRows.length : 0) - frozenLiveRowCountRef.current)
|
|
463
|
+
: 0;
|
|
464
|
+
|
|
465
|
+
useEffect(() => {
|
|
466
|
+
previousMeasurements.current = {
|
|
467
|
+
timelineRows: resolvedTimelineRows,
|
|
468
|
+
composerRows: bottomChromeRows,
|
|
469
|
+
shellHeight: finalShellHeight,
|
|
470
|
+
shellWidth: finalShellWidth,
|
|
471
|
+
};
|
|
472
|
+
}, [resolvedTimelineRows, bottomChromeRows, finalShellHeight, finalShellWidth]);
|
|
473
|
+
|
|
474
|
+
const clonedComposer = React.isValidElement(composer)
|
|
475
|
+
? React.cloneElement(
|
|
476
|
+
composer as React.ReactElement<{ selectionProfile?: TerminalSelectionProfile }>,
|
|
477
|
+
{ selectionProfile },
|
|
478
|
+
)
|
|
479
|
+
: composer;
|
|
480
|
+
|
|
481
|
+
const contentWidth = getContentWidth(layout.cols);
|
|
482
|
+
const panelStagePaddingY = appLayoutBudget.panelStagePaddingY;
|
|
483
|
+
|
|
484
|
+
const panelAvailableRows = appLayoutBudget.panelRows;
|
|
485
|
+
|
|
486
|
+
const activePanelLayout = useMemo<ActivePanelLayout>(() => {
|
|
487
|
+
const panelBoxHeight = Math.max(3, resolvedTimelineRows - 2 * panelStagePaddingY);
|
|
488
|
+
const showBorder = panelBoxHeight >= 7;
|
|
489
|
+
const borderRows = showBorder ? 2 : 0;
|
|
490
|
+
const borderCols = showBorder ? 4 : 0;
|
|
491
|
+
const panelTitleRows = showBorder ? 1 : 0;
|
|
492
|
+
const panelHeaderRows = panelBoxHeight >= 9 ? 1 : 0;
|
|
493
|
+
const panelChromeRows = borderRows + panelTitleRows + panelHeaderRows;
|
|
494
|
+
|
|
495
|
+
const availableRows = Math.max(1, panelBoxHeight - panelChromeRows);
|
|
496
|
+
const availableCols = Math.max(20, contentWidth - borderCols);
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
width: contentWidth,
|
|
500
|
+
height: panelBoxHeight,
|
|
501
|
+
availableRows,
|
|
502
|
+
availableCols,
|
|
503
|
+
};
|
|
504
|
+
}, [resolvedTimelineRows, panelStagePaddingY, contentWidth]);
|
|
505
|
+
|
|
506
|
+
const panelLayout = useMemo<PanelLayout>(() => {
|
|
507
|
+
return {
|
|
508
|
+
mode: appLayoutBudget.mode,
|
|
509
|
+
availableRows: appLayoutBudget.activePanelRows,
|
|
510
|
+
availableCols: appLayoutBudget.activePanelCols,
|
|
511
|
+
};
|
|
512
|
+
}, [appLayoutBudget.mode, appLayoutBudget.activePanelRows, appLayoutBudget.activePanelCols]);
|
|
513
|
+
|
|
514
|
+
const mainContent = (
|
|
515
|
+
<AppLayoutBudgetContext.Provider value={appLayoutBudget}>
|
|
516
|
+
<Box flexDirection="column" width={contentWidth} height="100%">
|
|
517
|
+
<MemoizedTopHeader
|
|
518
|
+
authState={authState}
|
|
519
|
+
workspaceLabel={workspaceLabel}
|
|
520
|
+
layout={layout}
|
|
521
|
+
runtimeSummary={runtimeSummary}
|
|
522
|
+
headerConfig={headerConfig}
|
|
523
|
+
updateAvailable={updateAvailable}
|
|
524
|
+
/>
|
|
525
|
+
|
|
526
|
+
{headerToContentGapRows > 0 && (
|
|
527
|
+
<Box height={headerToContentGapRows} />
|
|
528
|
+
)}
|
|
529
|
+
|
|
530
|
+
<Box
|
|
531
|
+
flexDirection="column"
|
|
532
|
+
height={resolvedTimelineRows}
|
|
533
|
+
overflow="hidden"
|
|
534
|
+
display={showTimeline ? "flex" : "none"}
|
|
535
|
+
>
|
|
536
|
+
<Timeline
|
|
537
|
+
key={`timeline-${clearCount}`}
|
|
538
|
+
staticEvents={staticEvents}
|
|
539
|
+
activeEvents={activeEvents}
|
|
540
|
+
layout={layout}
|
|
541
|
+
uiState={uiState}
|
|
542
|
+
viewportRows={resolvedTimelineRows}
|
|
543
|
+
verboseMode={verboseMode}
|
|
544
|
+
authState={authState}
|
|
545
|
+
workspaceLabel={workspaceLabel}
|
|
546
|
+
workspaceRoot={workspaceRoot}
|
|
547
|
+
mouseCapture={mouseCapture}
|
|
548
|
+
onMouseActivity={onMouseActivity}
|
|
549
|
+
contentSized
|
|
550
|
+
/>
|
|
551
|
+
</Box>
|
|
552
|
+
|
|
553
|
+
{showMainPanel && (
|
|
554
|
+
<Box flexDirection="column" height={resolvedTimelineRows} overflow="hidden" justifyContent="center">
|
|
555
|
+
{mainPanel}
|
|
556
|
+
</Box>
|
|
557
|
+
)}
|
|
558
|
+
|
|
559
|
+
{showPanelStage && (
|
|
560
|
+
<Box flexDirection="column" height={resolvedTimelineRows} overflow="hidden" paddingY={panelStagePaddingY}>
|
|
561
|
+
<PanelAvailableRowsContext.Provider value={panelAvailableRows}>
|
|
562
|
+
<ActivePanelLayoutContext.Provider value={activePanelLayout}>
|
|
563
|
+
<PanelLayoutContext.Provider value={panelLayout}>
|
|
564
|
+
{process.env.CODEXA_DEBUG_LAYOUT === "1" && (
|
|
565
|
+
<Box>
|
|
566
|
+
<Text color="red">
|
|
567
|
+
DEBUG layout: rows={layout.rows} cols={layout.cols} mode={layout.mode} headerRows={headerRows} panelRows={panelAvailableRows} bottomChromeRows={appLayoutBudget.bottomChromeBudget.totalRows}
|
|
568
|
+
</Text>
|
|
569
|
+
</Box>
|
|
570
|
+
)}
|
|
571
|
+
{injectPanelLayout(panel, panelAvailableRows, activePanelLayout, panelLayout)}
|
|
572
|
+
</PanelLayoutContext.Provider>
|
|
573
|
+
</ActivePanelLayoutContext.Provider>
|
|
574
|
+
</PanelAvailableRowsContext.Provider>
|
|
575
|
+
</Box>
|
|
576
|
+
)}
|
|
577
|
+
|
|
578
|
+
{showPanelStage && panelHint}
|
|
579
|
+
|
|
580
|
+
{effectiveShowComposer && (
|
|
581
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
582
|
+
{nativePaused && (
|
|
583
|
+
<NativePauseBar unseenRows={nativeUnseenRows} />
|
|
584
|
+
)}
|
|
585
|
+
{clonedComposer}
|
|
586
|
+
</Box>
|
|
587
|
+
)}
|
|
588
|
+
</Box>
|
|
589
|
+
</AppLayoutBudgetContext.Provider>
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
if (showMainPanelFullOutput) {
|
|
593
|
+
const fullOutputContent = (
|
|
594
|
+
<Box flexDirection="column" width={contentWidth}>
|
|
595
|
+
<MemoizedTopHeader
|
|
596
|
+
authState={authState}
|
|
597
|
+
workspaceLabel={workspaceLabel}
|
|
598
|
+
layout={layout}
|
|
599
|
+
runtimeSummary={runtimeSummary}
|
|
600
|
+
headerConfig={headerConfig}
|
|
601
|
+
updateAvailable={updateAvailable}
|
|
602
|
+
/>
|
|
603
|
+
|
|
604
|
+
{headerToContentGapRows > 0 && (
|
|
605
|
+
<Box height={headerToContentGapRows} />
|
|
606
|
+
)}
|
|
607
|
+
|
|
608
|
+
{mainPanel}
|
|
609
|
+
|
|
610
|
+
{showComposer && (
|
|
611
|
+
<Box flexDirection="column" flexShrink={0}>
|
|
612
|
+
{composer}
|
|
613
|
+
</Box>
|
|
614
|
+
)}
|
|
615
|
+
</Box>
|
|
616
|
+
);
|
|
617
|
+
|
|
618
|
+
if (shellWidth > contentWidth) {
|
|
619
|
+
return (
|
|
620
|
+
<Box flexDirection="column" width="100%" alignItems="center">
|
|
621
|
+
{fullOutputContent}
|
|
622
|
+
</Box>
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
return fullOutputContent;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (shellWidth > contentWidth) {
|
|
629
|
+
return (
|
|
630
|
+
<Box flexDirection="column" width="100%" height={finalShellHeight} alignItems="center">
|
|
631
|
+
{mainContent}
|
|
632
|
+
</Box>
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
return (
|
|
637
|
+
<Box flexDirection="column" width="100%" height={finalShellHeight}>
|
|
638
|
+
{mainContent}
|
|
639
|
+
</Box>
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export const AppShell = memo(AppShellInner, (prev, next) => {
|
|
644
|
+
const panelPropsEqual = next.screen === "main"
|
|
645
|
+
? prev.mainPanel === next.mainPanel
|
|
646
|
+
: (prev.panel === next.panel && prev.panelHint === next.panelHint);
|
|
647
|
+
|
|
648
|
+
return (
|
|
649
|
+
prev.layout.cols === next.layout.cols &&
|
|
650
|
+
prev.layout.rows === next.layout.rows &&
|
|
651
|
+
prev.layout.mode === next.layout.mode &&
|
|
652
|
+
prev.layout.layoutEpoch === next.layout.layoutEpoch &&
|
|
653
|
+
prev.screen === next.screen &&
|
|
654
|
+
prev.authState === next.authState &&
|
|
655
|
+
prev.workspaceLabel === next.workspaceLabel &&
|
|
656
|
+
prev.workspaceRoot === next.workspaceRoot &&
|
|
657
|
+
prev.runtimeSummary === next.runtimeSummary &&
|
|
658
|
+
prev.staticEvents === next.staticEvents &&
|
|
659
|
+
prev.activeEvents === next.activeEvents &&
|
|
660
|
+
prev.uiState === next.uiState &&
|
|
661
|
+
prev.composerRows === next.composerRows &&
|
|
662
|
+
prev.composer === next.composer &&
|
|
663
|
+
prev.mainPanel === next.mainPanel &&
|
|
664
|
+
prev.mainPanelMode === next.mainPanelMode &&
|
|
665
|
+
prev.verboseMode === next.verboseMode &&
|
|
666
|
+
prev.mouseCapture === next.mouseCapture &&
|
|
667
|
+
prev.onMouseActivity === next.onMouseActivity &&
|
|
668
|
+
prev.clearCount === next.clearCount &&
|
|
669
|
+
prev.updateAvailable === next.updateAvailable &&
|
|
670
|
+
panelPropsEqual
|
|
671
|
+
);
|
|
672
|
+
});
|