@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,814 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as renderDebug from "../perf/renderDebug.js";
|
|
3
|
+
import { resetInkOutputForFreshFrame, type InkRenderInstance } from "./inkRenderReset.js";
|
|
4
|
+
import { traceTerminalClear } from "./terminalControl.js";
|
|
5
|
+
|
|
6
|
+
interface ClearBoundaryStdoutLike {
|
|
7
|
+
columns?: number;
|
|
8
|
+
rows?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface TerminalClearLike {
|
|
12
|
+
clearTranscript: (source: string) => void;
|
|
13
|
+
clearViewport: (source: string) => void;
|
|
14
|
+
setAlternateScreen?: (enabled: boolean, source: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface LogShadowState {
|
|
18
|
+
previousOutputLength: number;
|
|
19
|
+
previousLineCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface TerminalControlSequenceStats {
|
|
23
|
+
cursorUpCount: number;
|
|
24
|
+
cursorHomeCount: number;
|
|
25
|
+
eraseLineCount: number;
|
|
26
|
+
eraseDisplayCount: number;
|
|
27
|
+
carriageReturnCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type InkLogLike = {
|
|
31
|
+
(output: string): unknown;
|
|
32
|
+
clear?: () => unknown;
|
|
33
|
+
sync?: (output: string) => unknown;
|
|
34
|
+
reset?: () => unknown;
|
|
35
|
+
willRender?: (output: string) => unknown;
|
|
36
|
+
isCursorDirty?: () => unknown;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface SavedNormalBufferFrame {
|
|
40
|
+
lastOutput: string;
|
|
41
|
+
lastOutputToRender: string;
|
|
42
|
+
lastOutputHeight: number;
|
|
43
|
+
fullStaticOutput: string;
|
|
44
|
+
cols: number | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ClearFrameBoundaryRenderState {
|
|
48
|
+
generation: number;
|
|
49
|
+
staticEventsLength: number;
|
|
50
|
+
activeEventsLength: number;
|
|
51
|
+
transcriptCleared: boolean;
|
|
52
|
+
clearGenerationReady?: boolean;
|
|
53
|
+
uiStateKind: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ClearFrameBoundaryController {
|
|
57
|
+
beginClearGeneration: (generation: number) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Mirror the React render state into the boundary's gate. Returns `true` when
|
|
60
|
+
* a post-clear authoritative frame is now ready to commit but hasn't been
|
|
61
|
+
* written yet — the caller must force one more render so Ink's
|
|
62
|
+
* renderInteractiveFrame runs again and flushes it (see app.tsx).
|
|
63
|
+
*/
|
|
64
|
+
syncRenderState: (state: ClearFrameBoundaryRenderState) => boolean;
|
|
65
|
+
getState: () => {
|
|
66
|
+
clearPending: boolean;
|
|
67
|
+
pendingGeneration: number | null;
|
|
68
|
+
committedGeneration: number | null;
|
|
69
|
+
renderGeneration: number;
|
|
70
|
+
transcriptCleared: boolean;
|
|
71
|
+
lastFrameWasAuthoritative: boolean;
|
|
72
|
+
widthRepaintPending: boolean;
|
|
73
|
+
overlayActive: boolean;
|
|
74
|
+
logShadow: LogShadowState;
|
|
75
|
+
};
|
|
76
|
+
dispose: () => void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface CreateClearFrameBoundaryOptions {
|
|
80
|
+
instance: InkRenderInstance | null;
|
|
81
|
+
terminalControl: TerminalClearLike;
|
|
82
|
+
stdout: ClearBoundaryStdoutLike;
|
|
83
|
+
source?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Reports whether the committing render tree is an overlay screen (any
|
|
86
|
+
* screen other than "main"). Read at frame-write time so alternate-screen
|
|
87
|
+
* enter/exit happens atomically with the first frame of the new screen —
|
|
88
|
+
* never from an effect that runs after the frame already hit the wrong
|
|
89
|
+
* buffer. The callback must reflect the render that produced the frame
|
|
90
|
+
* (app.tsx passes a ref assigned during render).
|
|
91
|
+
*/
|
|
92
|
+
isOverlayActive?: () => boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Called synchronously whenever a width change requires the home/transcript
|
|
95
|
+
* content to be rebuilt. The physical clear is DEFERRED until the caller has
|
|
96
|
+
* re-rendered with a fresh <Static> instance: Ink's <Static> never re-emits
|
|
97
|
+
* items once flushed, so the boundary suppresses frames until a frame
|
|
98
|
+
* arrives that carries the re-flushed static content, then clears and writes
|
|
99
|
+
* that frame atomically. The caller must use this to force a fresh render
|
|
100
|
+
* with a new <Static> key so its content gets reflushed at the new width.
|
|
101
|
+
*/
|
|
102
|
+
onWidthResizeRefresh?: () => void;
|
|
103
|
+
/**
|
|
104
|
+
* Reports the repaint generation of the committing render tree (the counter
|
|
105
|
+
* onWidthResizeRefresh bumps), read at frame-write time like isOverlayActive.
|
|
106
|
+
* Lets the boundary tell the genuine post-bump <Static> re-flush apart from
|
|
107
|
+
* an incremental pre-resize static chunk that happens to land while the
|
|
108
|
+
* repaint is pending — committing the latter would clear the scrollback and
|
|
109
|
+
* then write only that chunk, losing the rest of the transcript.
|
|
110
|
+
*/
|
|
111
|
+
getRenderedRepaintGeneration?: () => number;
|
|
112
|
+
/**
|
|
113
|
+
* Reports the raw terminal column count the committing render tree was laid
|
|
114
|
+
* out against, read at frame-write time. The viewport hook commits new
|
|
115
|
+
* dimensions on a trailing settle (~100ms after the resize event), so the
|
|
116
|
+
* <Static> re-flush must not be requested until a commit whose layout
|
|
117
|
+
* already matches stdout.columns — remounting earlier would re-flush the
|
|
118
|
+
* transcript at the pre-resize width.
|
|
119
|
+
*/
|
|
120
|
+
getRenderedLayoutCols?: () => number | undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface FrameMarkerCounts {
|
|
124
|
+
codexaLogoCount: number;
|
|
125
|
+
providerMigratedCount: number;
|
|
126
|
+
launchModeCount: number;
|
|
127
|
+
composerCount: number;
|
|
128
|
+
footerCount: number;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const LOGO_LINE = /██╔════╝██╔═══██╗/g;
|
|
132
|
+
const PROVIDER_MIGRATED = /Provider migrated/g;
|
|
133
|
+
const LAUNCH_MODE = /Launch mode/g;
|
|
134
|
+
const COMPOSER = /│ ❯/g;
|
|
135
|
+
const FOOTER_CONTEXT = /Context:/g;
|
|
136
|
+
const ANSI_SEQUENCE = /\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07]*(?:\x07|\x1b\\)/g;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Safety valve for the deferred width repaint: if the re-flushed static frame
|
|
140
|
+
* never arrives (unexpected render stall), stop suppressing and commit the
|
|
141
|
+
* next frame with whatever static content Ink has accumulated, so the UI can
|
|
142
|
+
* never freeze behind the gate.
|
|
143
|
+
*/
|
|
144
|
+
const WIDTH_REPAINT_MAX_SUPPRESSED_FRAMES = 8;
|
|
145
|
+
|
|
146
|
+
function stableFrameHash(text: string): string {
|
|
147
|
+
return createHash("sha1").update(text, "utf8").digest("hex").slice(0, 12);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function countMatches(text: string, pattern: RegExp): number {
|
|
151
|
+
pattern.lastIndex = 0;
|
|
152
|
+
let count = 0;
|
|
153
|
+
while (pattern.exec(text)) count += 1;
|
|
154
|
+
pattern.lastIndex = 0;
|
|
155
|
+
return count;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function countFrameMarkers(text: string): FrameMarkerCounts {
|
|
159
|
+
const plainText = text.replace(ANSI_SEQUENCE, "");
|
|
160
|
+
return {
|
|
161
|
+
codexaLogoCount: countMatches(plainText, LOGO_LINE),
|
|
162
|
+
providerMigratedCount: countMatches(plainText, PROVIDER_MIGRATED),
|
|
163
|
+
launchModeCount: countMatches(plainText, LAUNCH_MODE),
|
|
164
|
+
composerCount: countMatches(plainText, COMPOSER),
|
|
165
|
+
footerCount: countMatches(plainText, FOOTER_CONTEXT),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function buildFrameText(instance: InkRenderInstance, output: string, staticOutput = ""): string {
|
|
170
|
+
const fullStaticOutput = instance.fullStaticOutput ?? "";
|
|
171
|
+
if (!staticOutput) return `${fullStaticOutput}${output}`;
|
|
172
|
+
if (!fullStaticOutput || staticOutput.includes(fullStaticOutput)) {
|
|
173
|
+
return `${staticOutput}${output}`;
|
|
174
|
+
}
|
|
175
|
+
return `${fullStaticOutput}${staticOutput}${output}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function snapshotFrameState(instance: InkRenderInstance, logShadow: LogShadowState) {
|
|
179
|
+
return {
|
|
180
|
+
lastOutputLength: (instance.lastOutput ?? "").length,
|
|
181
|
+
lastOutputToRenderLength: (instance.lastOutputToRender ?? "").length,
|
|
182
|
+
lastOutputHeight: instance.lastOutputHeight ?? 0,
|
|
183
|
+
fullStaticOutputLength: (instance.fullStaticOutput ?? "").length,
|
|
184
|
+
logPreviousOutputLength: logShadow.previousOutputLength,
|
|
185
|
+
logPreviousLineCount: logShadow.previousLineCount,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function lineCountForOutput(output: string): number {
|
|
190
|
+
if (output.length === 0) return 0;
|
|
191
|
+
return output.split("\n").length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function countAnsiMatches(text: string, pattern: RegExp): number {
|
|
195
|
+
pattern.lastIndex = 0;
|
|
196
|
+
let count = 0;
|
|
197
|
+
while (pattern.exec(text)) count += 1;
|
|
198
|
+
pattern.lastIndex = 0;
|
|
199
|
+
return count;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function inspectControlSequences(output: string): TerminalControlSequenceStats {
|
|
203
|
+
return {
|
|
204
|
+
cursorUpCount: countAnsiMatches(output, /\x1b\[[0-9]*A/g),
|
|
205
|
+
cursorHomeCount: countAnsiMatches(output, /\x1b\[[0-9;]*H/g),
|
|
206
|
+
eraseLineCount: countAnsiMatches(output, /\x1b\[[0-9]*K/g),
|
|
207
|
+
eraseDisplayCount: countAnsiMatches(output, /\x1b\[[0-9]*J/g),
|
|
208
|
+
carriageReturnCount: countAnsiMatches(output, /\r/g),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function wrapInkLogForTrace(
|
|
213
|
+
instance: InkRenderInstance,
|
|
214
|
+
source: string,
|
|
215
|
+
shadow: LogShadowState,
|
|
216
|
+
): () => void {
|
|
217
|
+
const currentLog = instance.log;
|
|
218
|
+
if (typeof currentLog !== "function") {
|
|
219
|
+
return () => {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const original = currentLog as InkLogLike;
|
|
223
|
+
|
|
224
|
+
const wrapped: InkLogLike = ((output: string) => {
|
|
225
|
+
const before = { ...shadow };
|
|
226
|
+
const controlSequences = inspectControlSequences(output);
|
|
227
|
+
const result = original(output);
|
|
228
|
+
shadow.previousOutputLength = output.length;
|
|
229
|
+
shadow.previousLineCount = lineCountForOutput(output);
|
|
230
|
+
renderDebug.traceEvent("terminal", "inkLogWrite", {
|
|
231
|
+
source,
|
|
232
|
+
outputLength: output.length,
|
|
233
|
+
outputHash: stableFrameHash(output),
|
|
234
|
+
previousOutputLengthBefore: before.previousOutputLength,
|
|
235
|
+
previousLineCountBefore: before.previousLineCount,
|
|
236
|
+
previousOutputLengthAfter: shadow.previousOutputLength,
|
|
237
|
+
previousLineCountAfter: shadow.previousLineCount,
|
|
238
|
+
...controlSequences,
|
|
239
|
+
});
|
|
240
|
+
return result;
|
|
241
|
+
}) as InkLogLike;
|
|
242
|
+
|
|
243
|
+
wrapped.clear = () => {
|
|
244
|
+
const before = { ...shadow };
|
|
245
|
+
const result = original.clear?.();
|
|
246
|
+
shadow.previousOutputLength = 0;
|
|
247
|
+
shadow.previousLineCount = 0;
|
|
248
|
+
renderDebug.traceEvent("terminal", "inkLogClear", {
|
|
249
|
+
source,
|
|
250
|
+
previousOutputLengthBefore: before.previousOutputLength,
|
|
251
|
+
previousLineCountBefore: before.previousLineCount,
|
|
252
|
+
previousOutputLengthAfter: shadow.previousOutputLength,
|
|
253
|
+
previousLineCountAfter: shadow.previousLineCount,
|
|
254
|
+
});
|
|
255
|
+
return result;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
wrapped.sync = (output: string) => {
|
|
259
|
+
const before = { ...shadow };
|
|
260
|
+
const controlSequences = inspectControlSequences(output);
|
|
261
|
+
const result = original.sync?.(output);
|
|
262
|
+
shadow.previousOutputLength = output.length;
|
|
263
|
+
shadow.previousLineCount = lineCountForOutput(output);
|
|
264
|
+
renderDebug.traceEvent("terminal", "inkLogSync", {
|
|
265
|
+
source,
|
|
266
|
+
outputLength: output.length,
|
|
267
|
+
previousOutputLengthBefore: before.previousOutputLength,
|
|
268
|
+
previousLineCountBefore: before.previousLineCount,
|
|
269
|
+
previousOutputLengthAfter: shadow.previousOutputLength,
|
|
270
|
+
previousLineCountAfter: shadow.previousLineCount,
|
|
271
|
+
...controlSequences,
|
|
272
|
+
});
|
|
273
|
+
return result;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
wrapped.reset = () => {
|
|
277
|
+
const before = { ...shadow };
|
|
278
|
+
const result = original.reset?.();
|
|
279
|
+
shadow.previousOutputLength = 0;
|
|
280
|
+
shadow.previousLineCount = 0;
|
|
281
|
+
renderDebug.traceEvent("terminal", "inkLogReset", {
|
|
282
|
+
source,
|
|
283
|
+
previousOutputLengthBefore: before.previousOutputLength,
|
|
284
|
+
previousLineCountBefore: before.previousLineCount,
|
|
285
|
+
previousOutputLengthAfter: shadow.previousOutputLength,
|
|
286
|
+
previousLineCountAfter: shadow.previousLineCount,
|
|
287
|
+
});
|
|
288
|
+
return result;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
wrapped.willRender = original.willRender ? (output: string) => original.willRender?.(output) : undefined;
|
|
292
|
+
wrapped.isCursorDirty = original.isCursorDirty ? () => original.isCursorDirty?.() : undefined;
|
|
293
|
+
|
|
294
|
+
instance.log = wrapped as unknown as { reset?: () => void };
|
|
295
|
+
|
|
296
|
+
return () => {
|
|
297
|
+
instance.log = original as unknown as { reset?: () => void };
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function createClearFrameBoundaryController({
|
|
302
|
+
instance,
|
|
303
|
+
terminalControl,
|
|
304
|
+
stdout,
|
|
305
|
+
source = "src/core/terminal/clearFrameBoundary.ts",
|
|
306
|
+
isOverlayActive,
|
|
307
|
+
onWidthResizeRefresh,
|
|
308
|
+
getRenderedRepaintGeneration,
|
|
309
|
+
getRenderedLayoutCols,
|
|
310
|
+
}: CreateClearFrameBoundaryOptions): ClearFrameBoundaryController | null {
|
|
311
|
+
const originalRenderInteractiveFrame = instance?.renderInteractiveFrame;
|
|
312
|
+
if (!instance || typeof originalRenderInteractiveFrame !== "function") {
|
|
313
|
+
renderDebug.traceEvent("terminal", "clearBoundaryInit", {
|
|
314
|
+
source,
|
|
315
|
+
liveInkInstanceResolved: Boolean(instance),
|
|
316
|
+
renderInteractiveFrameResolved: false,
|
|
317
|
+
});
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let clearPending = false;
|
|
322
|
+
let pendingGeneration: number | null = null;
|
|
323
|
+
let committedGeneration: number | null = null;
|
|
324
|
+
let renderGeneration = 0;
|
|
325
|
+
let transcriptCleared = false;
|
|
326
|
+
let clearGenerationReady = false;
|
|
327
|
+
let staticEventsLength = 0;
|
|
328
|
+
let activeEventsLength = 0;
|
|
329
|
+
let uiStateKind = "IDLE";
|
|
330
|
+
let lastFrameWasAuthoritative = false;
|
|
331
|
+
let suppressedPostClearStaticOutput = "";
|
|
332
|
+
// Deferred width repaint: armed when the terminal width changes under a
|
|
333
|
+
// main-screen frame, resolved when the re-flushed static frame arrives.
|
|
334
|
+
let widthRepaintPending = false;
|
|
335
|
+
let widthRepaintSuppressedFrames = 0;
|
|
336
|
+
let widthRepaintTargetGeneration: number | null = null;
|
|
337
|
+
let widthRepaintReflushRequested = false;
|
|
338
|
+
// Alternate-screen overlay state. While an overlay is active, the normal
|
|
339
|
+
// buffer's Ink caches are parked here so diffing can resume against the
|
|
340
|
+
// buffer's true contents when the overlay exits (DECSET 1049 restores the
|
|
341
|
+
// normal buffer byte-for-byte).
|
|
342
|
+
let overlayActive = false;
|
|
343
|
+
let savedNormalFrame: SavedNormalBufferFrame | null = null;
|
|
344
|
+
let overlayCapturedStaticOutput = "";
|
|
345
|
+
let previousCols = stdout.columns;
|
|
346
|
+
let previousRows = stdout.rows;
|
|
347
|
+
const logShadow: LogShadowState = { previousOutputLength: 0, previousLineCount: 0 };
|
|
348
|
+
|
|
349
|
+
const restoreLog = wrapInkLogForTrace(instance, source, logShadow);
|
|
350
|
+
const boundOriginal = originalRenderInteractiveFrame.bind(instance);
|
|
351
|
+
|
|
352
|
+
const isPostClearFrameReady = (): boolean => {
|
|
353
|
+
if (!clearPending || pendingGeneration === null) return false;
|
|
354
|
+
return renderGeneration >= pendingGeneration && (transcriptCleared || clearGenerationReady);
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const setAlternateScreen = (enabled: boolean, reason: string): void => {
|
|
358
|
+
terminalControl.setAlternateScreen?.(enabled, `${source}:${reason}`);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Ink's onRender appends this frame's static chunk to fullStaticOutput
|
|
363
|
+
* BEFORE calling renderInteractiveFrame. When the chunk must not land in the
|
|
364
|
+
* currently active buffer (overlay frames), peel it back off so the cache
|
|
365
|
+
* keeps describing the normal buffer only.
|
|
366
|
+
*/
|
|
367
|
+
const detachStaticChunkFromFullStatic = (staticOutput: string): void => {
|
|
368
|
+
if (!staticOutput) return;
|
|
369
|
+
const fullStatic = instance.fullStaticOutput ?? "";
|
|
370
|
+
if (fullStatic.endsWith(staticOutput)) {
|
|
371
|
+
instance.fullStaticOutput = fullStatic.slice(0, fullStatic.length - staticOutput.length);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const armWidthRepaint = (reason: string): void => {
|
|
376
|
+
widthRepaintPending = true;
|
|
377
|
+
widthRepaintSuppressedFrames = 0;
|
|
378
|
+
widthRepaintTargetGeneration = null;
|
|
379
|
+
// The <Static> re-flush is requested later, from the first suppressed
|
|
380
|
+
// frame whose committed layout already matches the new terminal width —
|
|
381
|
+
// requesting it here would remount <Static> against the pre-resize layout
|
|
382
|
+
// (the viewport hook commits dimensions on a trailing settle).
|
|
383
|
+
widthRepaintReflushRequested = false;
|
|
384
|
+
renderDebug.traceEvent("terminal", "widthRepaintArmed", {
|
|
385
|
+
source,
|
|
386
|
+
reason,
|
|
387
|
+
currentCols: stdout.columns,
|
|
388
|
+
});
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const commitAuthoritativeFrame = (
|
|
392
|
+
output: string,
|
|
393
|
+
outputHeight: number,
|
|
394
|
+
staticOutput: string,
|
|
395
|
+
clearMode: "transcript" | "viewport",
|
|
396
|
+
reason: string,
|
|
397
|
+
): void => {
|
|
398
|
+
traceTerminalClear(`${source}:${reason}`, {
|
|
399
|
+
mode: clearMode,
|
|
400
|
+
clearGeneration: pendingGeneration ?? committedGeneration,
|
|
401
|
+
});
|
|
402
|
+
if (clearMode === "transcript") {
|
|
403
|
+
terminalControl.clearTranscript(`${source}:${reason}`);
|
|
404
|
+
} else {
|
|
405
|
+
terminalControl.clearViewport(`${source}:${reason}`);
|
|
406
|
+
}
|
|
407
|
+
const resetRan = resetInkOutputForFreshFrame({ instance, columns: stdout.columns });
|
|
408
|
+
renderDebug.traceEvent("terminal", "authoritativeFrameCommit", {
|
|
409
|
+
source,
|
|
410
|
+
reason,
|
|
411
|
+
clearMode,
|
|
412
|
+
inkCacheResetEmitted: resetRan,
|
|
413
|
+
staticLength: staticOutput.length,
|
|
414
|
+
outputLength: output.length,
|
|
415
|
+
});
|
|
416
|
+
lastFrameWasAuthoritative = true;
|
|
417
|
+
boundOriginal(output, outputHeight, staticOutput);
|
|
418
|
+
// Keep fullStaticOutput describing what is actually above the live frame in
|
|
419
|
+
// this buffer; onRender's accumulation was wiped by the cache reset above.
|
|
420
|
+
instance.fullStaticOutput = staticOutput;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
instance.renderInteractiveFrame = (output: string, outputHeight: number, staticOutput: string) => {
|
|
424
|
+
const currentCols = stdout.columns;
|
|
425
|
+
const currentRows = stdout.rows;
|
|
426
|
+
const widthChanged = previousCols !== currentCols;
|
|
427
|
+
const resizeDetected = widthChanged || previousRows !== currentRows;
|
|
428
|
+
const overlayNow = Boolean(isOverlayActive?.());
|
|
429
|
+
previousCols = currentCols;
|
|
430
|
+
previousRows = currentRows;
|
|
431
|
+
|
|
432
|
+
if (resizeDetected) {
|
|
433
|
+
renderDebug.traceEvent("terminal", "resizeAfterClear", {
|
|
434
|
+
currentCols,
|
|
435
|
+
currentRows,
|
|
436
|
+
widthChanged,
|
|
437
|
+
clearPending,
|
|
438
|
+
widthRepaintPending,
|
|
439
|
+
overlayActive,
|
|
440
|
+
overlayNow,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── Overlay enter: main → overlay ─────────────────────────────────────
|
|
445
|
+
// Switch buffers BEFORE the overlay frame is written so it lands in the
|
|
446
|
+
// alternate screen, never in the normal buffer/scrollback.
|
|
447
|
+
if (overlayNow && !overlayActive) {
|
|
448
|
+
detachStaticChunkFromFullStatic(staticOutput);
|
|
449
|
+
savedNormalFrame = {
|
|
450
|
+
lastOutput: instance.lastOutput ?? "",
|
|
451
|
+
lastOutputToRender: instance.lastOutputToRender ?? "",
|
|
452
|
+
lastOutputHeight: instance.lastOutputHeight ?? 0,
|
|
453
|
+
fullStaticOutput: instance.fullStaticOutput ?? "",
|
|
454
|
+
cols: currentCols,
|
|
455
|
+
};
|
|
456
|
+
// Static content that arrives from this frame on belongs to the normal
|
|
457
|
+
// buffer's transcript — hold it until the overlay exits.
|
|
458
|
+
overlayCapturedStaticOutput = staticOutput;
|
|
459
|
+
overlayActive = true;
|
|
460
|
+
setAlternateScreen(true, "overlayEnter");
|
|
461
|
+
// DECSET 1049 keeps the cursor where the normal buffer left it; home it
|
|
462
|
+
// so the overlay frame starts at the top of the blank alternate buffer.
|
|
463
|
+
terminalControl.clearViewport(`${source}:overlayEnter`);
|
|
464
|
+
resetInkOutputForFreshFrame({ instance, columns: currentCols });
|
|
465
|
+
renderDebug.traceEvent("terminal", "overlayEnter", {
|
|
466
|
+
source,
|
|
467
|
+
currentCols,
|
|
468
|
+
currentRows,
|
|
469
|
+
savedLastOutputHeight: savedNormalFrame.lastOutputHeight,
|
|
470
|
+
capturedStaticLength: overlayCapturedStaticOutput.length,
|
|
471
|
+
});
|
|
472
|
+
boundOriginal(output, outputHeight, "");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// ── Overlay exit: overlay → main ──────────────────────────────────────
|
|
477
|
+
if (!overlayNow && overlayActive) {
|
|
478
|
+
overlayActive = false;
|
|
479
|
+
detachStaticChunkFromFullStatic(staticOutput);
|
|
480
|
+
setAlternateScreen(false, "overlayExit");
|
|
481
|
+
|
|
482
|
+
const saved = savedNormalFrame;
|
|
483
|
+
savedNormalFrame = null;
|
|
484
|
+
const pendingStatic = `${overlayCapturedStaticOutput}${staticOutput}`;
|
|
485
|
+
overlayCapturedStaticOutput = "";
|
|
486
|
+
|
|
487
|
+
if (clearPending) {
|
|
488
|
+
// A /clear was issued while the overlay was open; the post-clear gate
|
|
489
|
+
// owns the next authoritative frame. Fall through to the clear
|
|
490
|
+
// machinery below with the caches untouched.
|
|
491
|
+
if (pendingStatic) {
|
|
492
|
+
suppressedPostClearStaticOutput = pendingStatic;
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
// DECSET 1049l restored the normal buffer exactly as saved; restore the
|
|
496
|
+
// caches so Ink diffs against what is really on screen.
|
|
497
|
+
if (saved) {
|
|
498
|
+
instance.lastOutput = saved.lastOutput;
|
|
499
|
+
instance.lastOutputToRender = saved.lastOutputToRender;
|
|
500
|
+
instance.lastOutputHeight = saved.lastOutputHeight;
|
|
501
|
+
instance.fullStaticOutput = saved.fullStaticOutput;
|
|
502
|
+
(instance.log as InkLogLike | undefined)?.sync?.(saved.lastOutputToRender);
|
|
503
|
+
}
|
|
504
|
+
renderDebug.traceEvent("terminal", "overlayExit", {
|
|
505
|
+
source,
|
|
506
|
+
currentCols,
|
|
507
|
+
currentRows,
|
|
508
|
+
widthChangedWhileAway: saved ? saved.cols !== currentCols : false,
|
|
509
|
+
pendingStaticLength: pendingStatic.length,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
if (saved && saved.cols !== currentCols) {
|
|
513
|
+
// The terminal was resized while the overlay was open; the restored
|
|
514
|
+
// normal buffer reflowed and is stale. Repaint it from scratch. The
|
|
515
|
+
// remounted <Static> re-emits everything held in React state, so the
|
|
516
|
+
// pending chunks are covered by the re-flush.
|
|
517
|
+
armWidthRepaint("overlayExitWidthChanged");
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
instance.fullStaticOutput = `${instance.fullStaticOutput ?? ""}${pendingStatic}`;
|
|
522
|
+
boundOriginal(output, outputHeight, pendingStatic);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ── Overlay steady state ──────────────────────────────────────────────
|
|
528
|
+
if (overlayNow) {
|
|
529
|
+
if (staticOutput) {
|
|
530
|
+
// Transcript rows flushed while the overlay is open would be written
|
|
531
|
+
// into the alternate buffer and lost on exit; hold them instead.
|
|
532
|
+
detachStaticChunkFromFullStatic(staticOutput);
|
|
533
|
+
overlayCapturedStaticOutput += staticOutput;
|
|
534
|
+
}
|
|
535
|
+
if (widthChanged) {
|
|
536
|
+
// The alternate buffer has no scrollback; a viewport clear plus a full
|
|
537
|
+
// rewrite is a complete repaint.
|
|
538
|
+
terminalControl.clearViewport(`${source}:overlayResize`);
|
|
539
|
+
resetInkOutputForFreshFrame({ instance, columns: currentCols });
|
|
540
|
+
lastFrameWasAuthoritative = true;
|
|
541
|
+
}
|
|
542
|
+
boundOriginal(output, outputHeight, "");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ── Main screen: /clear generation gate ───────────────────────────────
|
|
547
|
+
const postClearReady = isPostClearFrameReady();
|
|
548
|
+
const isFirstPostClearCommit = clearPending && postClearReady;
|
|
549
|
+
const staleFrameSuppressed = clearPending && !postClearReady;
|
|
550
|
+
|
|
551
|
+
// ── Main screen: deferred width repaint ───────────────────────────────
|
|
552
|
+
// A width change reflows the previously-rendered frame; a diffed write
|
|
553
|
+
// would leave that reflowed frame stacked behind the new one — GNOME
|
|
554
|
+
// Terminal keeps it in scrollback on a grow and re-exposes it. But this
|
|
555
|
+
// frame was still built from React state that may predate the resize, and
|
|
556
|
+
// <Static> never re-emits flushed content on its own. So: suppress frames
|
|
557
|
+
// until the caller has re-rendered with a fresh <Static> (its re-flushed
|
|
558
|
+
// content arrives as staticOutput), then clear scrollback and write that
|
|
559
|
+
// frame atomically. Skip while a clear is pending: that path owns the next
|
|
560
|
+
// authoritative frame and already repaints from a clean baseline.
|
|
561
|
+
if (widthChanged && !clearPending && !widthRepaintPending) {
|
|
562
|
+
armWidthRepaint("widthChanged");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (widthRepaintPending && !clearPending) {
|
|
566
|
+
widthRepaintSuppressedFrames += 1;
|
|
567
|
+
|
|
568
|
+
// Phase 1: wait for a commit whose layout matches the new terminal
|
|
569
|
+
// width (the viewport hook commits dimensions on a trailing settle),
|
|
570
|
+
// then request the <Static> re-flush exactly once.
|
|
571
|
+
const renderedLayoutCols = getRenderedLayoutCols?.();
|
|
572
|
+
const layoutReady = getRenderedLayoutCols === undefined
|
|
573
|
+
|| renderedLayoutCols === currentCols;
|
|
574
|
+
if (!widthRepaintReflushRequested) {
|
|
575
|
+
if (layoutReady && widthRepaintSuppressedFrames <= WIDTH_REPAINT_MAX_SUPPRESSED_FRAMES) {
|
|
576
|
+
widthRepaintReflushRequested = true;
|
|
577
|
+
widthRepaintTargetGeneration = getRenderedRepaintGeneration
|
|
578
|
+
? getRenderedRepaintGeneration() + 1
|
|
579
|
+
: null;
|
|
580
|
+
renderDebug.traceEvent("terminal", "widthRepaintReflushRequested", {
|
|
581
|
+
source,
|
|
582
|
+
currentCols,
|
|
583
|
+
suppressedFrames: widthRepaintSuppressedFrames,
|
|
584
|
+
targetGeneration: widthRepaintTargetGeneration,
|
|
585
|
+
});
|
|
586
|
+
onWidthResizeRefresh?.();
|
|
587
|
+
// This frame still carries the pre-remount static state; suppress it
|
|
588
|
+
// and commit the re-flushed frame the bump produces.
|
|
589
|
+
if (getRenderedRepaintGeneration !== undefined) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Phase 2: commit the first frame that carries the re-flushed static
|
|
596
|
+
// content from the post-bump render.
|
|
597
|
+
const repaintGenerationRendered = widthRepaintTargetGeneration === null
|
|
598
|
+
|| (getRenderedRepaintGeneration?.() ?? 0) >= widthRepaintTargetGeneration;
|
|
599
|
+
const repaintFrameReady = staticOutput !== ""
|
|
600
|
+
&& repaintGenerationRendered
|
|
601
|
+
&& widthRepaintReflushRequested;
|
|
602
|
+
if (repaintFrameReady) {
|
|
603
|
+
widthRepaintPending = false;
|
|
604
|
+
widthRepaintTargetGeneration = null;
|
|
605
|
+
widthRepaintReflushRequested = false;
|
|
606
|
+
commitAuthoritativeFrame(output, outputHeight, staticOutput, "transcript", "resizeRefresh");
|
|
607
|
+
renderDebug.traceEvent("terminal", "resizeRefreshCommitted", {
|
|
608
|
+
committedGeneration,
|
|
609
|
+
currentCols,
|
|
610
|
+
resizeRefreshConsumed: true,
|
|
611
|
+
...countFrameMarkers(`${staticOutput}${output}`),
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (widthRepaintSuppressedFrames <= WIDTH_REPAINT_MAX_SUPPRESSED_FRAMES) {
|
|
616
|
+
renderDebug.traceEvent("terminal", "widthRepaintFrameSuppressed", {
|
|
617
|
+
source,
|
|
618
|
+
suppressedFrames: widthRepaintSuppressedFrames,
|
|
619
|
+
currentCols,
|
|
620
|
+
layoutReady,
|
|
621
|
+
reflushRequested: widthRepaintReflushRequested,
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
// Safety valve: never freeze the UI behind the gate. Repaint with the
|
|
626
|
+
// static content Ink has accumulated, even if it predates the resize.
|
|
627
|
+
widthRepaintPending = false;
|
|
628
|
+
widthRepaintTargetGeneration = null;
|
|
629
|
+
widthRepaintReflushRequested = false;
|
|
630
|
+
commitAuthoritativeFrame(
|
|
631
|
+
output,
|
|
632
|
+
outputHeight,
|
|
633
|
+
staticOutput || (instance.fullStaticOutput ?? ""),
|
|
634
|
+
"transcript",
|
|
635
|
+
"resizeRefreshFallback",
|
|
636
|
+
);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const frameClassification = isFirstPostClearCommit
|
|
641
|
+
? "post-clear"
|
|
642
|
+
: (clearPending ? "pre-clear" : "normal");
|
|
643
|
+
const effectiveStaticOutput = isFirstPostClearCommit && !staticOutput && suppressedPostClearStaticOutput
|
|
644
|
+
? suppressedPostClearStaticOutput
|
|
645
|
+
: staticOutput;
|
|
646
|
+
const frameText = isFirstPostClearCommit
|
|
647
|
+
? `${effectiveStaticOutput}${output}`
|
|
648
|
+
: buildFrameText(instance, output, staticOutput);
|
|
649
|
+
const markerCounts = countFrameMarkers(frameText);
|
|
650
|
+
const frameHash = stableFrameHash(frameText);
|
|
651
|
+
const clearGenerationUsed = isFirstPostClearCommit ? pendingGeneration : committedGeneration;
|
|
652
|
+
const before = snapshotFrameState(instance, logShadow);
|
|
653
|
+
|
|
654
|
+
renderDebug.traceEvent("terminal", "clearBoundaryFrame", {
|
|
655
|
+
clearPending,
|
|
656
|
+
pendingGeneration,
|
|
657
|
+
committedGeneration,
|
|
658
|
+
renderGeneration,
|
|
659
|
+
transcriptCleared,
|
|
660
|
+
staticEventsLength,
|
|
661
|
+
activeEventsLength,
|
|
662
|
+
uiStateKind,
|
|
663
|
+
frameClassification,
|
|
664
|
+
isPostClearFrame: postClearReady,
|
|
665
|
+
staleFrameSuppressed,
|
|
666
|
+
frameWriteAllowed: !staleFrameSuppressed,
|
|
667
|
+
widthChanged,
|
|
668
|
+
currentCols,
|
|
669
|
+
currentRows,
|
|
670
|
+
clearGenerationUsed,
|
|
671
|
+
frameLength: frameText.length,
|
|
672
|
+
frameHash,
|
|
673
|
+
...markerCounts,
|
|
674
|
+
...before,
|
|
675
|
+
previousLineCountBefore: before.logPreviousLineCount,
|
|
676
|
+
lastOutputHeightBefore: before.lastOutputHeight,
|
|
677
|
+
physicalClearImmediatelyBeforeFrame: isFirstPostClearCommit,
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
if (staleFrameSuppressed) {
|
|
681
|
+
if (staticOutput) {
|
|
682
|
+
suppressedPostClearStaticOutput = staticOutput;
|
|
683
|
+
}
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (isFirstPostClearCommit) {
|
|
688
|
+
commitAuthoritativeFrame(output, outputHeight, effectiveStaticOutput, "transcript", "firstPostClearFrame");
|
|
689
|
+
renderDebug.traceEvent("terminal", "clearBoundaryCommit", {
|
|
690
|
+
clearGeneration: pendingGeneration,
|
|
691
|
+
physicalTerminalClearEmitted: true,
|
|
692
|
+
before,
|
|
693
|
+
afterReset: snapshotFrameState(instance, logShadow),
|
|
694
|
+
});
|
|
695
|
+
} else {
|
|
696
|
+
lastFrameWasAuthoritative = false;
|
|
697
|
+
boundOriginal(output, outputHeight, effectiveStaticOutput);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const after = snapshotFrameState(instance, logShadow);
|
|
701
|
+
renderDebug.traceEvent("terminal", "clearBoundaryFrameCommitted", {
|
|
702
|
+
clearPending,
|
|
703
|
+
pendingGeneration,
|
|
704
|
+
committedGeneration,
|
|
705
|
+
renderGeneration,
|
|
706
|
+
transcriptCleared,
|
|
707
|
+
frameClassification: isFirstPostClearCommit ? "post-clear-authoritative" : frameClassification,
|
|
708
|
+
frameLength: frameText.length,
|
|
709
|
+
frameHash,
|
|
710
|
+
...markerCounts,
|
|
711
|
+
...after,
|
|
712
|
+
diffedFrame: !isFirstPostClearCommit,
|
|
713
|
+
fullAuthoritativeFrame: isFirstPostClearCommit,
|
|
714
|
+
widthChanged,
|
|
715
|
+
currentCols,
|
|
716
|
+
currentRows,
|
|
717
|
+
clearGenerationUsed,
|
|
718
|
+
previousLineCountAfter: after.logPreviousLineCount,
|
|
719
|
+
lastOutputHeightAfter: after.lastOutputHeight,
|
|
720
|
+
physicalClearImmediatelyBeforeFrame: isFirstPostClearCommit,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
if (isFirstPostClearCommit) {
|
|
724
|
+
committedGeneration = pendingGeneration;
|
|
725
|
+
clearPending = false;
|
|
726
|
+
pendingGeneration = null;
|
|
727
|
+
suppressedPostClearStaticOutput = "";
|
|
728
|
+
renderDebug.traceEvent("terminal", "firstCommittedPostClearFrame", {
|
|
729
|
+
committedGeneration,
|
|
730
|
+
frameHash,
|
|
731
|
+
firstFrameAuthoritative: true,
|
|
732
|
+
...markerCounts,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
renderDebug.traceEvent("terminal", "clearBoundaryInit", {
|
|
738
|
+
source,
|
|
739
|
+
liveInkInstanceResolved: true,
|
|
740
|
+
renderInteractiveFrameResolved: true,
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
return {
|
|
744
|
+
beginClearGeneration(generation) {
|
|
745
|
+
if (!Number.isFinite(generation)) return false;
|
|
746
|
+
clearPending = true;
|
|
747
|
+
pendingGeneration = generation;
|
|
748
|
+
suppressedPostClearStaticOutput = "";
|
|
749
|
+
// The post-clear frame repaints from a physically cleared baseline, which
|
|
750
|
+
// supersedes any in-flight width repaint.
|
|
751
|
+
widthRepaintPending = false;
|
|
752
|
+
renderDebug.traceEvent("terminal", "clearBoundaryBegin", {
|
|
753
|
+
clearGeneration: generation,
|
|
754
|
+
clearPending,
|
|
755
|
+
renderGeneration,
|
|
756
|
+
transcriptCleared,
|
|
757
|
+
staticEventsLength,
|
|
758
|
+
activeEventsLength,
|
|
759
|
+
uiStateKind,
|
|
760
|
+
before: snapshotFrameState(instance, logShadow),
|
|
761
|
+
});
|
|
762
|
+
return true;
|
|
763
|
+
},
|
|
764
|
+
syncRenderState(state) {
|
|
765
|
+
if (!Number.isFinite(state.generation)) return false;
|
|
766
|
+
renderGeneration = state.generation;
|
|
767
|
+
transcriptCleared = state.transcriptCleared;
|
|
768
|
+
clearGenerationReady = state.clearGenerationReady === true;
|
|
769
|
+
staticEventsLength = state.staticEventsLength;
|
|
770
|
+
activeEventsLength = state.activeEventsLength;
|
|
771
|
+
uiStateKind = state.uiStateKind;
|
|
772
|
+
// Ink writes a frame during the React commit (resetAfterCommit), which runs
|
|
773
|
+
// BEFORE the passive effect that calls this. So the cleared frame's
|
|
774
|
+
// renderInteractiveFrame already ran (and was suppressed) against the stale
|
|
775
|
+
// gate. When the gate is now satisfiable, signal the host to force exactly
|
|
776
|
+
// one more render so the authoritative post-clear frame is flushed.
|
|
777
|
+
const postClearRepaintPending = clearPending && isPostClearFrameReady();
|
|
778
|
+
renderDebug.traceEvent("terminal", "clearBoundaryRenderState", {
|
|
779
|
+
renderGeneration,
|
|
780
|
+
transcriptCleared,
|
|
781
|
+
clearGenerationReady,
|
|
782
|
+
staticEventsLength,
|
|
783
|
+
activeEventsLength,
|
|
784
|
+
uiStateKind,
|
|
785
|
+
clearPending,
|
|
786
|
+
pendingGeneration,
|
|
787
|
+
committedGeneration,
|
|
788
|
+
postClearRepaintPending,
|
|
789
|
+
});
|
|
790
|
+
return postClearRepaintPending;
|
|
791
|
+
},
|
|
792
|
+
getState() {
|
|
793
|
+
return {
|
|
794
|
+
clearPending,
|
|
795
|
+
pendingGeneration,
|
|
796
|
+
committedGeneration,
|
|
797
|
+
renderGeneration,
|
|
798
|
+
transcriptCleared,
|
|
799
|
+
lastFrameWasAuthoritative,
|
|
800
|
+
widthRepaintPending,
|
|
801
|
+
overlayActive,
|
|
802
|
+
logShadow: { ...logShadow },
|
|
803
|
+
};
|
|
804
|
+
},
|
|
805
|
+
dispose() {
|
|
806
|
+
if (overlayActive) {
|
|
807
|
+
setAlternateScreen(false, "dispose");
|
|
808
|
+
overlayActive = false;
|
|
809
|
+
}
|
|
810
|
+
instance.renderInteractiveFrame = originalRenderInteractiveFrame;
|
|
811
|
+
restoreLog();
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
}
|