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
package/src/ui/layout.ts
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsive layout constants and hook.
|
|
3
|
+
*
|
|
4
|
+
* Five modes based on both terminal columns and rows:
|
|
5
|
+
*
|
|
6
|
+
* micro very small rows/cols → one-line shell
|
|
7
|
+
* compact normal short terminal → compact shell, no large logo
|
|
8
|
+
* normal standard terminal → compact header, roomy content
|
|
9
|
+
* wide large terminal → decorative header allowed
|
|
10
|
+
* max maximized terminal → full decorative layout
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
14
|
+
import { useStdout } from "ink";
|
|
15
|
+
import stringWidth from "string-width";
|
|
16
|
+
import * as renderDebug from "../core/perf/renderDebug.js";
|
|
17
|
+
import { setTerminalResizing, isTerminalResizing } from "../core/terminal/terminalControl.js";
|
|
18
|
+
|
|
19
|
+
export const BREAKPOINT_MAX = 180;
|
|
20
|
+
export const BREAKPOINT_WIDE = 140;
|
|
21
|
+
export const BREAKPOINT_NORMAL = 90;
|
|
22
|
+
export const BREAKPOINT_COMPACT = 60;
|
|
23
|
+
export const ROW_BREAKPOINT_MAX = 40;
|
|
24
|
+
export const ROW_BREAKPOINT_WIDE = 30;
|
|
25
|
+
export const ROW_BREAKPOINT_NORMAL = 20;
|
|
26
|
+
export const ROW_BREAKPOINT_COMPACT = 14;
|
|
27
|
+
export const MIN_TERMINAL_COLS = 20;
|
|
28
|
+
export const MIN_TERMINAL_ROWS = 10;
|
|
29
|
+
export const MIN_VIEWPORT_COLS = 20;
|
|
30
|
+
export const MIN_VIEWPORT_ROWS = 10;
|
|
31
|
+
export const RESTORE_SETTLE_MS = process.env.NODE_ENV === "test" ? 0 : 100;
|
|
32
|
+
export const STARTUP_TINY_MIN_COLS = 40;
|
|
33
|
+
export const STARTUP_TINY_MIN_ROWS = 14;
|
|
34
|
+
export const STARTUP_FULL_MIN_COLS = 100; // matches LOGO_LARGE_MIN_COLS in logoVariants.ts
|
|
35
|
+
export const STARTUP_FULL_MIN_BODY_ROWS = 4;
|
|
36
|
+
export const STARTUP_FULL_SAFE_PADDING_ROWS = 1;
|
|
37
|
+
export const STARTUP_COMPACT_INTRO_ROWS = 4;
|
|
38
|
+
export const STARTUP_TINY_MESSAGE_ROWS = 3;
|
|
39
|
+
export const transcriptContentIndent = 4; // 2 for DashCard border + 2 for prompt prefix
|
|
40
|
+
const DEFAULT_COLUMNS = 120;
|
|
41
|
+
const DEFAULT_ROWS = 24;
|
|
42
|
+
|
|
43
|
+
export type LayoutMode = "compact" | "regular" | "expanded";
|
|
44
|
+
export type StartupHeaderMode = "large" | "compact" | "tiny";
|
|
45
|
+
|
|
46
|
+
export interface Layout {
|
|
47
|
+
cols: number;
|
|
48
|
+
rows: number;
|
|
49
|
+
mode: LayoutMode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type PanelLayout = {
|
|
53
|
+
mode: "compact" | "regular" | "expanded";
|
|
54
|
+
availableRows: number;
|
|
55
|
+
availableCols: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type BottomChromeBudget = {
|
|
59
|
+
runtimeMetadataRows: number;
|
|
60
|
+
composerRows: number;
|
|
61
|
+
transientStatusRows: number;
|
|
62
|
+
bottomPaddingRows: number;
|
|
63
|
+
totalRows: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type AppLayoutBudget = {
|
|
67
|
+
mode: LayoutMode;
|
|
68
|
+
rows: number;
|
|
69
|
+
cols: number;
|
|
70
|
+
|
|
71
|
+
headerRows: number;
|
|
72
|
+
headerGapRows: number;
|
|
73
|
+
panelStagePaddingY: number;
|
|
74
|
+
|
|
75
|
+
activePanelRows: number;
|
|
76
|
+
activePanelCols: number;
|
|
77
|
+
|
|
78
|
+
bottomChromeBudget: BottomChromeBudget;
|
|
79
|
+
composerRows: number;
|
|
80
|
+
|
|
81
|
+
showNormalLogo: boolean;
|
|
82
|
+
showCompactHeader: boolean;
|
|
83
|
+
placeMetadataBesideLogo: boolean;
|
|
84
|
+
placeMetadataBelowLogo: boolean;
|
|
85
|
+
|
|
86
|
+
// Backward compatibility fields:
|
|
87
|
+
transcriptRows: number;
|
|
88
|
+
panelRows: number;
|
|
89
|
+
showLargeLogo: boolean;
|
|
90
|
+
showPanelSeparators: boolean;
|
|
91
|
+
showPanelColumnHeaders: boolean;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const PanelAvailableRowsContext = createContext<number | undefined>(undefined);
|
|
95
|
+
export const AppLayoutBudgetContext = createContext<AppLayoutBudget | undefined>(undefined);
|
|
96
|
+
export const PanelLayoutContext = createContext<PanelLayout | undefined>(undefined);
|
|
97
|
+
|
|
98
|
+
export interface ActivePanelLayout {
|
|
99
|
+
width: number;
|
|
100
|
+
height: number;
|
|
101
|
+
availableRows: number;
|
|
102
|
+
availableCols: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const ActivePanelLayoutContext = createContext<ActivePanelLayout | undefined>(undefined);
|
|
106
|
+
|
|
107
|
+
export function usePanelAvailableRows(): number | undefined {
|
|
108
|
+
return useContext(PanelAvailableRowsContext);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function useAppLayoutBudget(): AppLayoutBudget | undefined {
|
|
112
|
+
return useContext(AppLayoutBudgetContext);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function useActivePanelLayout(): ActivePanelLayout | undefined {
|
|
116
|
+
return useContext(ActivePanelLayoutContext);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function usePanelLayout(): PanelLayout | undefined {
|
|
120
|
+
return useContext(PanelLayoutContext);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface TerminalViewport extends Layout {
|
|
124
|
+
rawCols?: number;
|
|
125
|
+
rawRows?: number;
|
|
126
|
+
contentWidth: number;
|
|
127
|
+
isCramped: boolean;
|
|
128
|
+
unstable: boolean;
|
|
129
|
+
layoutEpoch: number;
|
|
130
|
+
isResizing: boolean;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Dimension helpers ────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
function isValidDimension(value: number | undefined): value is number {
|
|
136
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function normalizeDimension(value: number | undefined, fallback: number): number {
|
|
140
|
+
if (!isValidDimension(value)) {
|
|
141
|
+
// If even the fallback is invalid, use an absolute floor.
|
|
142
|
+
return isValidDimension(fallback) ? Math.floor(fallback) : 10;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return Math.floor(value);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function isRenderableViewport(cols: number | undefined, rows: number | undefined): boolean {
|
|
149
|
+
return isValidDimension(cols)
|
|
150
|
+
&& isValidDimension(rows)
|
|
151
|
+
&& Math.floor(cols) >= MIN_VIEWPORT_COLS
|
|
152
|
+
&& Math.floor(rows) >= MIN_VIEWPORT_ROWS;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Returns true if the terminal is below the minimum supported size for a full UI. */
|
|
156
|
+
export function isCrampedTerminal(cols: number | undefined, rows: number | undefined): boolean {
|
|
157
|
+
const safeCols = normalizeDimension(cols, DEFAULT_COLUMNS);
|
|
158
|
+
const safeRows = normalizeDimension(rows, DEFAULT_ROWS);
|
|
159
|
+
return safeCols < MIN_TERMINAL_COLS || safeRows < MIN_TERMINAL_ROWS;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Leave a 1-column gutter so box-drawing borders never land exactly on the
|
|
164
|
+
* terminal edge, which can trigger a horizontal scrollbar in some Windows hosts.
|
|
165
|
+
*/
|
|
166
|
+
export function getShellWidth(cols: number | undefined): number {
|
|
167
|
+
return Math.max(20, (cols ?? 120) - 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns the responsive width of the main content area. Large/maximized
|
|
172
|
+
* terminals retain a small gutter but continue growing with the viewport.
|
|
173
|
+
*/
|
|
174
|
+
export function getContentWidth(cols: number | undefined): number {
|
|
175
|
+
const shellWidth = getShellWidth(cols);
|
|
176
|
+
|
|
177
|
+
if (shellWidth < 100) return shellWidth;
|
|
178
|
+
if (shellWidth < 150) return shellWidth - 4;
|
|
179
|
+
if (shellWidth < 200) return shellWidth - 8;
|
|
180
|
+
|
|
181
|
+
return shellWidth - 12;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function getUsableShellWidth(cols: number | undefined, reservedColumns = 0): number {
|
|
185
|
+
return Math.max(1, getShellWidth(cols) - reservedColumns);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Leave a 1-row gutter so renders do not land exactly on the terminal bottom
|
|
190
|
+
* edge, which can trigger viewport scroll drift in some hosts.
|
|
191
|
+
*/
|
|
192
|
+
export function getShellHeight(rows: number | undefined): number {
|
|
193
|
+
return Math.max(10, (rows ?? DEFAULT_ROWS) - 1);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface StartupHeaderModeParams {
|
|
197
|
+
cols: number | undefined;
|
|
198
|
+
rows: number | undefined;
|
|
199
|
+
introRows: number;
|
|
200
|
+
composerRows: number;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function resolveStartupHeaderMode({
|
|
204
|
+
cols,
|
|
205
|
+
rows,
|
|
206
|
+
introRows,
|
|
207
|
+
composerRows,
|
|
208
|
+
}: StartupHeaderModeParams): StartupHeaderMode {
|
|
209
|
+
const safeCols = normalizeDimension(cols, DEFAULT_COLUMNS);
|
|
210
|
+
const safeRows = normalizeDimension(rows, DEFAULT_ROWS);
|
|
211
|
+
|
|
212
|
+
if (safeCols < STARTUP_TINY_MIN_COLS || safeRows < STARTUP_TINY_MIN_ROWS) {
|
|
213
|
+
return "tiny";
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const shellHeight = getShellHeight(safeRows);
|
|
217
|
+
const fullStartupRows = introRows
|
|
218
|
+
+ composerRows
|
|
219
|
+
+ STARTUP_FULL_MIN_BODY_ROWS
|
|
220
|
+
+ STARTUP_FULL_SAFE_PADDING_ROWS;
|
|
221
|
+
|
|
222
|
+
if (safeCols >= STARTUP_FULL_MIN_COLS && shellHeight >= fullStartupRows) {
|
|
223
|
+
return "large";
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return "compact";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function getVisualWidth(text: string): number {
|
|
230
|
+
return stringWidth(text);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function clampVisualText(text: string, maxWidth: number): string {
|
|
234
|
+
if (maxWidth <= 0) return "";
|
|
235
|
+
if (getVisualWidth(text) <= maxWidth) return text;
|
|
236
|
+
|
|
237
|
+
const ellipsis = maxWidth > 1 ? "…" : "";
|
|
238
|
+
const suffixWidth = getVisualWidth(ellipsis);
|
|
239
|
+
let output = "";
|
|
240
|
+
|
|
241
|
+
for (const char of Array.from(text)) {
|
|
242
|
+
if (getVisualWidth(output + char) + suffixWidth > maxWidth) {
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
output += char;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return output + ellipsis;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function computeMode(cols: number, rows: number): LayoutMode {
|
|
252
|
+
if (rows <= 24 || cols <= 100) {
|
|
253
|
+
return "compact";
|
|
254
|
+
}
|
|
255
|
+
if (cols >= 140 && rows >= 30) {
|
|
256
|
+
return "expanded";
|
|
257
|
+
}
|
|
258
|
+
return "regular";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function isDecorativeLayoutMode(mode: LayoutMode): boolean {
|
|
262
|
+
return mode === "expanded";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function isCompactShellMode(mode: LayoutMode): boolean {
|
|
266
|
+
return mode === "compact" || mode === "regular";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface AppLayoutBudgetParams {
|
|
270
|
+
cols: number | undefined;
|
|
271
|
+
rows: number | undefined;
|
|
272
|
+
composerRows?: number;
|
|
273
|
+
panelHintRows?: number;
|
|
274
|
+
headerRows?: number;
|
|
275
|
+
headerGapRows?: number;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function computeAppLayoutBudget({
|
|
279
|
+
cols,
|
|
280
|
+
rows,
|
|
281
|
+
composerRows = 4,
|
|
282
|
+
panelHintRows = 0,
|
|
283
|
+
headerRows,
|
|
284
|
+
headerGapRows,
|
|
285
|
+
}: AppLayoutBudgetParams): AppLayoutBudget {
|
|
286
|
+
const safeCols = normalizeDimension(cols, DEFAULT_COLUMNS);
|
|
287
|
+
const safeRows = normalizeDimension(rows, DEFAULT_ROWS);
|
|
288
|
+
const mode = computeMode(safeCols, safeRows);
|
|
289
|
+
const shellHeight = getShellHeight(safeRows);
|
|
290
|
+
|
|
291
|
+
const showNormalLogo =
|
|
292
|
+
process.env["UBUME_NO_ASCII_LOGO"] !== "1" && (
|
|
293
|
+
mode === "regular" ||
|
|
294
|
+
mode === "expanded" ||
|
|
295
|
+
(mode === "compact" && safeCols >= 72)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const showCompactHeader = !showNormalLogo;
|
|
299
|
+
|
|
300
|
+
const placeMetadataBesideLogo =
|
|
301
|
+
showNormalLogo && safeCols >= 95;
|
|
302
|
+
|
|
303
|
+
const placeMetadataBelowLogo =
|
|
304
|
+
showNormalLogo && !placeMetadataBesideLogo;
|
|
305
|
+
|
|
306
|
+
const resolvedHeaderRows = headerRows ?? (showNormalLogo ? 6 : 1);
|
|
307
|
+
const resolvedHeaderGapRows = headerGapRows ?? (mode === "compact" ? 0 : 1);
|
|
308
|
+
const panelStagePaddingY = mode === "compact" ? 0 : 1;
|
|
309
|
+
const resolvedComposerRows = mode === "compact" ? 3 : Math.max(0, composerRows);
|
|
310
|
+
const runtimeMetadataRows = 1;
|
|
311
|
+
const transientStatusRows = 0;
|
|
312
|
+
const bottomPaddingRows = mode === "compact" ? 0 : 1;
|
|
313
|
+
const bottomChromeBudget: BottomChromeBudget = {
|
|
314
|
+
runtimeMetadataRows,
|
|
315
|
+
composerRows: resolvedComposerRows,
|
|
316
|
+
transientStatusRows,
|
|
317
|
+
bottomPaddingRows,
|
|
318
|
+
totalRows: runtimeMetadataRows + resolvedComposerRows + transientStatusRows + bottomPaddingRows,
|
|
319
|
+
};
|
|
320
|
+
const baseReservedRows =
|
|
321
|
+
resolvedHeaderRows +
|
|
322
|
+
resolvedHeaderGapRows +
|
|
323
|
+
panelStagePaddingY * 2 +
|
|
324
|
+
bottomChromeBudget.totalRows +
|
|
325
|
+
Math.max(0, panelHintRows);
|
|
326
|
+
|
|
327
|
+
const activePanelRows = Math.max(1, shellHeight - baseReservedRows);
|
|
328
|
+
const contentWidth = getContentWidth(safeCols);
|
|
329
|
+
|
|
330
|
+
const isCompact = mode === "compact";
|
|
331
|
+
const borderRows = 2;
|
|
332
|
+
const titleRows = 1;
|
|
333
|
+
const headerRowsInPanel = isCompact ? 0 : 1;
|
|
334
|
+
const panelChromeRows = borderRows + titleRows + headerRowsInPanel;
|
|
335
|
+
const innerAvailableRows = Math.max(1, activePanelRows - panelChromeRows);
|
|
336
|
+
|
|
337
|
+
const borderCols = 4;
|
|
338
|
+
const innerAvailableCols = Math.max(20, contentWidth - borderCols);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
mode,
|
|
342
|
+
rows: safeRows,
|
|
343
|
+
cols: safeCols,
|
|
344
|
+
headerRows: resolvedHeaderRows,
|
|
345
|
+
headerGapRows: resolvedHeaderGapRows,
|
|
346
|
+
panelStagePaddingY,
|
|
347
|
+
activePanelRows: innerAvailableRows,
|
|
348
|
+
activePanelCols: innerAvailableCols,
|
|
349
|
+
bottomChromeBudget,
|
|
350
|
+
composerRows: resolvedComposerRows,
|
|
351
|
+
showNormalLogo,
|
|
352
|
+
showCompactHeader,
|
|
353
|
+
placeMetadataBesideLogo,
|
|
354
|
+
placeMetadataBelowLogo,
|
|
355
|
+
|
|
356
|
+
// Backward compatibility fields:
|
|
357
|
+
transcriptRows: activePanelRows,
|
|
358
|
+
panelRows: innerAvailableRows,
|
|
359
|
+
showLargeLogo: mode === "expanded",
|
|
360
|
+
showPanelSeparators: mode === "expanded",
|
|
361
|
+
showPanelColumnHeaders: mode === "expanded",
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function createLayoutSnapshot(
|
|
366
|
+
cols: number | undefined,
|
|
367
|
+
rows: number | undefined,
|
|
368
|
+
fallback: Layout = {
|
|
369
|
+
cols: DEFAULT_COLUMNS,
|
|
370
|
+
rows: DEFAULT_ROWS,
|
|
371
|
+
mode: computeMode(DEFAULT_COLUMNS, DEFAULT_ROWS),
|
|
372
|
+
},
|
|
373
|
+
): TerminalViewport {
|
|
374
|
+
const nextCols = normalizeDimension(cols, fallback.cols);
|
|
375
|
+
const nextRows = normalizeDimension(rows, fallback.rows);
|
|
376
|
+
|
|
377
|
+
const stableLayout = {
|
|
378
|
+
cols: nextCols,
|
|
379
|
+
rows: nextRows,
|
|
380
|
+
mode: computeMode(nextCols, nextRows),
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const isCramped = isCrampedTerminal(nextCols, nextRows);
|
|
384
|
+
const contentWidth = getContentWidth(nextCols);
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
...stableLayout,
|
|
388
|
+
contentWidth,
|
|
389
|
+
isCramped,
|
|
390
|
+
unstable: false,
|
|
391
|
+
layoutEpoch: 0,
|
|
392
|
+
isResizing: false,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function snapshot(stdout: NodeJS.WriteStream, fallback?: Layout): Layout {
|
|
397
|
+
return createLayoutSnapshot(stdout.columns, stdout.rows, fallback);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function createTerminalViewport(
|
|
401
|
+
cols: number | undefined,
|
|
402
|
+
rows: number | undefined,
|
|
403
|
+
fallback?: TerminalViewport,
|
|
404
|
+
isResizing = false,
|
|
405
|
+
): TerminalViewport {
|
|
406
|
+
const fallbackLayout = fallback
|
|
407
|
+
? { cols: fallback.cols, rows: fallback.rows, mode: fallback.mode }
|
|
408
|
+
: undefined;
|
|
409
|
+
const unstable = !isRenderableViewport(cols, rows);
|
|
410
|
+
const stableLayout = unstable && fallbackLayout
|
|
411
|
+
? fallbackLayout
|
|
412
|
+
: createLayoutSnapshot(cols, rows, fallbackLayout);
|
|
413
|
+
|
|
414
|
+
const isCramped = isCrampedTerminal(cols, rows);
|
|
415
|
+
const contentWidth = getContentWidth(stableLayout.cols);
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
...stableLayout,
|
|
419
|
+
rawCols: cols,
|
|
420
|
+
rawRows: rows,
|
|
421
|
+
contentWidth,
|
|
422
|
+
isCramped,
|
|
423
|
+
unstable,
|
|
424
|
+
layoutEpoch: fallback?.layoutEpoch ?? 0,
|
|
425
|
+
isResizing,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function advanceTerminalViewport(
|
|
430
|
+
current: TerminalViewport,
|
|
431
|
+
cols: number | undefined,
|
|
432
|
+
rows: number | undefined,
|
|
433
|
+
isResizing = false,
|
|
434
|
+
): TerminalViewport {
|
|
435
|
+
const next = createTerminalViewport(cols, rows, current, isResizing);
|
|
436
|
+
|
|
437
|
+
if (process.env.UBUME_LAYOUT_DEBUG === "1") {
|
|
438
|
+
renderDebug.traceEvent("layout", "advanceViewport", {
|
|
439
|
+
cols: next.cols,
|
|
440
|
+
rows: next.rows,
|
|
441
|
+
contentWidth: next.contentWidth,
|
|
442
|
+
isCramped: next.isCramped,
|
|
443
|
+
isResizing: next.isResizing,
|
|
444
|
+
mode: next.mode,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (!next.unstable && current.unstable) {
|
|
449
|
+
return {
|
|
450
|
+
...next,
|
|
451
|
+
layoutEpoch: current.layoutEpoch + 1,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
...next,
|
|
457
|
+
layoutEpoch: current.layoutEpoch,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ─── Hook ────────────────────────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
/** React hook — returns live layout that ignores transient invalid restore sizes. */
|
|
464
|
+
export function useTerminalViewport(): TerminalViewport {
|
|
465
|
+
const { stdout } = useStdout();
|
|
466
|
+
const [viewport, setViewport] = useState<TerminalViewport>(() => createTerminalViewport(stdout.columns, stdout.rows));
|
|
467
|
+
const settleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
468
|
+
|
|
469
|
+
useEffect(() => {
|
|
470
|
+
const commit = (isResizing = false) => {
|
|
471
|
+
setViewport((current) => {
|
|
472
|
+
const nextViewport = advanceTerminalViewport(current, stdout.columns, stdout.rows, isResizing);
|
|
473
|
+
if (
|
|
474
|
+
current.cols === nextViewport.cols &&
|
|
475
|
+
current.rows === nextViewport.rows &&
|
|
476
|
+
current.mode === nextViewport.mode &&
|
|
477
|
+
current.unstable === nextViewport.unstable &&
|
|
478
|
+
current.layoutEpoch === nextViewport.layoutEpoch &&
|
|
479
|
+
current.rawCols === nextViewport.rawCols &&
|
|
480
|
+
current.rawRows === nextViewport.rawRows &&
|
|
481
|
+
current.isResizing === nextViewport.isResizing
|
|
482
|
+
) {
|
|
483
|
+
renderDebug.traceFlickerEvent("measurementUpdate", {
|
|
484
|
+
result: "skipped",
|
|
485
|
+
cols: nextViewport.cols,
|
|
486
|
+
rows: nextViewport.rows,
|
|
487
|
+
mode: nextViewport.mode,
|
|
488
|
+
unstable: nextViewport.unstable,
|
|
489
|
+
isResizing: nextViewport.isResizing,
|
|
490
|
+
});
|
|
491
|
+
return current;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
renderDebug.traceFlickerEvent("measurementUpdate", {
|
|
495
|
+
result: "updated",
|
|
496
|
+
cols: nextViewport.cols,
|
|
497
|
+
rows: nextViewport.rows,
|
|
498
|
+
mode: nextViewport.mode,
|
|
499
|
+
unstable: nextViewport.unstable,
|
|
500
|
+
isResizing: nextViewport.isResizing,
|
|
501
|
+
});
|
|
502
|
+
return nextViewport;
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const onResize = () => {
|
|
507
|
+
renderDebug.traceEvent("terminal", "viewportHookResize", {
|
|
508
|
+
cols: stdout.columns,
|
|
509
|
+
rows: stdout.rows,
|
|
510
|
+
renderable: isRenderableViewport(stdout.columns, stdout.rows),
|
|
511
|
+
});
|
|
512
|
+
renderDebug.traceLayoutValidity("useTerminalViewport", {
|
|
513
|
+
rawCols: stdout.columns,
|
|
514
|
+
rawRows: stdout.rows,
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Leading edge: immediately enter isResizing state but do NOT commit
|
|
518
|
+
// new dimensions yet. This freezes the layout to prevent tearing while
|
|
519
|
+
// dragging, and signals animations to pause.
|
|
520
|
+
setTerminalResizing(true);
|
|
521
|
+
setViewport((current) => ({ ...current, isResizing: true }));
|
|
522
|
+
|
|
523
|
+
if (settleTimerRef.current) {
|
|
524
|
+
clearTimeout(settleTimerRef.current);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
settleTimerRef.current = setTimeout(() => {
|
|
528
|
+
settleTimerRef.current = null;
|
|
529
|
+
setTerminalResizing(false);
|
|
530
|
+
// Trailing edge: commit final dimensions and exit isResizing state.
|
|
531
|
+
commit(false);
|
|
532
|
+
}, RESTORE_SETTLE_MS);
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
stdout.on("resize", onResize);
|
|
536
|
+
return () => {
|
|
537
|
+
stdout.off("resize", onResize);
|
|
538
|
+
if (settleTimerRef.current) {
|
|
539
|
+
clearTimeout(settleTimerRef.current);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
}, [stdout]);
|
|
543
|
+
|
|
544
|
+
return viewport;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Calculate available vertical rows for active panels, falling back to a layout-based
|
|
549
|
+
* budget if availableRows is not explicitly provided.
|
|
550
|
+
*/
|
|
551
|
+
export function getAvailableRowsForPanel(
|
|
552
|
+
layout: Layout,
|
|
553
|
+
passedAvailableRows?: number
|
|
554
|
+
): number {
|
|
555
|
+
if (passedAvailableRows !== undefined) {
|
|
556
|
+
return passedAvailableRows;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return computeAppLayoutBudget({ cols: layout.cols, rows: layout.rows }).panelRows;
|
|
560
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useFocus, useInput } from "ink";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { useTheme } from "../theme.js";
|
|
6
|
+
|
|
7
|
+
export interface PendingImportFile {
|
|
8
|
+
srcPath: string;
|
|
9
|
+
rawPath: string;
|
|
10
|
+
destFilename: string;
|
|
11
|
+
isImage: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AttachmentImportPanelProps {
|
|
15
|
+
focusId: string;
|
|
16
|
+
files: PendingImportFile[];
|
|
17
|
+
attachmentsDir: string;
|
|
18
|
+
workspaceRoot: string;
|
|
19
|
+
modelSupportsVision: boolean | null;
|
|
20
|
+
onConfirm: () => void;
|
|
21
|
+
onCancel: () => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function compactHomePath(value: string, homeDir = os.homedir()): string {
|
|
25
|
+
const normalizedValue = value.replace(/\\/g, "/");
|
|
26
|
+
const normalizedHome = homeDir.replace(/\\/g, "/").replace(/\/$/, "");
|
|
27
|
+
if (!normalizedHome || normalizedValue === normalizedHome) return normalizedValue;
|
|
28
|
+
return normalizedValue.startsWith(`${normalizedHome}/`)
|
|
29
|
+
? `~/${normalizedValue.slice(normalizedHome.length + 1)}`
|
|
30
|
+
: normalizedValue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function AttachmentImportPanel({
|
|
34
|
+
focusId,
|
|
35
|
+
files,
|
|
36
|
+
attachmentsDir,
|
|
37
|
+
workspaceRoot,
|
|
38
|
+
modelSupportsVision,
|
|
39
|
+
onConfirm,
|
|
40
|
+
onCancel,
|
|
41
|
+
}: AttachmentImportPanelProps) {
|
|
42
|
+
const theme = useTheme();
|
|
43
|
+
const { isFocused } = useFocus({ id: focusId, autoFocus: true });
|
|
44
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
45
|
+
|
|
46
|
+
useInput((input, key) => {
|
|
47
|
+
if (key.escape) {
|
|
48
|
+
onCancel();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (key.return) {
|
|
52
|
+
if (selectedIndex === 0) onConfirm();
|
|
53
|
+
else onCancel();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (key.leftArrow) setSelectedIndex(0);
|
|
57
|
+
if (key.rightArrow || key.tab) setSelectedIndex(1);
|
|
58
|
+
}, { isActive: isFocused });
|
|
59
|
+
|
|
60
|
+
const relativeAttachmentsDir = path.relative(workspaceRoot, attachmentsDir).replace(/\\/g, "/");
|
|
61
|
+
const displayedAttachmentsDir = relativeAttachmentsDir.startsWith("..")
|
|
62
|
+
? compactHomePath(attachmentsDir)
|
|
63
|
+
: relativeAttachmentsDir;
|
|
64
|
+
const hasImages = files.some((f) => f.isImage);
|
|
65
|
+
const showVisionWarning = hasImages && modelSupportsVision === false;
|
|
66
|
+
const fileLabel = files.length === 1 ? "file" : "files";
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Box flexDirection="column" width="100%" marginTop={1}>
|
|
70
|
+
<Box
|
|
71
|
+
borderStyle="round"
|
|
72
|
+
borderColor={theme.border}
|
|
73
|
+
paddingX={2}
|
|
74
|
+
paddingY={1}
|
|
75
|
+
width="100%"
|
|
76
|
+
>
|
|
77
|
+
<Text color={theme.accent} bold>IMPORT FILE </Text>
|
|
78
|
+
<Text color={theme.textMuted}>
|
|
79
|
+
Copy {files.length} outside-workspace {fileLabel} into {displayedAttachmentsDir}?
|
|
80
|
+
</Text>
|
|
81
|
+
</Box>
|
|
82
|
+
|
|
83
|
+
<Box
|
|
84
|
+
borderStyle="round"
|
|
85
|
+
borderColor={theme.border}
|
|
86
|
+
paddingX={2}
|
|
87
|
+
paddingY={1}
|
|
88
|
+
marginTop={1}
|
|
89
|
+
width="100%"
|
|
90
|
+
flexDirection="column"
|
|
91
|
+
>
|
|
92
|
+
{files.map((file, i) => (
|
|
93
|
+
<Box key={i} marginBottom={i < files.length - 1 ? 1 : 0}>
|
|
94
|
+
<Text color={theme.text}>{path.basename(file.srcPath)} </Text>
|
|
95
|
+
<Text color={theme.textDim}>
|
|
96
|
+
{"→ "}{displayedAttachmentsDir}/{file.destFilename}
|
|
97
|
+
</Text>
|
|
98
|
+
</Box>
|
|
99
|
+
))}
|
|
100
|
+
|
|
101
|
+
{showVisionWarning && (
|
|
102
|
+
<Box marginTop={1}>
|
|
103
|
+
<Text color={theme.warning}>
|
|
104
|
+
Note: active model may not support images.
|
|
105
|
+
</Text>
|
|
106
|
+
</Box>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
<Box marginTop={1}>
|
|
110
|
+
<Text color={selectedIndex === 0 ? theme.accent : theme.textMuted} bold={selectedIndex === 0}>
|
|
111
|
+
{selectedIndex === 0 ? "› " : " "}Import once
|
|
112
|
+
</Text>
|
|
113
|
+
<Text color={theme.textDim}> · </Text>
|
|
114
|
+
<Text color={selectedIndex === 1 ? theme.accent : theme.textMuted} bold={selectedIndex === 1}>
|
|
115
|
+
{selectedIndex === 1 ? "› " : " "}Cancel
|
|
116
|
+
</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
|
|
119
|
+
<Box marginTop={1}>
|
|
120
|
+
<Text color={theme.textMuted}>
|
|
121
|
+
This copies only the listed {fileLabel}; it does not grant folder or workspace access.
|
|
122
|
+
</Text>
|
|
123
|
+
</Box>
|
|
124
|
+
|
|
125
|
+
<Box marginTop={1}>
|
|
126
|
+
<Text color={theme.textDim}>←/→ to navigate · Enter to select · Esc to cancel</Text>
|
|
127
|
+
</Box>
|
|
128
|
+
</Box>
|
|
129
|
+
</Box>
|
|
130
|
+
);
|
|
131
|
+
}
|