ubume 0.1.0
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 +88 -0
- package/bin/codexa-local-harness-bridge.js +3 -0
- package/bin/codexa.js +28 -0
- package/bin/ubume-local-harness-bridge.js +325 -0
- package/bin/ubume.js +398 -0
- package/package.json +66 -0
- package/src/app.tsx +5759 -0
- package/src/commands/handler.ts +889 -0
- package/src/config/appVersion.ts +69 -0
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +196 -0
- package/src/config/layeredConfig.ts +853 -0
- package/src/config/legacyEnv.ts +16 -0
- package/src/config/persistence.ts +377 -0
- package/src/config/runtimeConfig.ts +558 -0
- package/src/config/settings.ts +405 -0
- package/src/config/toml-serialize.ts +109 -0
- package/src/config/trustStore.ts +84 -0
- package/src/config/updateCheckCache.ts +85 -0
- package/src/core/README.md +55 -0
- package/src/core/agent/loop.ts +464 -0
- package/src/core/agent/protocol.ts +345 -0
- package/src/core/agent/tools.ts +423 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/codex/codexExecArgs.ts +166 -0
- package/src/core/codex/codexLaunch.ts +163 -0
- package/src/core/codex/codexPrompt.ts +429 -0
- package/src/core/debug/inputDebug.ts +51 -0
- package/src/core/debug/localStreamDebug.ts +50 -0
- package/src/core/debug/modelStateDebug.ts +35 -0
- package/src/core/executables/antigravityExecutable.ts +48 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.ts +160 -0
- package/src/core/executables/executableResolver.ts +164 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/models/codexCapabilities.ts +97 -0
- package/src/core/models/codexModelCapabilities.ts +624 -0
- package/src/core/models/codexModelsCacheSeed.ts +153 -0
- package/src/core/models/modelSpecs.ts +303 -0
- package/src/core/models/providerModelCache.ts +94 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.ts +398 -0
- package/src/core/process/CommandRunner.ts +280 -0
- package/src/core/process/processValidation.ts +111 -0
- package/src/core/providerLauncher/launcher.ts +220 -0
- package/src/core/providerLauncher/registry.ts +354 -0
- package/src/core/providerLauncher/types.ts +95 -0
- package/src/core/providerLauncher/workspaceConfig.ts +487 -0
- package/src/core/providerRuntime/anthropic.ts +580 -0
- package/src/core/providerRuntime/antigravity.ts +500 -0
- package/src/core/providerRuntime/capabilityProfile.ts +383 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
- package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
- package/src/core/providerRuntime/codexaCupy.ts +97 -0
- package/src/core/providerRuntime/codexaNative.ts +425 -0
- package/src/core/providerRuntime/contextMetadata.ts +397 -0
- package/src/core/providerRuntime/gemini.ts +789 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.ts +770 -0
- package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
- package/src/core/providerRuntime/localOutputBudget.ts +17 -0
- package/src/core/providerRuntime/mistralVibe.ts +667 -0
- package/src/core/providerRuntime/models.ts +175 -0
- package/src/core/providerRuntime/reasoning.ts +20 -0
- package/src/core/providerRuntime/registry.ts +286 -0
- package/src/core/providerRuntime/types.ts +142 -0
- package/src/core/providerRuntime/unsloth.ts +216 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.ts +378 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +91 -0
- package/src/core/shared/attachments.ts +101 -0
- package/src/core/shared/cleanupFastFail.ts +67 -0
- package/src/core/shared/clipboard.ts +24 -0
- package/src/core/shared/clipboardImage.ts +111 -0
- package/src/core/shared/githubDiagnostics.ts +222 -0
- package/src/core/shared/hollowResponseFormat.ts +39 -0
- package/src/core/terminal/clearFrameBoundary.ts +852 -0
- package/src/core/terminal/frameLock.ts +110 -0
- package/src/core/terminal/inkRenderReset.ts +123 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.ts +169 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalTitle.ts +400 -0
- package/src/core/version/channel.ts +27 -0
- package/src/core/version/packageManager.ts +119 -0
- package/src/core/version/updateCheck.ts +203 -0
- package/src/core/workspace/appData.ts +107 -0
- package/src/core/workspace/conversationStore.ts +335 -0
- package/src/core/workspace/launchContext.ts +259 -0
- package/src/core/workspace/planStorage.ts +135 -0
- package/src/core/workspace/projectInstructions.ts +54 -0
- package/src/core/workspace/scratchDir.ts +64 -0
- package/src/core/workspace/workspaceActivity.ts +384 -0
- package/src/core/workspace/workspaceGuard.ts +377 -0
- package/src/core/workspace/workspaceRoot.ts +47 -0
- package/src/exec.ts +73 -0
- package/src/headless/execArgs.ts +296 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.tsx +270 -0
- package/src/legacyEnvBootstrap.ts +5 -0
- package/src/session/appSession.ts +771 -0
- package/src/session/chatLifecycle.ts +994 -0
- package/src/session/conversation.ts +107 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/persistedResponse.ts +93 -0
- package/src/session/planFlow.ts +159 -0
- package/src/session/planTranscript.ts +19 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +234 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/chrome/ActivityBars.tsx +68 -0
- package/src/ui/chrome/ActivityIndicator.tsx +58 -0
- package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
- package/src/ui/chrome/AppShell.tsx +474 -0
- package/src/ui/chrome/BottomComposer.tsx +1135 -0
- package/src/ui/chrome/DashCard.tsx +82 -0
- package/src/ui/chrome/RunFooter.tsx +65 -0
- package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
- package/src/ui/chrome/Spinner.tsx +25 -0
- package/src/ui/chrome/TopHeader.tsx +439 -0
- package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
- package/src/ui/chrome/busyStatusAnimation.ts +11 -0
- package/src/ui/input/commandNormalize.ts +66 -0
- package/src/ui/input/focus.ts +73 -0
- package/src/ui/input/imageAttachments.ts +18 -0
- package/src/ui/input/inputBuffer.ts +203 -0
- package/src/ui/input/pastedContent.ts +75 -0
- package/src/ui/input/rawArrowKeys.ts +28 -0
- package/src/ui/input/slashCommands.ts +43 -0
- package/src/ui/input/useStdinRawModeLease.ts +23 -0
- package/src/ui/layout.ts +560 -0
- package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
- package/src/ui/panels/AuthPanel.tsx +149 -0
- package/src/ui/panels/BackendPicker.tsx +28 -0
- package/src/ui/panels/ModePicker.tsx +31 -0
- package/src/ui/panels/ModelPicker.tsx +31 -0
- package/src/ui/panels/ModelPickerScreen.tsx +761 -0
- package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
- package/src/ui/panels/Panel.tsx +51 -0
- package/src/ui/panels/PermissionsPanel.tsx +78 -0
- package/src/ui/panels/PlanActionPicker.tsx +187 -0
- package/src/ui/panels/ProviderPicker.tsx +753 -0
- package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
- package/src/ui/panels/ReasoningPicker.tsx +46 -0
- package/src/ui/panels/ResumePicker.tsx +90 -0
- package/src/ui/panels/SelectionPanel.tsx +138 -0
- package/src/ui/panels/SettingsPanel.tsx +156 -0
- package/src/ui/panels/TextEntryPanel.tsx +139 -0
- package/src/ui/panels/ThemePicker.tsx +32 -0
- package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
- package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
- package/src/ui/panels/responsivePickerViewport.ts +64 -0
- package/src/ui/render/Markdown.tsx +331 -0
- package/src/ui/render/diffRenderer.ts +116 -0
- package/src/ui/render/logoVariants.ts +113 -0
- package/src/ui/render/modeDisplay.ts +52 -0
- package/src/ui/render/outputPipeline.ts +64 -0
- package/src/ui/render/runtimeDisplay.ts +128 -0
- package/src/ui/render/terminalAnswerFormat.ts +128 -0
- package/src/ui/render/textLayout.ts +392 -0
- package/src/ui/theme.tsx +274 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
- package/src/ui/timeline/AgentBlock.tsx +130 -0
- package/src/ui/timeline/StaticIntroItem.tsx +54 -0
- package/src/ui/timeline/ThinkingBlock.tsx +100 -0
- package/src/ui/timeline/Timeline.tsx +1410 -0
- package/src/ui/timeline/TranscriptShell.tsx +302 -0
- package/src/ui/timeline/TurnGroup.tsx +673 -0
- package/src/ui/timeline/layoutListWindow.ts +145 -0
- package/src/ui/timeline/liveViewportWindow.ts +68 -0
- package/src/ui/timeline/progressEntries.ts +156 -0
- package/src/ui/timeline/runActivityView.ts +37 -0
- package/src/ui/timeline/staticTranscriptCache.ts +174 -0
- package/src/ui/timeline/streamCoalesce.ts +53 -0
- package/src/ui/timeline/timelineMeasure.ts +3273 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility for windowing large lists in limited terminal rows.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface ListWindowOptions {
|
|
6
|
+
itemCount: number;
|
|
7
|
+
selectedIndex: number;
|
|
8
|
+
availableRows: number;
|
|
9
|
+
/** Fixed rows consumed by headers, borders, titles, etc. */
|
|
10
|
+
chromeRows: number;
|
|
11
|
+
/** Whether to show "↑ N more" and "↓ N more" indicators. */
|
|
12
|
+
showIndicators?: boolean;
|
|
13
|
+
/** Whether to show "Showing X-Y of N" range line. */
|
|
14
|
+
showRangeLine?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ListWindowResult {
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
visibleCount: number;
|
|
21
|
+
showAbove: boolean;
|
|
22
|
+
showBelow: boolean;
|
|
23
|
+
showRange: boolean;
|
|
24
|
+
hiddenAbove: number;
|
|
25
|
+
hiddenBelow: number;
|
|
26
|
+
/** Total rows consumed including chrome and indicators. */
|
|
27
|
+
totalRows: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Calculates a contiguous window of items that fits within available rows,
|
|
32
|
+
* ensuring the selected item is always visible.
|
|
33
|
+
*/
|
|
34
|
+
export function calculateListWindow({
|
|
35
|
+
itemCount,
|
|
36
|
+
selectedIndex,
|
|
37
|
+
availableRows,
|
|
38
|
+
chromeRows,
|
|
39
|
+
showIndicators = true,
|
|
40
|
+
showRangeLine = false,
|
|
41
|
+
}: ListWindowOptions): ListWindowResult {
|
|
42
|
+
if (itemCount === 0) {
|
|
43
|
+
return {
|
|
44
|
+
start: 0,
|
|
45
|
+
end: 0,
|
|
46
|
+
visibleCount: 0,
|
|
47
|
+
showAbove: false,
|
|
48
|
+
showBelow: false,
|
|
49
|
+
showRange: false,
|
|
50
|
+
hiddenAbove: 0,
|
|
51
|
+
hiddenBelow: 0,
|
|
52
|
+
totalRows: chromeRows,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const rangeReserved = showRangeLine ? 1 : 0;
|
|
57
|
+
|
|
58
|
+
// If everything fits including the range line, just show everything.
|
|
59
|
+
if (itemCount + chromeRows + rangeReserved <= availableRows) {
|
|
60
|
+
return {
|
|
61
|
+
start: 0,
|
|
62
|
+
end: itemCount,
|
|
63
|
+
visibleCount: itemCount,
|
|
64
|
+
showAbove: false,
|
|
65
|
+
showBelow: false,
|
|
66
|
+
showRange: showRangeLine,
|
|
67
|
+
hiddenAbove: 0,
|
|
68
|
+
hiddenBelow: 0,
|
|
69
|
+
totalRows: itemCount + chromeRows + rangeReserved,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Otherwise we must window.
|
|
74
|
+
// We reserve space for indicators if requested.
|
|
75
|
+
// We MUST fit within availableRows.
|
|
76
|
+
const indicatorReserved = showIndicators ? 2 : 0;
|
|
77
|
+
const baseReserved = chromeRows + indicatorReserved + rangeReserved;
|
|
78
|
+
|
|
79
|
+
let itemRows = Math.max(1, availableRows - baseReserved);
|
|
80
|
+
let start = 0;
|
|
81
|
+
let end = 0;
|
|
82
|
+
|
|
83
|
+
// Iteratively refine to account for dynamic indicators.
|
|
84
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
85
|
+
const visibleCount = Math.max(1, Math.min(itemCount, itemRows));
|
|
86
|
+
|
|
87
|
+
// Center selected item if possible
|
|
88
|
+
start = Math.max(0, selectedIndex - Math.floor(visibleCount / 2));
|
|
89
|
+
if (start + visibleCount > itemCount) {
|
|
90
|
+
start = Math.max(0, itemCount - visibleCount);
|
|
91
|
+
}
|
|
92
|
+
end = Math.min(itemCount, start + visibleCount);
|
|
93
|
+
|
|
94
|
+
const actualAbove = start > 0;
|
|
95
|
+
const actualBelow = end < itemCount;
|
|
96
|
+
const dynamicReserved = chromeRows
|
|
97
|
+
+ (showRangeLine ? 1 : 0)
|
|
98
|
+
+ (showIndicators && actualAbove ? 1 : 0)
|
|
99
|
+
+ (showIndicators && actualBelow ? 1 : 0);
|
|
100
|
+
|
|
101
|
+
const nextItemRows = Math.max(1, availableRows - dynamicReserved);
|
|
102
|
+
if (nextItemRows === itemRows) break;
|
|
103
|
+
itemRows = nextItemRows;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Final visibility check for selectedIndex
|
|
107
|
+
if (selectedIndex < start) {
|
|
108
|
+
start = selectedIndex;
|
|
109
|
+
end = Math.min(itemCount, start + itemRows);
|
|
110
|
+
} else if (selectedIndex >= end) {
|
|
111
|
+
end = selectedIndex + 1;
|
|
112
|
+
start = Math.max(0, end - itemRows);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Final clamping to ensure we don't exceed availableRows if indicators are shown
|
|
116
|
+
let finalAbove = showIndicators && start > 0;
|
|
117
|
+
let finalBelow = showIndicators && end < itemCount;
|
|
118
|
+
|
|
119
|
+
while (end - start + chromeRows + (showRangeLine ? 1 : 0) + (finalAbove ? 1 : 0) + (finalBelow ? 1 : 0) > availableRows && end - start > 1) {
|
|
120
|
+
// Shrink window from the side further from selectedIndex
|
|
121
|
+
if (selectedIndex - start > end - 1 - selectedIndex) {
|
|
122
|
+
start++;
|
|
123
|
+
} else {
|
|
124
|
+
end--;
|
|
125
|
+
}
|
|
126
|
+
finalAbove = showIndicators && start > 0;
|
|
127
|
+
finalBelow = showIndicators && end < itemCount;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
start,
|
|
132
|
+
end,
|
|
133
|
+
visibleCount: end - start,
|
|
134
|
+
showAbove: finalAbove,
|
|
135
|
+
showBelow: finalBelow,
|
|
136
|
+
showRange: showRangeLine,
|
|
137
|
+
hiddenAbove: start,
|
|
138
|
+
hiddenBelow: itemCount - end,
|
|
139
|
+
totalRows: (end - start)
|
|
140
|
+
+ chromeRows
|
|
141
|
+
+ (showRangeLine ? 1 : 0)
|
|
142
|
+
+ (finalAbove ? 1 : 0)
|
|
143
|
+
+ (finalBelow ? 1 : 0),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { TimelineRow } from "./timelineMeasure.js";
|
|
2
|
+
import { buildFrameElisionRow } from "./timelineMeasure.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Rows kept free below the live window to absorb composer-measurement drift.
|
|
6
|
+
* Ink erases the scrollback (`\x1b[2J\x1b[3J\x1b[H`) on every frame whose live
|
|
7
|
+
* output is taller than the terminal, which drops the user to the bottom while
|
|
8
|
+
* they are reading scrollback. The window must therefore stay strictly inside
|
|
9
|
+
* the viewport even when the composer is one row taller than measured.
|
|
10
|
+
*/
|
|
11
|
+
export const LIVE_WINDOW_SAFETY_ROWS = 1;
|
|
12
|
+
|
|
13
|
+
/** Rows a re-capped card costs: its own top border plus the elision notice. */
|
|
14
|
+
const FRAME_RECAP_ROWS = 2;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Walk back from `start` to the top row of the card `start` sits inside.
|
|
18
|
+
* Returns null when the slice does not cut a card open — either the row carries
|
|
19
|
+
* no frame at all, or it already is a card's top row.
|
|
20
|
+
*/
|
|
21
|
+
function findOpenFrameTop(
|
|
22
|
+
rows: TimelineRow[],
|
|
23
|
+
start: number,
|
|
24
|
+
): { topRow: TimelineRow; topIndex: number } | null {
|
|
25
|
+
const frame = rows[start]?.frame;
|
|
26
|
+
if (!frame || frame.role === "top") return null;
|
|
27
|
+
|
|
28
|
+
for (let index = start - 1; index >= 0; index -= 1) {
|
|
29
|
+
const candidate = rows[index];
|
|
30
|
+
const candidateFrame = candidate?.frame;
|
|
31
|
+
if (!candidateFrame || candidateFrame.id !== frame.id) return null;
|
|
32
|
+
if (candidateFrame.role === "top") return { topRow: candidate!, topIndex: index };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Keep only the last `maxRows` live rows. Returns the input array itself when
|
|
40
|
+
* it already fits so memoized consumers keep their identity.
|
|
41
|
+
*
|
|
42
|
+
* The window may only ever shrink — growing past `maxRows` is what makes Ink
|
|
43
|
+
* wipe the scrollback. So when the cut lands inside a bordered card, the card is
|
|
44
|
+
* re-capped rather than extended: two rows of tail are traded for the card's own
|
|
45
|
+
* top border plus an `⋯ N rows hidden` notice, which keeps the box complete
|
|
46
|
+
* instead of leaving a headless frame that starts mid-sentence.
|
|
47
|
+
*/
|
|
48
|
+
export function windowLiveRows(rows: TimelineRow[], maxRows: number): TimelineRow[] {
|
|
49
|
+
const limit = Math.max(0, Math.floor(maxRows));
|
|
50
|
+
if (rows.length <= limit) return rows;
|
|
51
|
+
if (limit === 0) return [];
|
|
52
|
+
|
|
53
|
+
const plainStart = rows.length - limit;
|
|
54
|
+
if (limit <= FRAME_RECAP_ROWS + 1 || !findOpenFrameTop(rows, plainStart)) {
|
|
55
|
+
return rows.slice(plainStart);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const recapStart = rows.length - (limit - FRAME_RECAP_ROWS);
|
|
59
|
+
const frame = findOpenFrameTop(rows, recapStart);
|
|
60
|
+
if (!frame) return rows.slice(plainStart);
|
|
61
|
+
|
|
62
|
+
const hiddenRows = recapStart - frame.topIndex - 1;
|
|
63
|
+
return [
|
|
64
|
+
frame.topRow,
|
|
65
|
+
buildFrameElisionRow(frame.topRow, hiddenRows),
|
|
66
|
+
...rows.slice(recapStart),
|
|
67
|
+
];
|
|
68
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import type { RunProgressBlock, RunProgressEntry, RunProgressSource } from "../../session/types.js";
|
|
2
|
+
import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
|
|
3
|
+
import { wrapPlainText } from "../render/textLayout.js";
|
|
4
|
+
|
|
5
|
+
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export interface VisibleProgressBlock {
|
|
8
|
+
id: string;
|
|
9
|
+
key: string;
|
|
10
|
+
label: string;
|
|
11
|
+
headline: string;
|
|
12
|
+
text: string;
|
|
13
|
+
source: RunProgressSource;
|
|
14
|
+
entryId: string;
|
|
15
|
+
entrySequence: number;
|
|
16
|
+
blockSequence: number;
|
|
17
|
+
status: RunProgressBlock["status"];
|
|
18
|
+
isLatest: boolean;
|
|
19
|
+
isActive: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface VisibleProgressBlocks {
|
|
23
|
+
hiddenCount: number;
|
|
24
|
+
totalCount: number;
|
|
25
|
+
blocks: VisibleProgressBlock[];
|
|
26
|
+
latestBlock: VisibleProgressBlock | null;
|
|
27
|
+
latestActiveBlock: VisibleProgressBlock | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Block selection ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function sourceLabel(source: RunProgressSource): string {
|
|
33
|
+
switch (source) {
|
|
34
|
+
case "reasoning":
|
|
35
|
+
return "Reasoning";
|
|
36
|
+
case "todo":
|
|
37
|
+
return "Todo";
|
|
38
|
+
case "tool":
|
|
39
|
+
return "Tool";
|
|
40
|
+
case "activity":
|
|
41
|
+
return "Activity";
|
|
42
|
+
case "stderr":
|
|
43
|
+
return "Process";
|
|
44
|
+
case "transcript":
|
|
45
|
+
return "Progress";
|
|
46
|
+
default:
|
|
47
|
+
return "Update";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function firstMeaningfulLine(text: string): string {
|
|
52
|
+
return sanitizeTerminalOutput(text)
|
|
53
|
+
.replace(/\r\n/g, "\n")
|
|
54
|
+
.replace(/\r/g, "\n")
|
|
55
|
+
.split("\n")
|
|
56
|
+
.map((line) => line.trim())
|
|
57
|
+
.find(Boolean) ?? "";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildProgressHeadline(source: RunProgressSource, text: string, status: RunProgressBlock["status"]): string {
|
|
61
|
+
const label = status === "active" ? "Current" : sourceLabel(source);
|
|
62
|
+
const firstLine = firstMeaningfulLine(text);
|
|
63
|
+
return firstLine ? `${label}: ${firstLine}` : label;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toVisibleProgressBlocks(entries: RunProgressEntry[]): VisibleProgressBlock[] {
|
|
67
|
+
const blocks: VisibleProgressBlock[] = [];
|
|
68
|
+
let visibleSequence = 0;
|
|
69
|
+
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
for (const block of entry.blocks) {
|
|
72
|
+
if (!block.text.trim()) continue;
|
|
73
|
+
visibleSequence += 1;
|
|
74
|
+
blocks.push({
|
|
75
|
+
id: block.id,
|
|
76
|
+
key: block.id,
|
|
77
|
+
label: `Update ${visibleSequence}`,
|
|
78
|
+
headline: buildProgressHeadline(entry.source, block.text, block.status),
|
|
79
|
+
text: block.text,
|
|
80
|
+
source: entry.source,
|
|
81
|
+
entryId: entry.id,
|
|
82
|
+
entrySequence: entry.sequence,
|
|
83
|
+
blockSequence: block.sequence,
|
|
84
|
+
status: block.status,
|
|
85
|
+
isLatest: false,
|
|
86
|
+
isActive: block.status === "active",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return blocks.map((block, index) => ({
|
|
92
|
+
...block,
|
|
93
|
+
isLatest: index === blocks.length - 1,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getProgressUpdateCount(entries: RunProgressEntry[]): number {
|
|
98
|
+
return toVisibleProgressBlocks(entries).length;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function selectVisibleProgressBlocks(entries: RunProgressEntry[], maxVisible: number): VisibleProgressBlocks {
|
|
102
|
+
const blocks = toVisibleProgressBlocks(entries);
|
|
103
|
+
const safeMax = Math.max(0, maxVisible);
|
|
104
|
+
const findLatestActiveBlock = (candidates: VisibleProgressBlock[]): VisibleProgressBlock | null => {
|
|
105
|
+
for (let index = candidates.length - 1; index >= 0; index -= 1) {
|
|
106
|
+
if (candidates[index]?.isActive) {
|
|
107
|
+
return candidates[index]!;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (blocks.length <= safeMax) {
|
|
114
|
+
return {
|
|
115
|
+
hiddenCount: 0,
|
|
116
|
+
totalCount: blocks.length,
|
|
117
|
+
blocks,
|
|
118
|
+
latestBlock: blocks[blocks.length - 1] ?? null,
|
|
119
|
+
latestActiveBlock: findLatestActiveBlock(blocks),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const visible = blocks.slice(-safeMax);
|
|
124
|
+
return {
|
|
125
|
+
hiddenCount: blocks.length - safeMax,
|
|
126
|
+
totalCount: blocks.length,
|
|
127
|
+
blocks: visible,
|
|
128
|
+
latestBlock: visible[visible.length - 1] ?? null,
|
|
129
|
+
latestActiveBlock: findLatestActiveBlock(visible),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function formatProgressBlockBodyLines(text: string, width: number): string[] {
|
|
134
|
+
const contentWidth = Math.max(1, width);
|
|
135
|
+
const normalized = sanitizeTerminalOutput(text)
|
|
136
|
+
.replace(/\r\n/g, "\n")
|
|
137
|
+
.replace(/\r/g, "\n")
|
|
138
|
+
.replace(/[ \t]+\n/g, "\n");
|
|
139
|
+
|
|
140
|
+
if (!normalized.trim()) {
|
|
141
|
+
return [" "];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const rows: string[] = [];
|
|
145
|
+
for (const rawLine of normalized.split("\n")) {
|
|
146
|
+
if (!rawLine.trim()) {
|
|
147
|
+
rows.push("");
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const wrapped = wrapPlainText(rawLine, contentWidth);
|
|
152
|
+
rows.push(...(wrapped.length > 0 ? wrapped : [" "]));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return rows.length > 0 ? rows : [" "];
|
|
156
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { RunEvent } from "../../session/types.js";
|
|
2
|
+
|
|
3
|
+
const MAX_VISIBLE_ACTIVE_ACTIVITY = 6;
|
|
4
|
+
const MAX_VISIBLE_SUMMARY_ACTIVITY = 4;
|
|
5
|
+
export function selectVisibleRunActivity(event: RunEvent) {
|
|
6
|
+
const source = event.status === "running"
|
|
7
|
+
? event.activity
|
|
8
|
+
: event.activitySummary?.recent ?? event.activity;
|
|
9
|
+
const limit = event.status === "running" ? MAX_VISIBLE_ACTIVE_ACTIVITY : MAX_VISIBLE_SUMMARY_ACTIVITY;
|
|
10
|
+
const visible = source.slice(-limit);
|
|
11
|
+
return {
|
|
12
|
+
visible,
|
|
13
|
+
hiddenCount: Math.max(0, source.length - visible.length),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shouldShowRawOutputFallback(event: RunEvent): boolean {
|
|
18
|
+
void event;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getVisibleRawOutputLines(event: RunEvent): string[] {
|
|
23
|
+
void event;
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatRunActivityStats(event: RunEvent): string | null {
|
|
28
|
+
const summary = event.activitySummary;
|
|
29
|
+
if (!summary || event.touchedFileCount === 0) return null;
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
`${event.touchedFileCount} file${event.touchedFileCount === 1 ? "" : "s"} touched`,
|
|
33
|
+
`+${summary.created}`,
|
|
34
|
+
`~${summary.modified}`,
|
|
35
|
+
`-${summary.deleted}`,
|
|
36
|
+
].join(" ");
|
|
37
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { AssistantEvent, ErrorEvent, RunEvent, ShellEvent, SystemEvent, UserPromptEvent } from "../../session/types.js";
|
|
2
|
+
import type { RenderTimelineItem } from "./Timeline.js";
|
|
3
|
+
import type { TurnOpacity } from "./TurnGroup.js";
|
|
4
|
+
import { buildNativeTranscriptParts, type NativeTranscriptRowItem, type TimelineRow } from "./timelineMeasure.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Incremental row builder for the finalized (static) half of the transcript.
|
|
8
|
+
*
|
|
9
|
+
* Ink's <Static> prints each item once and never re-renders it, and finalized
|
|
10
|
+
* timeline events are immutable, so rebuilding every past turn's rows on each
|
|
11
|
+
* React commit (every keystroke, every streaming flush) is pure waste that
|
|
12
|
+
* grows with session length. This cache keys rows by the render item's key
|
|
13
|
+
* and revalidates by event identity plus the derived opacity, so a commit
|
|
14
|
+
* that changes nothing in the static half costs one Map lookup per item.
|
|
15
|
+
*
|
|
16
|
+
* Retention: only the newest MAX_RETAINED_STATIC_TURNS turns keep row objects.
|
|
17
|
+
* Older turns still emit placeholder items (with no rows) so the <Static> item
|
|
18
|
+
* count stays monotonic — Ink advances its printed index by items.length, so a
|
|
19
|
+
* shrinking list would swallow the next newly finalized turn. A turn that
|
|
20
|
+
* leaves the window keeps exactly the item keys it had while retained; those
|
|
21
|
+
* keys survive generation changes so the count never drops. Their rows are
|
|
22
|
+
* already in the terminal's scrollback; the only visible effect is that a
|
|
23
|
+
* width resize (which re-flushes <Static> from these items) redraws just the
|
|
24
|
+
* retained window.
|
|
25
|
+
*/
|
|
26
|
+
export const MAX_RETAINED_STATIC_TURNS = 200;
|
|
27
|
+
|
|
28
|
+
const EMPTY_ROWS: TimelineRow[] = Object.freeze([]) as unknown as TimelineRow[];
|
|
29
|
+
|
|
30
|
+
interface CachedTurnEntry {
|
|
31
|
+
kind: "turn";
|
|
32
|
+
user: UserPromptEvent | null;
|
|
33
|
+
run: RunEvent | null;
|
|
34
|
+
assistant: AssistantEvent | null;
|
|
35
|
+
opacity: TurnOpacity;
|
|
36
|
+
items: NativeTranscriptRowItem[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CachedEventEntry {
|
|
40
|
+
kind: "event";
|
|
41
|
+
event: SystemEvent | ErrorEvent | ShellEvent;
|
|
42
|
+
items: NativeTranscriptRowItem[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
type CachedEntry = CachedTurnEntry | CachedEventEntry;
|
|
46
|
+
|
|
47
|
+
export interface StaticTranscriptBuildOptions {
|
|
48
|
+
totalWidth: number;
|
|
49
|
+
verboseMode: boolean;
|
|
50
|
+
workspaceRoot: string | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface StaticTranscriptCache {
|
|
54
|
+
generation: string;
|
|
55
|
+
entries: Map<string, CachedEntry>;
|
|
56
|
+
/** Item keys of turns outside the retention window; kept across generations. */
|
|
57
|
+
placeholders: Map<string, string[]>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createStaticTranscriptCache(): StaticTranscriptCache {
|
|
61
|
+
return { generation: "", entries: new Map(), placeholders: new Map() };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function staticTranscriptGeneration(options: StaticTranscriptBuildOptions): string {
|
|
65
|
+
return `${options.totalWidth}|${options.verboseMode ? 1 : 0}|${options.workspaceRoot ?? ""}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isCacheHit(cached: CachedEntry, item: RenderTimelineItem): boolean {
|
|
69
|
+
if (item.type === "event") return cached.kind === "event" && cached.event === item.event;
|
|
70
|
+
if (item.type !== "turn" || cached.kind !== "turn") return false;
|
|
71
|
+
return cached.user === item.item.user
|
|
72
|
+
&& cached.run === item.item.run
|
|
73
|
+
&& cached.assistant === item.item.assistant
|
|
74
|
+
&& cached.opacity === item.renderState.opacity;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isCacheable(item: RenderTimelineItem, liveRowCount: number): boolean {
|
|
78
|
+
if (item.type === "event") return true;
|
|
79
|
+
if (item.type !== "turn") return false;
|
|
80
|
+
return item.item.run?.status !== "running" && item.renderState.question === null && liveRowCount === 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build native row items for the given render items, reusing cached rows for
|
|
85
|
+
* every item whose inputs are unchanged. Output is identical to a single
|
|
86
|
+
* batched buildNativeTranscriptParts call over the same items, except that
|
|
87
|
+
* turns outside the retention window carry empty rows.
|
|
88
|
+
*/
|
|
89
|
+
export function buildStaticTranscript(
|
|
90
|
+
cache: StaticTranscriptCache,
|
|
91
|
+
renderItems: RenderTimelineItem[],
|
|
92
|
+
options: StaticTranscriptBuildOptions,
|
|
93
|
+
): { staticItems: NativeTranscriptRowItem[]; liveRows: TimelineRow[] } {
|
|
94
|
+
const generation = staticTranscriptGeneration(options);
|
|
95
|
+
if (cache.generation !== generation) {
|
|
96
|
+
cache.generation = generation;
|
|
97
|
+
cache.entries.clear();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const buildOptions = {
|
|
101
|
+
totalWidth: options.totalWidth,
|
|
102
|
+
verboseMode: options.verboseMode,
|
|
103
|
+
workspaceRoot: options.workspaceRoot,
|
|
104
|
+
debugLabel: "transcript-shell-static",
|
|
105
|
+
};
|
|
106
|
+
const staticItems: NativeTranscriptRowItem[] = [];
|
|
107
|
+
const liveRows: TimelineRow[] = [];
|
|
108
|
+
const seen = new Set<string>();
|
|
109
|
+
|
|
110
|
+
let turnCount = 0;
|
|
111
|
+
for (const item of renderItems) if (item.type === "turn") turnCount += 1;
|
|
112
|
+
const firstRetainedTurnOrdinal = Math.max(0, turnCount - MAX_RETAINED_STATIC_TURNS);
|
|
113
|
+
let turnOrdinal = 0;
|
|
114
|
+
|
|
115
|
+
for (const item of renderItems) {
|
|
116
|
+
if (item.type === "intro") {
|
|
117
|
+
staticItems.push(...buildNativeTranscriptParts([item], buildOptions).staticItems);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (item.type === "turn") {
|
|
122
|
+
const ordinal = turnOrdinal;
|
|
123
|
+
turnOrdinal += 1;
|
|
124
|
+
if (ordinal < firstRetainedTurnOrdinal) {
|
|
125
|
+
let keys = cache.placeholders.get(item.key);
|
|
126
|
+
if (!keys) {
|
|
127
|
+
// Leaving the window: keep this turn's item keys so the count stays
|
|
128
|
+
// exactly what Ink already printed. A turn first seen outside the
|
|
129
|
+
// window was never printed by this <Static>, so one item suffices.
|
|
130
|
+
keys = cache.entries.get(item.key)?.items.map((entry) => entry.key) ?? [item.key];
|
|
131
|
+
cache.placeholders.set(item.key, keys);
|
|
132
|
+
cache.entries.delete(item.key);
|
|
133
|
+
}
|
|
134
|
+
for (const key of keys) staticItems.push({ key, rows: EMPTY_ROWS });
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const cached = cache.entries.get(item.key);
|
|
140
|
+
if (cached && isCacheHit(cached, item)) {
|
|
141
|
+
staticItems.push(...cached.items);
|
|
142
|
+
seen.add(item.key);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const parts = buildNativeTranscriptParts([item], buildOptions);
|
|
147
|
+
staticItems.push(...parts.staticItems);
|
|
148
|
+
liveRows.push(...parts.liveRows);
|
|
149
|
+
if (!isCacheable(item, parts.liveRows.length)) continue;
|
|
150
|
+
|
|
151
|
+
cache.entries.set(
|
|
152
|
+
item.key,
|
|
153
|
+
item.type === "event"
|
|
154
|
+
? { kind: "event", event: item.event, items: parts.staticItems }
|
|
155
|
+
: {
|
|
156
|
+
kind: "turn",
|
|
157
|
+
user: item.item.user,
|
|
158
|
+
run: item.item.run,
|
|
159
|
+
assistant: item.item.assistant,
|
|
160
|
+
opacity: item.renderState.opacity,
|
|
161
|
+
items: parts.staticItems,
|
|
162
|
+
},
|
|
163
|
+
);
|
|
164
|
+
seen.add(item.key);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (cache.entries.size > seen.size) {
|
|
168
|
+
for (const key of cache.entries.keys()) {
|
|
169
|
+
if (!seen.has(key)) cache.entries.delete(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { staticItems, liveRows };
|
|
174
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RunProgressBlock } from "../../session/types.js";
|
|
2
|
+
|
|
3
|
+
interface ThinkingLikeEvent {
|
|
4
|
+
kind: string;
|
|
5
|
+
streamSeq: number;
|
|
6
|
+
block: RunProgressBlock;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Merge each run of consecutive `kind === "thinking"` events into a single
|
|
11
|
+
* event so contiguous chain-of-thought renders under one header. Local models
|
|
12
|
+
* emit every reasoning paragraph as its own progress item, which otherwise
|
|
13
|
+
* stacks one labeled block per sentence. Thoughts separated by a tool call or
|
|
14
|
+
* response segment stay separate — only uninterrupted runs merge.
|
|
15
|
+
*/
|
|
16
|
+
export function coalesceConsecutiveThinking<T extends { kind: string; streamSeq: number }>(events: T[]): T[] {
|
|
17
|
+
const result: T[] = [];
|
|
18
|
+
let index = 0;
|
|
19
|
+
while (index < events.length) {
|
|
20
|
+
const event = events[index]!;
|
|
21
|
+
if (event.kind !== "thinking") {
|
|
22
|
+
result.push(event);
|
|
23
|
+
index += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let end = index + 1;
|
|
28
|
+
while (end < events.length && events[end]!.kind === "thinking") {
|
|
29
|
+
end += 1;
|
|
30
|
+
}
|
|
31
|
+
if (end - index === 1) {
|
|
32
|
+
result.push(event);
|
|
33
|
+
index = end;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const members = events.slice(index, end) as unknown as ThinkingLikeEvent[];
|
|
38
|
+
const first = members[0]!;
|
|
39
|
+
const last = members[members.length - 1]!;
|
|
40
|
+
const mergedBlock: RunProgressBlock = {
|
|
41
|
+
...first.block,
|
|
42
|
+
text: members
|
|
43
|
+
.map((member) => member.block.text)
|
|
44
|
+
.filter((text) => text.trim().length > 0)
|
|
45
|
+
.join("\n\n"),
|
|
46
|
+
updatedAt: Math.max(...members.map((member) => member.block.updatedAt)),
|
|
47
|
+
status: last.block.status,
|
|
48
|
+
};
|
|
49
|
+
result.push({ ...event, block: mergedBlock });
|
|
50
|
+
index = end;
|
|
51
|
+
}
|
|
52
|
+
return result;
|
|
53
|
+
}
|