@golba98/codexa 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +396 -100
- package/bin/codexa.js +62 -144
- package/package.json +14 -8
- package/src/app.tsx +596 -306
- package/src/commands/handler.ts +6 -6
- package/src/config/buildInfo.ts +2 -2
- package/src/config/layeredConfig.ts +1 -1
- package/src/config/persistence.ts +10 -0
- package/src/config/runtimeConfig.ts +1 -1
- package/src/config/settings.ts +8 -16
- package/src/config/trustStore.ts +1 -1
- package/src/config/updateCheckCache.ts +19 -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/codexExecutable.ts +1 -0
- package/src/core/executables/executableResolver.ts +65 -43
- package/src/core/perf/renderDebug.ts +10 -6
- package/src/core/process/processValidation.ts +9 -5
- package/src/core/providerLauncher/launcher.ts +59 -42
- 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/version/channel.ts +23 -0
- package/src/core/version/updateCheck.ts +193 -0
- package/src/core/{launchContext.ts → workspace/launchContext.ts} +32 -39
- 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 +3 -2
- package/src/ui/ActionRequiredBlock.tsx +5 -5
- package/src/ui/ActivityBars.tsx +3 -3
- package/src/ui/ActivityIndicator.tsx +6 -6
- package/src/ui/AgentBlock.tsx +6 -6
- package/src/ui/AnimatedStatusText.tsx +1 -1
- package/src/ui/AppShell.tsx +670 -719
- package/src/ui/AttachmentImportPanel.tsx +8 -8
- package/src/ui/AuthPanel.tsx +20 -20
- package/src/ui/BottomComposer.tsx +158 -118
- package/src/ui/DashCard.tsx +3 -3
- package/src/ui/Markdown.tsx +17 -17
- package/src/ui/ModelPickerScreen.tsx +222 -42
- package/src/ui/ModelReasoningPicker.tsx +15 -15
- package/src/ui/Panel.tsx +3 -3
- package/src/ui/PlanActionPicker.tsx +6 -6
- package/src/ui/PlanReviewPanel.tsx +9 -9
- package/src/ui/ProviderPicker.tsx +735 -321
- package/src/ui/RunFooter.tsx +3 -3
- package/src/ui/RuntimeStatusBar.tsx +108 -0
- package/src/ui/SelectionPanel.tsx +8 -4
- package/src/ui/SettingsPanel.tsx +9 -9
- package/src/ui/Spinner.tsx +1 -1
- package/src/ui/TextEntryPanel.tsx +11 -11
- package/src/ui/ThinkingBlock.tsx +8 -8
- package/src/ui/Timeline.tsx +1625 -1472
- package/src/ui/TopHeader.tsx +437 -293
- package/src/ui/TranscriptShell.tsx +322 -0
- package/src/ui/TurnGroup.tsx +33 -33
- package/src/ui/UpdateAvailableCard.tsx +41 -0
- package/src/ui/UpdatePromptPanel.tsx +197 -0
- package/src/ui/focus.ts +3 -0
- package/src/ui/layout.ts +299 -25
- package/src/ui/layoutListWindow.ts +145 -0
- package/src/ui/logoVariants.ts +103 -0
- package/src/ui/modeDisplay.ts +12 -12
- package/src/ui/runtimeDisplay.ts +112 -0
- package/src/ui/textLayout.ts +15 -4
- package/src/ui/theme.tsx +274 -395
- package/src/ui/timelineMeasure.ts +218 -136
- package/scripts/audit-codexa-capabilities.mjs +0 -466
- package/scripts/gen-build-info.mjs +0 -33
- package/scripts/smoke-terminal-bench.mjs +0 -35
- package/src/appRenderStability.test.ts +0 -131
- package/src/commands/handler.test.ts +0 -655
- package/src/config/launchArgs.test.ts +0 -189
- package/src/config/layeredConfig.test.ts +0 -143
- package/src/config/persistence.test.ts +0 -114
- package/src/config/runtimeConfig.test.ts +0 -218
- package/src/config/settings.test.ts +0 -155
- package/src/config/trustStore.test.ts +0 -29
- package/src/core/attachments.test.ts +0 -155
- package/src/core/auth/codexAuth.test.ts +0 -68
- package/src/core/cleanupFastFail.test.ts +0 -76
- package/src/core/codex.ts +0 -124
- package/src/core/codexExecArgs.test.ts +0 -195
- package/src/core/codexLaunch.test.ts +0 -205
- package/src/core/codexPrompt.test.ts +0 -252
- package/src/core/executables/codexExecutable.test.ts +0 -212
- package/src/core/executables/executableResolver.test.ts +0 -129
- package/src/core/executables/geminiExecutable.test.ts +0 -116
- package/src/core/executables/pathSanityScan.test.ts +0 -47
- package/src/core/githubDiagnostics.test.ts +0 -92
- package/src/core/hollowResponseFormat.test.ts +0 -58
- package/src/core/launchContext.test.ts +0 -157
- package/src/core/models/codexCapabilities.test.ts +0 -45
- package/src/core/models/codexModelCapabilities.test.ts +0 -246
- package/src/core/models/modelSpecs.test.ts +0 -283
- package/src/core/perf/renderDebug.test.ts +0 -230
- package/src/core/planStorage.test.ts +0 -143
- package/src/core/process/CommandRunner.test.ts +0 -105
- package/src/core/projectInstructions.test.ts +0 -50
- package/src/core/providerLauncher/launcher.test.ts +0 -238
- package/src/core/providerLauncher/registry.test.ts +0 -324
- package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
- package/src/core/providerRuntime/anthropic.test.ts +0 -1120
- package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
- package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
- package/src/core/providerRuntime/gemini.test.ts +0 -437
- package/src/core/providerRuntime/lmstudio.test.ts +0 -168
- package/src/core/providerRuntime/local.test.ts +0 -787
- package/src/core/providerRuntime/registry.test.ts +0 -233
- package/src/core/providers/codexJsonStream.test.ts +0 -148
- package/src/core/providers/codexSubprocess.test.ts +0 -68
- package/src/core/providers/codexTranscript.test.ts +0 -284
- package/src/core/terminal/startupClear.test.ts +0 -55
- package/src/core/terminal/terminalCapabilities.test.ts +0 -93
- package/src/core/terminal/terminalControl.test.ts +0 -75
- package/src/core/terminal/terminalSanitize.test.ts +0 -22
- package/src/core/terminal/terminalSelection.test.ts +0 -42
- package/src/core/terminal/terminalTitle.test.ts +0 -328
- package/src/core/updateCheck.test.ts +0 -194
- package/src/core/updateCheck.ts +0 -172
- package/src/core/workspaceActivity.test.ts +0 -163
- package/src/core/workspaceGuard.test.ts +0 -151
- package/src/core/workspaceRoot.test.ts +0 -23
- package/src/exec.test.ts +0 -13
- package/src/headless/execArgs.test.ts +0 -147
- package/src/headless/execRunner.test.ts +0 -436
- package/src/index.test.tsx +0 -620
- package/src/session/appSession.test.ts +0 -897
- package/src/session/chatLifecycle.test.ts +0 -64
- package/src/session/liveRenderScheduler.test.ts +0 -201
- package/src/session/planFlow.test.ts +0 -103
- package/src/session/planTranscript.test.ts +0 -65
- package/src/session/promptRunSchedule.test.ts +0 -36
- package/src/ui/ActivityIndicator.test.tsx +0 -58
- package/src/ui/AgentBlock.test.ts +0 -6
- package/src/ui/AnimatedStatusText.test.ts +0 -16
- package/src/ui/AppShell.test.tsx +0 -1776
- package/src/ui/AttachmentImportPanel.test.tsx +0 -204
- package/src/ui/BottomComposer.test.ts +0 -674
- package/src/ui/CodexLogo.tsx +0 -55
- package/src/ui/Markdown.test.ts +0 -157
- package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
- package/src/ui/ModelPickerScreen.test.tsx +0 -99
- package/src/ui/ModelPickerState.test.tsx +0 -151
- package/src/ui/ModelReasoningPicker.test.tsx +0 -447
- package/src/ui/PlanReviewPanel.test.tsx +0 -267
- package/src/ui/PromptCardBorder.test.tsx +0 -161
- package/src/ui/ProviderPicker.test.tsx +0 -289
- package/src/ui/ProviderShortcut.test.tsx +0 -143
- package/src/ui/SettingsPanel.test.tsx +0 -233
- package/src/ui/StaticTranscriptItem.tsx +0 -56
- package/src/ui/Timeline.test.ts +0 -2067
- package/src/ui/TimelineNavigation.test.tsx +0 -201
- package/src/ui/TopHeader.test.tsx +0 -254
- package/src/ui/TurnGroup.test.tsx +0 -365
- package/src/ui/busyStatusAnimation.test.ts +0 -30
- package/src/ui/commandNormalize.test.ts +0 -142
- package/src/ui/diffRenderer.test.ts +0 -102
- package/src/ui/focusFlow.test.tsx +0 -1098
- package/src/ui/inputBuffer.test.ts +0 -151
- package/src/ui/layout.test.ts +0 -146
- package/src/ui/modeDisplay.test.ts +0 -42
- package/src/ui/runActivityView.test.ts +0 -89
- package/src/ui/runLifecycleView.test.tsx +0 -237
- package/src/ui/statusRenderIsolation.test.tsx +0 -654
- package/src/ui/terminalAnswerFormat.test.ts +0 -19
- package/src/ui/textLayout.test.ts +0 -18
- package/src/ui/themeFlow.test.ts +0 -53
- package/src/ui/timelineMeasureCache.test.ts +0 -986
- /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
|
@@ -24,7 +24,8 @@ import {
|
|
|
24
24
|
import { selectVisibleRunActivity } from "./runActivityView.js";
|
|
25
25
|
import { getTextUnits, getTextWidth, wrapPlainText, wrapCommandText, splitTextAtColumn } from "./textLayout.js";
|
|
26
26
|
import type { RenderTimelineItem } from "./Timeline.js";
|
|
27
|
-
import { normalizePlanReviewMarkdown } from "../core/planStorage.js";
|
|
27
|
+
import { normalizePlanReviewMarkdown } from "../core/workspace/planStorage.js";
|
|
28
|
+
import { LOGO_COMPACT, LOGO_COMPACT_MIN_COLS, LOGO_LARGE_MIN_COLS, selectLogoVariant } from "./logoVariants.js";
|
|
28
29
|
|
|
29
30
|
// ─── Exported types ───────────────────────────────────────────────────────────
|
|
30
31
|
|
|
@@ -40,7 +41,10 @@ export type TimelineTone =
|
|
|
40
41
|
| "borderSubtle"
|
|
41
42
|
| "borderActive"
|
|
42
43
|
| "panel"
|
|
43
|
-
| "star"
|
|
44
|
+
| "star"
|
|
45
|
+
| "logoPrimary"
|
|
46
|
+
| "logoSecondary"
|
|
47
|
+
| "logoShadow";
|
|
44
48
|
|
|
45
49
|
export interface TimelineRowSpan {
|
|
46
50
|
text: string;
|
|
@@ -95,14 +99,8 @@ const MAX_VISIBLE_PROGRESS_ENTRIES = 3;
|
|
|
95
99
|
const COMPACT_PROCESSING_BODY_LINE_CAP = 4;
|
|
96
100
|
const COMPACT_STREAMING_TAIL_CAP = 6;
|
|
97
101
|
const VISIBLE_THINKING_SOURCES = new Set(["reasoning", "todo"]);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"██╔════╝██╔═══██╗██╔══██╗██╔════╝╚██╗██╔╝██╔══██╗",
|
|
101
|
-
"██║ ██║ ██║██║ ██║█████╗ ╚███╔╝ ███████║",
|
|
102
|
-
"██║ ██║ ██║██║ ██║██╔══╝ ██╔██╗ ██╔══██║",
|
|
103
|
-
"╚██████╗╚██████╔╝██████╔╝███████╗██╔╝ ██╗██║ ██║",
|
|
104
|
-
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝",
|
|
105
|
-
];
|
|
102
|
+
// Logo rows for the intro item — selected dynamically from logoVariants.ts so
|
|
103
|
+
// the dead-code intro path stays consistent with the live TopHeader rendering.
|
|
106
104
|
|
|
107
105
|
// Matches sentence-ending punctuation followed (optionally after whitespace) by
|
|
108
106
|
// a capital letter starting a new word. Requires [A-Z] to be followed by [a-z]
|
|
@@ -174,6 +172,37 @@ function padSpansToWidth(spans: TimelineRowSpan[], width: number): TimelineRowSp
|
|
|
174
172
|
return next;
|
|
175
173
|
}
|
|
176
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Truncates a span list to at most `width` display columns, cutting the
|
|
177
|
+
* overflowing span on a grapheme/display-width boundary. Used as a safety net so
|
|
178
|
+
* a too-wide content row can never push a card border past its declared width.
|
|
179
|
+
*/
|
|
180
|
+
function clampSpansToWidth(spans: TimelineRowSpan[], width: number): TimelineRowSpan[] {
|
|
181
|
+
const safeWidth = Math.max(0, width);
|
|
182
|
+
const result: TimelineRowSpan[] = [];
|
|
183
|
+
let used = 0;
|
|
184
|
+
for (const span of spans) {
|
|
185
|
+
if (used >= safeWidth) break;
|
|
186
|
+
const spanWidth = getTextWidth(span.text);
|
|
187
|
+
if (used + spanWidth <= safeWidth) {
|
|
188
|
+
result.push({ ...span });
|
|
189
|
+
used += spanWidth;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const fitted = splitTextAtColumn(span.text, safeWidth - used).before;
|
|
193
|
+
if (fitted) {
|
|
194
|
+
result.push(cloneSpan(span, fitted));
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Clamp a row to `width` then pad it back out so it occupies exactly `width`. */
|
|
202
|
+
function fitSpansToWidth(spans: TimelineRowSpan[], width: number): TimelineRowSpan[] {
|
|
203
|
+
return padSpansToWidth(clampSpansToWidth(spans, width), width);
|
|
204
|
+
}
|
|
205
|
+
|
|
177
206
|
const ROW_CONTENT_CACHE_LIMIT = 2500;
|
|
178
207
|
const _rowContentCache = new Map<string, TimelineRow>();
|
|
179
208
|
|
|
@@ -434,14 +463,14 @@ function buildDashCardRows(params: {
|
|
|
434
463
|
return { ...span, tone: borderTone };
|
|
435
464
|
});
|
|
436
465
|
|
|
437
|
-
const rows: TimelineRow[] = [createRow(`${params.keyPrefix}-top`, topRow, width)];
|
|
466
|
+
const rows: TimelineRow[] = [createRow(`${params.keyPrefix}-top`, fitSpansToWidth(topRow, width), width)];
|
|
438
467
|
|
|
439
468
|
params.contentRows.forEach((row, index) => {
|
|
440
469
|
rows.push(createRow(
|
|
441
470
|
`${params.keyPrefix}-content-${index}`,
|
|
442
471
|
[
|
|
443
472
|
createSpan("│ ", borderTone),
|
|
444
|
-
...
|
|
473
|
+
...fitSpansToWidth(row, contentWidth),
|
|
445
474
|
createSpan(" │", borderTone),
|
|
446
475
|
],
|
|
447
476
|
width,
|
|
@@ -488,7 +517,7 @@ function buildPanelRows(params: {
|
|
|
488
517
|
`${params.keyPrefix}-content-${index}`,
|
|
489
518
|
[
|
|
490
519
|
createSpan("│ ", "borderActive"),
|
|
491
|
-
...
|
|
520
|
+
...fitSpansToWidth(row, contentWidth),
|
|
492
521
|
createSpan(" │", "borderActive"),
|
|
493
522
|
],
|
|
494
523
|
width,
|
|
@@ -1539,18 +1568,19 @@ export function buildStandaloneEventRows(item: Extract<RenderTimelineItem, { typ
|
|
|
1539
1568
|
return rows;
|
|
1540
1569
|
}
|
|
1541
1570
|
|
|
1542
|
-
export function buildIntroRows(item: Extract<RenderTimelineItem, { type: "intro" }>, width: number): TimelineRow[] {
|
|
1571
|
+
export function buildIntroRows(item: Extract<RenderTimelineItem, { type: "intro" }>, width: number): TimelineRow[] {
|
|
1543
1572
|
const rows: TimelineRow[] = [];
|
|
1544
1573
|
const { intro } = item;
|
|
1545
1574
|
const safeWidth = Math.max(10, width);
|
|
1546
1575
|
const startupHeaderMode = intro.startupHeaderMode
|
|
1547
|
-
?? (intro.layoutMode === "
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1576
|
+
?? (intro.layoutMode === "expanded" ? "large" : "compact");
|
|
1577
|
+
const workspaceName = getWorkspaceDisplayName(intro.workspaceLabel);
|
|
1578
|
+
if (startupHeaderMode === "tiny") {
|
|
1579
|
+
const messageRows = [
|
|
1580
|
+
`Codexa v${intro.version}`,
|
|
1581
|
+
workspaceName ? `Workspace: ${workspaceName}` : null,
|
|
1582
|
+
intro.providerLabel ? `Provider: ${intro.providerLabel}` : `Auth: ${intro.authLabel}`,
|
|
1583
|
+
].filter((line): line is string => Boolean(line));
|
|
1554
1584
|
messageRows.forEach((line, index) => {
|
|
1555
1585
|
rows.push(createRow(
|
|
1556
1586
|
`${item.key}-resize-${index}`,
|
|
@@ -1561,15 +1591,20 @@ export function buildIntroRows(item: Extract<RenderTimelineItem, { type: "intro"
|
|
|
1561
1591
|
return rows;
|
|
1562
1592
|
}
|
|
1563
1593
|
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const
|
|
1594
|
+
// Compact startup mode deliberately uses the one-line mark even when the
|
|
1595
|
+
// terminal is wide: its row budget is what made the full logo unsafe.
|
|
1596
|
+
const logoRows = startupHeaderMode === "large"
|
|
1597
|
+
? selectLogoVariant(safeWidth)
|
|
1598
|
+
: safeWidth >= LOGO_COMPACT_MIN_COLS ? LOGO_COMPACT : [];
|
|
1599
|
+
const effectiveLogoRows = logoRows.length > 0 ? logoRows : ["CODEXA"];
|
|
1600
|
+
if (startupHeaderMode === "large") {
|
|
1601
|
+
rows.push(createBlankRow(`${item.key}-top-gap`, safeWidth));
|
|
1602
|
+
}
|
|
1603
|
+
const logoWidth = effectiveLogoRows.reduce((maxWidth, line) => Math.max(maxWidth, getTextWidth(line)), 0);
|
|
1604
|
+
const metaLines = [
|
|
1570
1605
|
`Codexa v${intro.version}`,
|
|
1571
|
-
`Auth: ${intro.authLabel}`,
|
|
1572
1606
|
workspaceName ? `Workspace: ${workspaceName}` : null,
|
|
1607
|
+
intro.providerLabel ? `Provider: ${intro.providerLabel}` : `Auth: ${intro.authLabel}`,
|
|
1573
1608
|
].filter((line): line is string => Boolean(line));
|
|
1574
1609
|
const gapWidth = 2;
|
|
1575
1610
|
const widestMetaLine = metaLines.reduce((maxWidth, line) => Math.max(maxWidth, getTextWidth(line)), 0);
|
|
@@ -1577,19 +1612,27 @@ export function buildIntroRows(item: Extract<RenderTimelineItem, { type: "intro"
|
|
|
1577
1612
|
&& safeWidth >= logoWidth + gapWidth + widestMetaLine;
|
|
1578
1613
|
|
|
1579
1614
|
if (canRenderSideBySide) {
|
|
1580
|
-
const metaStartRow = Math.max(0, Math.floor((
|
|
1581
|
-
const rowCount = Math.max(
|
|
1615
|
+
const metaStartRow = Math.max(0, Math.floor((effectiveLogoRows.length - metaLines.length) / 2));
|
|
1616
|
+
const rowCount = Math.max(effectiveLogoRows.length, metaStartRow + metaLines.length);
|
|
1582
1617
|
const metaWidth = Math.max(1, safeWidth - logoWidth - gapWidth);
|
|
1583
1618
|
|
|
1584
1619
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
1585
|
-
const logoLine =
|
|
1620
|
+
const logoLine = effectiveLogoRows[rowIndex] ?? "";
|
|
1586
1621
|
const logoPadding = Math.max(0, logoWidth - getTextWidth(logoLine));
|
|
1587
1622
|
const metaIndex = rowIndex - metaStartRow;
|
|
1588
1623
|
const metaLine = metaIndex >= 0 && metaIndex < metaLines.length
|
|
1589
1624
|
? sanitizeTerminalOutput(metaLines[metaIndex]!)
|
|
1590
1625
|
: "";
|
|
1626
|
+
let logoTone: TimelineTone = "logoPrimary";
|
|
1627
|
+
if (effectiveLogoRows.length === 6) {
|
|
1628
|
+
if (rowIndex === 2 || rowIndex === 3) logoTone = "logoSecondary";
|
|
1629
|
+
else if (rowIndex === 4 || rowIndex === 5) logoTone = "logoShadow";
|
|
1630
|
+
} else if (effectiveLogoRows === LOGO_COMPACT) {
|
|
1631
|
+
logoTone = "accent";
|
|
1632
|
+
}
|
|
1633
|
+
// No bold on logo spans — bold on block/box-drawing chars causes spacing artifacts.
|
|
1591
1634
|
const spans = [
|
|
1592
|
-
createSpan(`${logoLine}${" ".repeat(logoPadding)}`,
|
|
1635
|
+
createSpan(`${logoLine}${" ".repeat(logoPadding)}`, logoTone),
|
|
1593
1636
|
createSpan(" ".repeat(gapWidth)),
|
|
1594
1637
|
];
|
|
1595
1638
|
|
|
@@ -1604,10 +1647,17 @@ export function buildIntroRows(item: Extract<RenderTimelineItem, { type: "intro"
|
|
|
1604
1647
|
));
|
|
1605
1648
|
}
|
|
1606
1649
|
} else {
|
|
1607
|
-
|
|
1650
|
+
effectiveLogoRows.forEach((line, index) => {
|
|
1651
|
+
let logoTone: TimelineTone = "logoPrimary";
|
|
1652
|
+
if (effectiveLogoRows.length === 6) {
|
|
1653
|
+
if (index === 2 || index === 3) logoTone = "logoSecondary";
|
|
1654
|
+
else if (index === 4 || index === 5) logoTone = "logoShadow";
|
|
1655
|
+
} else if (effectiveLogoRows === LOGO_COMPACT) {
|
|
1656
|
+
logoTone = "accent";
|
|
1657
|
+
}
|
|
1608
1658
|
rows.push(createRow(
|
|
1609
1659
|
`${item.key}-logo-${index}`,
|
|
1610
|
-
[createSpan(clampVisualText(line, safeWidth),
|
|
1660
|
+
[createSpan(clampVisualText(line, safeWidth), logoTone)],
|
|
1611
1661
|
safeWidth,
|
|
1612
1662
|
));
|
|
1613
1663
|
});
|
|
@@ -1684,7 +1734,7 @@ function applyTurnOpacity(rows: TimelineRow[], opacity: "active" | "recent" | "d
|
|
|
1684
1734
|
// ─── Stream event types ───────────────────────────────────────────────────────
|
|
1685
1735
|
|
|
1686
1736
|
export type StreamEvent =
|
|
1687
|
-
| { kind: "thinking"; streamSeq: number; block: RunProgressBlock
|
|
1737
|
+
| { kind: "thinking"; streamSeq: number; block: RunProgressBlock }
|
|
1688
1738
|
| { kind: "response"; streamSeq: number; segment: RunResponseSegment }
|
|
1689
1739
|
| { kind: "action"; streamSeq: number; tool: RunToolActivity }
|
|
1690
1740
|
| { kind: "actionSummary"; streamSeq: number; id: string; label: string; count: number }
|
|
@@ -1701,8 +1751,24 @@ function getCompactableActionLabel(event: StreamEvent): string | null {
|
|
|
1701
1751
|
return label === "Read file" || label === "List files" ? label : null;
|
|
1702
1752
|
}
|
|
1703
1753
|
|
|
1704
|
-
|
|
1705
|
-
|
|
1754
|
+
/**
|
|
1755
|
+
* Collapse bursts of same-label completed action cards into a single summary
|
|
1756
|
+
* line. This is a *height-reducing* transform, so it must only run for a
|
|
1757
|
+
* FINISHED turn: applying it while the run is still live shrinks the turn's
|
|
1758
|
+
* total height mid-stream, and the bottom-anchored viewport then re-reveals
|
|
1759
|
+
* earlier (already-scrolled-off) content — the "old states come back" glitch.
|
|
1760
|
+
*
|
|
1761
|
+
* Callers pass `finalized = run.status !== "running"`. We deliberately key off
|
|
1762
|
+
* `run.status` rather than the render phase: `resolveTurnRunPhase` reports
|
|
1763
|
+
* "final" during the ANSWER_VISIBLE window while the run is still running, so a
|
|
1764
|
+
* phase-based gate would compact during that intermediate, still-active frame.
|
|
1765
|
+
*/
|
|
1766
|
+
export function compactActionBursts(
|
|
1767
|
+
events: StreamEvent[],
|
|
1768
|
+
verbose: boolean,
|
|
1769
|
+
finalized: boolean,
|
|
1770
|
+
): StreamEvent[] {
|
|
1771
|
+
if (verbose || !finalized) return events;
|
|
1706
1772
|
|
|
1707
1773
|
const compacted: StreamEvent[] = [];
|
|
1708
1774
|
for (let index = 0; index < events.length;) {
|
|
@@ -1764,13 +1830,11 @@ function buildCodexThinkingRows(params: {
|
|
|
1764
1830
|
keyPrefix: string;
|
|
1765
1831
|
width: number;
|
|
1766
1832
|
event: Extract<StreamEvent, { kind: "thinking" }>;
|
|
1767
|
-
isLive: boolean;
|
|
1768
1833
|
verbose: boolean;
|
|
1769
1834
|
}): TimelineRow[] {
|
|
1770
1835
|
renderDebug.traceRender("ThinkingBlock", params.event.block.status, {
|
|
1771
1836
|
keyPrefix: params.keyPrefix,
|
|
1772
1837
|
streamSeq: params.event.streamSeq,
|
|
1773
|
-
isLive: params.isLive,
|
|
1774
1838
|
textLength: params.event.block.text.length,
|
|
1775
1839
|
});
|
|
1776
1840
|
|
|
@@ -1784,8 +1848,6 @@ function buildCodexThinkingRows(params: {
|
|
|
1784
1848
|
textCacheToken(block.text),
|
|
1785
1849
|
params.width,
|
|
1786
1850
|
params.verbose,
|
|
1787
|
-
params.isLive,
|
|
1788
|
-
params.event.isActive,
|
|
1789
1851
|
]);
|
|
1790
1852
|
|
|
1791
1853
|
return getCachedStreamingBlockRows(cacheKey, () => {
|
|
@@ -1806,10 +1868,6 @@ function buildCodexThinkingRows(params: {
|
|
|
1806
1868
|
]);
|
|
1807
1869
|
}
|
|
1808
1870
|
|
|
1809
|
-
if (params.isLive && params.event.isActive) {
|
|
1810
|
-
contentRows.push([createSpan("▌", "accent")]);
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
1871
|
return buildCodexPlainRows(params.keyPrefix, params.width, contentRows);
|
|
1814
1872
|
});
|
|
1815
1873
|
}
|
|
@@ -1839,10 +1897,15 @@ function getActionDisplayDescriptor(params: {
|
|
|
1839
1897
|
isLive: boolean;
|
|
1840
1898
|
borderTone: TimelineTone;
|
|
1841
1899
|
}): ActionDisplayDescriptor {
|
|
1842
|
-
|
|
1900
|
+
// Strip ANSI/control sequences before measuring or wrapping: string-width only
|
|
1901
|
+
// collapses *complete* escape sequences, so leftover bytes would otherwise be
|
|
1902
|
+
// counted (and wrapped) character-by-character and corrupt the card width.
|
|
1903
|
+
const command = normalizeCommand(sanitizeTerminalOutput(params.tool.command));
|
|
1843
1904
|
const label = getFriendlyActionLabel(command);
|
|
1905
|
+
// Bare label (no leading gap) — the head-row builder right-aligns it and owns
|
|
1906
|
+
// the spacing, so the gap can never get baked into a width calculation.
|
|
1844
1907
|
const duration = params.tool.completedAt != null
|
|
1845
|
-
?
|
|
1908
|
+
? formatDuration(params.tool.completedAt - params.tool.startedAt)
|
|
1846
1909
|
: "";
|
|
1847
1910
|
const summary = params.verbose ? params.tool.summary ?? "" : "";
|
|
1848
1911
|
const showLiveCursor = params.isLive && params.tool.status === "running";
|
|
@@ -1869,7 +1932,7 @@ function getActionDisplayDescriptor(params: {
|
|
|
1869
1932
|
return descriptor;
|
|
1870
1933
|
}
|
|
1871
1934
|
|
|
1872
|
-
function buildPlainActionDebugRows(params: {
|
|
1935
|
+
function buildPlainActionDebugRows(params: {
|
|
1873
1936
|
keyPrefix: string;
|
|
1874
1937
|
width: number;
|
|
1875
1938
|
descriptor: ActionDisplayDescriptor;
|
|
@@ -1877,7 +1940,7 @@ function buildPlainActionDebugRows(params: {
|
|
|
1877
1940
|
const statusText = params.descriptor.label
|
|
1878
1941
|
? `${params.descriptor.label}: ${params.descriptor.command}`
|
|
1879
1942
|
: params.descriptor.command;
|
|
1880
|
-
const suffix = params.descriptor.duration ? params.descriptor.duration : "";
|
|
1943
|
+
const suffix = params.descriptor.duration ? ` ${params.descriptor.duration}` : "";
|
|
1881
1944
|
const text = clampVisualText(`${params.descriptor.icon} ${statusText}${suffix}`, Math.max(1, params.width - 1));
|
|
1882
1945
|
renderDebug.traceEvent("action", "plainActionRow", {
|
|
1883
1946
|
actionId: params.descriptor.id,
|
|
@@ -1893,10 +1956,56 @@ function buildPlainActionDebugRows(params: {
|
|
|
1893
1956
|
],
|
|
1894
1957
|
params.width,
|
|
1895
1958
|
),
|
|
1896
|
-
];
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
|
|
1959
|
+
];
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function compactActionText(descriptor: ActionDisplayDescriptor): string {
|
|
1963
|
+
const command = descriptor.command.replace(/^([a-z][a-z0-9_]*):\s+/i, "$1 ");
|
|
1964
|
+
return descriptor.label && command === descriptor.command
|
|
1965
|
+
? `${descriptor.label} ${command}`
|
|
1966
|
+
: command;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function buildCompactActionRows(params: {
|
|
1970
|
+
keyPrefix: string;
|
|
1971
|
+
width: number;
|
|
1972
|
+
descriptor: ActionDisplayDescriptor;
|
|
1973
|
+
}): TimelineRow[] {
|
|
1974
|
+
const durationSuffix = params.descriptor.duration ? ` ${params.descriptor.duration}` : "";
|
|
1975
|
+
const liveSuffix = params.descriptor.showLiveCursor ? " ▌" : "";
|
|
1976
|
+
const availableWidth = Math.max(1, params.width - getTextWidth(params.descriptor.icon) - 1 - getTextWidth(durationSuffix) - getTextWidth(liveSuffix));
|
|
1977
|
+
const text = clampVisualText(compactActionText(params.descriptor), availableWidth);
|
|
1978
|
+
const rows: TimelineRow[] = [
|
|
1979
|
+
createRow(
|
|
1980
|
+
`${params.keyPrefix}-plain`,
|
|
1981
|
+
[
|
|
1982
|
+
createSpan(`${params.descriptor.icon} `, params.descriptor.iconTone),
|
|
1983
|
+
createSpan(text || " ", "text"),
|
|
1984
|
+
...(durationSuffix ? [createSpan(durationSuffix, "dim")] : []),
|
|
1985
|
+
...(liveSuffix ? [createSpan(liveSuffix, "accent")] : []),
|
|
1986
|
+
],
|
|
1987
|
+
params.width,
|
|
1988
|
+
),
|
|
1989
|
+
];
|
|
1990
|
+
|
|
1991
|
+
if (params.descriptor.verbose) {
|
|
1992
|
+
const detail = params.descriptor.showLiveCursor
|
|
1993
|
+
? "running"
|
|
1994
|
+
: params.descriptor.summary.trim() || "completed";
|
|
1995
|
+
rows.push(createRow(
|
|
1996
|
+
`${params.keyPrefix}-detail`,
|
|
1997
|
+
[
|
|
1998
|
+
createSpan(" "),
|
|
1999
|
+
createSpan(clampVisualText(detail, Math.max(1, params.width - 2)), "muted"),
|
|
2000
|
+
],
|
|
2001
|
+
params.width,
|
|
2002
|
+
));
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
return rows;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
export function buildActionEventRows(params: {
|
|
1900
2009
|
keyPrefix: string;
|
|
1901
2010
|
width: number;
|
|
1902
2011
|
event: Extract<StreamEvent, { kind: "action" }>;
|
|
@@ -1967,58 +2076,11 @@ export function buildActionEventRows(params: {
|
|
|
1967
2076
|
});
|
|
1968
2077
|
}
|
|
1969
2078
|
|
|
1970
|
-
const buildActionRows = () => {
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
: descriptor.summary.trim()
|
|
1976
|
-
? descriptor.summary
|
|
1977
|
-
: " ";
|
|
1978
|
-
const contentRows: TimelineRowSpan[][] = [];
|
|
1979
|
-
|
|
1980
|
-
if (descriptor.label) {
|
|
1981
|
-
contentRows.push([
|
|
1982
|
-
createSpan(`${descriptor.icon} `, descriptor.iconTone),
|
|
1983
|
-
createSpan(descriptor.label, "text"),
|
|
1984
|
-
...(descriptor.duration ? [createSpan(descriptor.duration, "dim")] : []),
|
|
1985
|
-
]);
|
|
1986
|
-
wrapPlainText(descriptor.command, commandWidth).forEach((row) => {
|
|
1987
|
-
contentRows.push([
|
|
1988
|
-
createSpan(" "),
|
|
1989
|
-
createSpan(row || " ", "muted"),
|
|
1990
|
-
]);
|
|
1991
|
-
});
|
|
1992
|
-
} else {
|
|
1993
|
-
const headRows = wrapPlainText(descriptor.command, commandWidth);
|
|
1994
|
-
headRows.forEach((row, rowIndex) => {
|
|
1995
|
-
contentRows.push([
|
|
1996
|
-
createSpan(rowIndex === 0 ? `${descriptor.icon} ` : " ", rowIndex === 0 ? descriptor.iconTone : undefined),
|
|
1997
|
-
createSpan(row || " ", "text"),
|
|
1998
|
-
...(rowIndex === 0 && descriptor.duration ? [createSpan(descriptor.duration, "dim")] : []),
|
|
1999
|
-
]);
|
|
2000
|
-
});
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
if (!descriptor.label) {
|
|
2004
|
-
contentRows.push([
|
|
2005
|
-
createSpan(" "),
|
|
2006
|
-
createSpan(" ", "muted"),
|
|
2007
|
-
]);
|
|
2008
|
-
}
|
|
2009
|
-
contentRows.push([
|
|
2010
|
-
createSpan(" "),
|
|
2011
|
-
createSpan(detailText, descriptor.showLiveCursor ? "accent" : "muted"),
|
|
2012
|
-
]);
|
|
2013
|
-
|
|
2014
|
-
return buildDashCardRows({
|
|
2015
|
-
keyPrefix: params.keyPrefix,
|
|
2016
|
-
width: params.width,
|
|
2017
|
-
title: "action",
|
|
2018
|
-
borderTone: descriptor.borderTone,
|
|
2019
|
-
contentRows: contentRows.length > 0 ? contentRows : [[createSpan(" ")]],
|
|
2020
|
-
});
|
|
2021
|
-
};
|
|
2079
|
+
const buildActionRows = () => buildCompactActionRows({
|
|
2080
|
+
keyPrefix: params.keyPrefix,
|
|
2081
|
+
width: params.width,
|
|
2082
|
+
descriptor,
|
|
2083
|
+
});
|
|
2022
2084
|
|
|
2023
2085
|
if (isCompleted) {
|
|
2024
2086
|
const rows = buildActionRows();
|
|
@@ -2168,13 +2230,11 @@ function buildApprovedPlanRows(params: {
|
|
|
2168
2230
|
|
|
2169
2231
|
function buildUnifiedStreamRows(item: Extract<RenderTimelineItem, { type: "turn" }>, width: number, options: { verbose?: boolean; workspaceRoot?: string | null }): TimelineRow[] {
|
|
2170
2232
|
const run = item.item.run!;
|
|
2171
|
-
const assistant = item.item.assistant;
|
|
2172
2233
|
const streaming = item.renderState.runPhase === "streaming";
|
|
2173
|
-
const dim = item.renderState.opacity !== "active";
|
|
2174
|
-
const borderTone = dim ? "borderSubtle" : streaming ? "borderActive" : "borderSubtle";
|
|
2175
2234
|
const actionBorderTone = item.renderState.opacity === "dim" ? "borderSubtle" : "borderActive";
|
|
2176
2235
|
const verbose = options.verbose ?? false;
|
|
2177
|
-
const
|
|
2236
|
+
const finalized = run.status !== "running";
|
|
2237
|
+
const events = compactActionBursts(collectStreamEvents(item), verbose, finalized);
|
|
2178
2238
|
|
|
2179
2239
|
const rows: TimelineRow[] = [];
|
|
2180
2240
|
|
|
@@ -2183,7 +2243,9 @@ function buildUnifiedStreamRows(item: Extract<RenderTimelineItem, { type: "turn"
|
|
|
2183
2243
|
const isLive = run.status === "running" && isLastEvent; // The cursor is on the last event
|
|
2184
2244
|
|
|
2185
2245
|
if (index > 0) {
|
|
2186
|
-
|
|
2246
|
+
// Key the gap by the stable creation-order streamSeq, not the array
|
|
2247
|
+
// index, so gaps don't remount/reorder when the event set changes.
|
|
2248
|
+
rows.push(createBlankRow(`${item.key}-stream-gap-${event.streamSeq}`, width));
|
|
2187
2249
|
}
|
|
2188
2250
|
|
|
2189
2251
|
if (event.kind === "thinking") {
|
|
@@ -2191,7 +2253,6 @@ function buildUnifiedStreamRows(item: Extract<RenderTimelineItem, { type: "turn"
|
|
|
2191
2253
|
keyPrefix: `${item.key}-codex-thinking-${event.streamSeq}`,
|
|
2192
2254
|
width,
|
|
2193
2255
|
event,
|
|
2194
|
-
isLive,
|
|
2195
2256
|
verbose,
|
|
2196
2257
|
}));
|
|
2197
2258
|
} else if (event.kind === "action") {
|
|
@@ -2232,7 +2293,7 @@ function buildUnifiedStreamRows(item: Extract<RenderTimelineItem, { type: "turn"
|
|
|
2232
2293
|
}
|
|
2233
2294
|
});
|
|
2234
2295
|
|
|
2235
|
-
if (!streaming &&
|
|
2296
|
+
if (!streaming && finalized) {
|
|
2236
2297
|
if (run.status === "canceled") {
|
|
2237
2298
|
rows.push(createBlankRow(`${item.key}-cancel-gap`, width));
|
|
2238
2299
|
rows.push(...buildCodexPlainRows(
|
|
@@ -2271,12 +2332,10 @@ function buildUnifiedStreamRows(item: Extract<RenderTimelineItem, { type: "turn"
|
|
|
2271
2332
|
return rows;
|
|
2272
2333
|
}
|
|
2273
2334
|
|
|
2274
|
-
function collectStreamEvents(
|
|
2275
|
-
item: Extract<RenderTimelineItem, { type: "turn" }>,
|
|
2276
|
-
streaming: boolean,
|
|
2277
|
-
): StreamEvent[] {
|
|
2335
|
+
function collectStreamEvents(item: Extract<RenderTimelineItem, { type: "turn" }>): StreamEvent[] {
|
|
2278
2336
|
const run = item.item.run!;
|
|
2279
2337
|
const assistant = item.item.assistant;
|
|
2338
|
+
const streaming = item.renderState.runPhase === "streaming";
|
|
2280
2339
|
const blocksById = new Map<string, RunProgressBlock>();
|
|
2281
2340
|
for (const entry of run.progressEntries ?? []) {
|
|
2282
2341
|
for (const block of entry.blocks) blocksById.set(block.id, block);
|
|
@@ -2288,14 +2347,23 @@ function collectStreamEvents(
|
|
|
2288
2347
|
const sortedItems = (run.streamItems ?? []).slice().sort((a, b) => a.streamSeq - b.streamSeq);
|
|
2289
2348
|
for (const it of sortedItems) {
|
|
2290
2349
|
if (it.kind === "thinking") {
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2350
|
+
// Active-turn topology stability: while the run is live we never surface
|
|
2351
|
+
// reasoning blocks. A thinking block is assigned its streamSeq early (when
|
|
2352
|
+
// its reasoning first streams) but only *completes* later — revealing it
|
|
2353
|
+
// mid-stream slots it in at that early streamSeq, ABOVE answer/action
|
|
2354
|
+
// blocks that have already streamed at higher streamSeqs. That late
|
|
2355
|
+
// insert-above is what reorders the live turn. Defer all reasoning to
|
|
2356
|
+
// finalize, where the full streamSeq order (reasoning included) reflows
|
|
2357
|
+
// atomically. (Height grows when it appears — never shrinks mid-stream.)
|
|
2358
|
+
if (run.status !== "running") {
|
|
2359
|
+
const block = blocksById.get(it.refId);
|
|
2360
|
+
if (block && block.text.trim().length > 0) {
|
|
2361
|
+
events.push({
|
|
2362
|
+
kind: "thinking",
|
|
2363
|
+
streamSeq: it.streamSeq,
|
|
2364
|
+
block,
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
2299
2367
|
}
|
|
2300
2368
|
} else if (it.kind === "action") {
|
|
2301
2369
|
const tool = toolsById.get(it.refId);
|
|
@@ -2326,13 +2394,15 @@ function collectStreamEvents(
|
|
|
2326
2394
|
if (!VISIBLE_THINKING_SOURCES.has(entry.source)) continue;
|
|
2327
2395
|
for (const block of entry.blocks) {
|
|
2328
2396
|
if (!block.text.trim()) continue;
|
|
2329
|
-
|
|
2397
|
+
// Same active-turn topology rule as the streamItems path: defer all
|
|
2398
|
+
// reasoning while the run is live so it cannot insert above already
|
|
2399
|
+
// streamed answer/action blocks. Reveal only once finalized.
|
|
2400
|
+
if (run.status === "running") continue;
|
|
2330
2401
|
legacySeq += 1;
|
|
2331
2402
|
events.push({
|
|
2332
2403
|
kind: "thinking",
|
|
2333
2404
|
streamSeq: legacySeq,
|
|
2334
2405
|
block,
|
|
2335
|
-
isActive: run.status === "running" && block.status === "active",
|
|
2336
2406
|
});
|
|
2337
2407
|
}
|
|
2338
2408
|
}
|
|
@@ -2467,8 +2537,10 @@ function buildStableIntroRows(item: Extract<RenderTimelineItem, { type: "intro"
|
|
|
2467
2537
|
innerWidth,
|
|
2468
2538
|
item.intro.version,
|
|
2469
2539
|
item.intro.layoutMode,
|
|
2540
|
+
item.intro.startupHeaderMode ?? "",
|
|
2470
2541
|
item.intro.authLabel,
|
|
2471
2542
|
textCacheToken(item.intro.workspaceLabel),
|
|
2543
|
+
item.intro.providerLabel ?? "",
|
|
2472
2544
|
]);
|
|
2473
2545
|
return getCachedFrozenRows(cacheKey, () => buildIntroRows(item, innerWidth));
|
|
2474
2546
|
}
|
|
@@ -2542,10 +2614,9 @@ function buildStableActiveTurnGroups(
|
|
|
2542
2614
|
}
|
|
2543
2615
|
|
|
2544
2616
|
const streaming = item.renderState.runPhase === "streaming";
|
|
2545
|
-
const dim = item.renderState.opacity !== "active";
|
|
2546
|
-
const borderTone = dim ? "borderSubtle" : streaming ? "borderActive" : "borderSubtle";
|
|
2547
2617
|
const actionBorderTone = item.renderState.opacity === "dim" ? "borderSubtle" : "borderActive";
|
|
2548
|
-
const
|
|
2618
|
+
const finalized = run.status !== "running";
|
|
2619
|
+
const events = compactActionBursts(collectStreamEvents(item), verbose, finalized);
|
|
2549
2620
|
let orderedRows = [...getCachedFrozenRows(rowCacheKey([
|
|
2550
2621
|
"stable-active-user",
|
|
2551
2622
|
item.key,
|
|
@@ -2561,7 +2632,9 @@ function buildStableActiveTurnGroups(
|
|
|
2561
2632
|
const isLastEvent = index === events.length - 1;
|
|
2562
2633
|
|
|
2563
2634
|
if (index > 0) {
|
|
2564
|
-
|
|
2635
|
+
// Stable creation-order key (streamSeq), not array index — matches the
|
|
2636
|
+
// native path and avoids index-based remount when events change.
|
|
2637
|
+
targetRows.push(createBlankRow(`${item.key}-stream-gap-${event.streamSeq}`, innerWidth));
|
|
2565
2638
|
}
|
|
2566
2639
|
|
|
2567
2640
|
if (event.kind === "thinking") {
|
|
@@ -2569,7 +2642,6 @@ function buildStableActiveTurnGroups(
|
|
|
2569
2642
|
keyPrefix: `${item.key}-codex-thinking-${event.streamSeq}`,
|
|
2570
2643
|
width: innerWidth,
|
|
2571
2644
|
event,
|
|
2572
|
-
isLive: liveEvent,
|
|
2573
2645
|
verbose,
|
|
2574
2646
|
});
|
|
2575
2647
|
targetRows.push(...(liveEvent ? build() : getCachedFrozenRows(rowCacheKey([
|
|
@@ -2698,7 +2770,6 @@ function buildNativeStreamEventRows(params: {
|
|
|
2698
2770
|
keyPrefix: `${item.key}-codex-thinking-${event.streamSeq}`,
|
|
2699
2771
|
width: innerWidth,
|
|
2700
2772
|
event,
|
|
2701
|
-
isLive: !params.forceStable && isNativeLiveStreamEvent(event, run),
|
|
2702
2773
|
verbose,
|
|
2703
2774
|
}));
|
|
2704
2775
|
} else if (event.kind === "action") {
|
|
@@ -2785,8 +2856,9 @@ function appendNativeTurnParts(
|
|
|
2785
2856
|
if (!run) return;
|
|
2786
2857
|
|
|
2787
2858
|
const events = compactActionBursts(
|
|
2788
|
-
collectStreamEvents(item
|
|
2859
|
+
collectStreamEvents(item),
|
|
2789
2860
|
verbose,
|
|
2861
|
+
run.status !== "running",
|
|
2790
2862
|
);
|
|
2791
2863
|
|
|
2792
2864
|
events.forEach((event, eventIndex) => {
|
|
@@ -2965,7 +3037,7 @@ export function buildTimelineSnapshot(
|
|
|
2965
3037
|
let builtRows: TimelineRow[];
|
|
2966
3038
|
|
|
2967
3039
|
if (item.type === "intro") {
|
|
2968
|
-
const cacheKey = `i:${item.key}:${innerWidth}:${item.intro.version}:${item.intro.layoutMode}:${item.intro.startupHeaderMode ?? ""}:${item.intro.authLabel}:${item.intro.workspaceLabel}`;
|
|
3040
|
+
const cacheKey = `i:${item.key}:${innerWidth}:${item.intro.version}:${item.intro.layoutMode}:${item.intro.startupHeaderMode ?? ""}:${item.intro.authLabel}:${item.intro.workspaceLabel}:${item.intro.providerLabel ?? ""}:v${LOGO_LARGE_MIN_COLS}`;
|
|
2969
3041
|
const cached = _staticRowCache.get(cacheKey);
|
|
2970
3042
|
if (cached) {
|
|
2971
3043
|
renderDebug.traceEvent("timeline", "rowGeneration", {
|
|
@@ -2989,8 +3061,18 @@ export function buildTimelineSnapshot(
|
|
|
2989
3061
|
builtRows = r;
|
|
2990
3062
|
}
|
|
2991
3063
|
} else if (item.type === "event") {
|
|
2992
|
-
// Standalone events
|
|
2993
|
-
const cacheKey =
|
|
3064
|
+
// Standalone events are immutable for a given event payload, not just id.
|
|
3065
|
+
const cacheKey = rowCacheKey([
|
|
3066
|
+
"event",
|
|
3067
|
+
item.key,
|
|
3068
|
+
item.event.type,
|
|
3069
|
+
item.event.id,
|
|
3070
|
+
innerWidth,
|
|
3071
|
+
textCacheToken("title" in item.event ? item.event.title : item.event.command),
|
|
3072
|
+
textCacheToken("content" in item.event ? item.event.content : item.event.summary ?? ""),
|
|
3073
|
+
"status" in item.event ? item.event.status : "",
|
|
3074
|
+
"durationMs" in item.event ? item.event.durationMs : "",
|
|
3075
|
+
]);
|
|
2994
3076
|
const cached = _staticRowCache.get(cacheKey);
|
|
2995
3077
|
if (cached) {
|
|
2996
3078
|
renderDebug.traceEvent("timeline", "rowGeneration", {
|