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,116 @@
|
|
|
1
|
+
import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
|
|
2
|
+
|
|
3
|
+
export type DiffRenderLineType = "file" | "hunk" | "add" | "remove" | "context" | "meta";
|
|
4
|
+
|
|
5
|
+
export interface DiffRenderLine {
|
|
6
|
+
type: DiffRenderLineType;
|
|
7
|
+
text: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DiffRenderOptions {
|
|
11
|
+
force?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DIFF_GIT_HEADER_PATTERN = /^diff --git\s+/;
|
|
15
|
+
const HUNK_HEADER_PATTERN = /^@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s*@@/;
|
|
16
|
+
const INDEX_HEADER_PATTERN = /^index\s+\S+\.\.\S+/;
|
|
17
|
+
const OLD_FILE_HEADER_PATTERN = /^---\s+\S+/;
|
|
18
|
+
const NEW_FILE_HEADER_PATTERN = /^\+\+\+\s+\S+/;
|
|
19
|
+
const ADD_LINE_PATTERN = /^\+(?!\+\+)/;
|
|
20
|
+
const REMOVE_LINE_PATTERN = /^-(?!--)/;
|
|
21
|
+
|
|
22
|
+
function normalizeDiffText(text: string): string {
|
|
23
|
+
return sanitizeTerminalOutput(text, { preserveTabs: false, tabSize: 2 })
|
|
24
|
+
.replace(/\r\n/g, "\n")
|
|
25
|
+
.replace(/\r/g, "\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getDiffLines(text: string): string[] {
|
|
29
|
+
return normalizeDiffText(text)
|
|
30
|
+
.split("\n")
|
|
31
|
+
.map((line) => line.trimEnd());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function trimBlankEdges(lines: string[]): string[] {
|
|
35
|
+
let start = 0;
|
|
36
|
+
let end = lines.length;
|
|
37
|
+
|
|
38
|
+
while (start < end && lines[start]?.trim() === "") start += 1;
|
|
39
|
+
while (end > start && lines[end - 1]?.trim() === "") end -= 1;
|
|
40
|
+
|
|
41
|
+
return lines.slice(start, end);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hasPairedFileHeaders(lines: readonly string[]): boolean {
|
|
45
|
+
return lines.some((line) => OLD_FILE_HEADER_PATTERN.test(line))
|
|
46
|
+
&& lines.some((line) => NEW_FILE_HEADER_PATTERN.test(line));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hasStrongDiffSignal(lines: readonly string[]): boolean {
|
|
50
|
+
return lines.some((line) =>
|
|
51
|
+
DIFF_GIT_HEADER_PATTERN.test(line)
|
|
52
|
+
|| HUNK_HEADER_PATTERN.test(line)
|
|
53
|
+
|| INDEX_HEADER_PATTERN.test(line)
|
|
54
|
+
) || hasPairedFileHeaders(lines);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasRealChangeLine(lines: readonly string[]): boolean {
|
|
58
|
+
return lines.some((line) => ADD_LINE_PATTERN.test(line) || REMOVE_LINE_PATTERN.test(line));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function shouldRenderUnifiedDiff(lines: readonly string[], options: DiffRenderOptions = {}): boolean {
|
|
62
|
+
if (lines.length < 2) return false;
|
|
63
|
+
if (options.force) {
|
|
64
|
+
return hasStrongDiffSignal(lines) || hasRealChangeLine(lines);
|
|
65
|
+
}
|
|
66
|
+
return hasStrongDiffSignal(lines) && hasRealChangeLine(lines);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function classifyDiffLine(line: string): DiffRenderLineType {
|
|
70
|
+
if (DIFF_GIT_HEADER_PATTERN.test(line) || OLD_FILE_HEADER_PATTERN.test(line) || NEW_FILE_HEADER_PATTERN.test(line)) {
|
|
71
|
+
return "file";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (HUNK_HEADER_PATTERN.test(line) || line.startsWith("@@")) {
|
|
75
|
+
return "hunk";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (ADD_LINE_PATTERN.test(line)) {
|
|
79
|
+
return "add";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (REMOVE_LINE_PATTERN.test(line)) {
|
|
83
|
+
return "remove";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (INDEX_HEADER_PATTERN.test(line) || line.startsWith("\\ No newline")) {
|
|
87
|
+
return "meta";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return "context";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isUnifiedDiff(text: string, options: DiffRenderOptions = {}): boolean {
|
|
94
|
+
return shouldRenderUnifiedDiff(trimBlankEdges(getDiffLines(text)), options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function renderUnifiedDiff(text: string, options: DiffRenderOptions = {}): DiffRenderLine[] {
|
|
98
|
+
const lines = trimBlankEdges(getDiffLines(text));
|
|
99
|
+
if (lines.length === 0) {
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!shouldRenderUnifiedDiff(lines, options)) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return lines.map((line) => ({
|
|
108
|
+
type: classifyDiffLine(line),
|
|
109
|
+
text: line,
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function maybeRenderDiff(text: string, options: DiffRenderOptions = {}): DiffRenderLine[] | null {
|
|
114
|
+
const rendered = renderUnifiedDiff(text, options);
|
|
115
|
+
return rendered.length > 0 ? rendered : null;
|
|
116
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { getTextWidth } from "./textLayout.js";
|
|
2
|
+
|
|
3
|
+
// ─── Logo constants ───────────────────────────────────────────────────────────
|
|
4
|
+
// Each variant is an array of exact terminal rows.
|
|
5
|
+
//
|
|
6
|
+
// IMPORTANT: Never apply `bold` when rendering these rows. Bold rendering of
|
|
7
|
+
// Unicode full-block (█) and box-drawing (╔═╗╝) characters causes per-glyph
|
|
8
|
+
// width/stroke differences in most terminal fonts (Ptyxis, GNOME Terminal,
|
|
9
|
+
// VS Code), producing visible gaps between characters that should be flush.
|
|
10
|
+
// The companion `wrap="truncate"` rule keeps each row on exactly one terminal
|
|
11
|
+
// line regardless of the surrounding Ink flex layout.
|
|
12
|
+
|
|
13
|
+
// Canonical Ubume brand wordmark — the ██ block art is the authoritative
|
|
14
|
+
// large logo for wide/max layouts.
|
|
15
|
+
export const UBUME_WORDMARK = [
|
|
16
|
+
"██╗ ██╗██████╗ ██╗ ██╗███╗ ███╗███████╗",
|
|
17
|
+
"██║ ██║██╔══██╗██║ ██║████╗ ████║██╔════╝",
|
|
18
|
+
"██║ ██║██████╔╝██║ ██║██╔████╔██║█████╗ ",
|
|
19
|
+
"██║ ██║██╔══██╗██║ ██║██║╚██╔╝██║██╔══╝ ",
|
|
20
|
+
"╚██████╔╝██████╔╝╚██████╔╝██║ ╚═╝ ██║███████╗",
|
|
21
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝",
|
|
22
|
+
].join("\n");
|
|
23
|
+
|
|
24
|
+
export const CODEXA_WORDMARK = UBUME_WORDMARK;
|
|
25
|
+
|
|
26
|
+
/** 6-row ANSI Shadow block-char logo. Requires cols ≥ LOGO_LARGE_MIN_COLS. */
|
|
27
|
+
export const LOGO_LARGE: readonly string[] = UBUME_WORDMARK.split("\n");
|
|
28
|
+
|
|
29
|
+
/** 4-row pure-ASCII art logo. Requires cols ≥ LOGO_MEDIUM_MIN_COLS. */
|
|
30
|
+
export const LOGO_MEDIUM: readonly string[] = [
|
|
31
|
+
" _ _ ____ _ _ __ __ _____ ",
|
|
32
|
+
"| | | | __ )| | | | \\/ | ____|",
|
|
33
|
+
"| |_| | _ \\| |_| | |\\/| | _| ",
|
|
34
|
+
" \\___/|____/ \\___/|_| |_|_____|",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** 1-row compact logo. Requires cols ≥ LOGO_COMPACT_MIN_COLS. */
|
|
38
|
+
export const LOGO_COMPACT: readonly string[] = [
|
|
39
|
+
"✦ UBUME",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// ─── Breakpoints ──────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
// Aligned with MEDIUM_HEADER_MIN_COLUMNS so any side-by-side-capable terminal
|
|
45
|
+
// shows the canonical block wordmark instead of the thin ASCII fallback.
|
|
46
|
+
export const LOGO_LARGE_MIN_COLS = 72;
|
|
47
|
+
export const LOGO_MEDIUM_MIN_COLS = 72;
|
|
48
|
+
export const LOGO_COMPACT_MIN_COLS = 48;
|
|
49
|
+
|
|
50
|
+
// Minimum terminal rows each variant needs to render without crowding out the
|
|
51
|
+
// metadata + composer. A wide-but-short terminal (e.g. VS Code's bottom panel)
|
|
52
|
+
// must step DOWN to a smaller logo instead of dropping straight to text-only.
|
|
53
|
+
export const LOGO_LARGE_MIN_ROWS = 35;
|
|
54
|
+
export const LOGO_MEDIUM_MIN_ROWS = 16;
|
|
55
|
+
export const LOGO_COMPACT_MIN_ROWS = 12;
|
|
56
|
+
|
|
57
|
+
const LOGO_VARIANTS: readonly { logo: readonly string[]; minCols: number; minRows: number }[] = [
|
|
58
|
+
{ logo: LOGO_LARGE, minCols: LOGO_LARGE_MIN_COLS, minRows: LOGO_LARGE_MIN_ROWS },
|
|
59
|
+
{ logo: LOGO_MEDIUM, minCols: LOGO_MEDIUM_MIN_COLS, minRows: LOGO_MEDIUM_MIN_ROWS },
|
|
60
|
+
{ logo: LOGO_COMPACT, minCols: LOGO_COMPACT_MIN_COLS, minRows: LOGO_COMPACT_MIN_ROWS },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// ─── Selection ────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
function isNoLogoEnv(): boolean {
|
|
66
|
+
return process.env["UBUME_NO_ASCII_LOGO"] === "1" || process.env["CODEXA_NO_ASCII_LOGO"] === "1";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isCompactLogoEnv(): boolean {
|
|
70
|
+
return process.env["UBUME_COMPACT_LOGO"] === "1" || process.env["CODEXA_COMPACT_LOGO"] === "1";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Returns the best logo variant for the given terminal column count.
|
|
75
|
+
*
|
|
76
|
+
* Env overrides:
|
|
77
|
+
* UBUME_NO_ASCII_LOGO=1 / CODEXA_NO_ASCII_LOGO=1 → always text-only (empty array)
|
|
78
|
+
* UBUME_COMPACT_LOGO=1 / CODEXA_COMPACT_LOGO=1 → always compact single-line logo
|
|
79
|
+
*/
|
|
80
|
+
export function selectLogoVariant(cols: number): readonly string[] {
|
|
81
|
+
if (isNoLogoEnv()) return [];
|
|
82
|
+
if (isCompactLogoEnv()) return LOGO_COMPACT;
|
|
83
|
+
if (cols >= LOGO_LARGE_MIN_COLS) return LOGO_LARGE;
|
|
84
|
+
if (cols >= LOGO_COMPACT_MIN_COLS) return LOGO_COMPACT;
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Returns the largest logo variant that fits BOTH the available columns and
|
|
90
|
+
* rows. Unlike {@link selectLogoVariant} (columns-only), this degrades a
|
|
91
|
+
* too-tall logo to a shorter one before falling back to text-only — so a
|
|
92
|
+
* wide-but-short terminal keeps a logo instead of collapsing to a flat line.
|
|
93
|
+
* Returns an empty array only when even the 1-row compact logo cannot fit.
|
|
94
|
+
*
|
|
95
|
+
* Honours the same env overrides as {@link selectLogoVariant}.
|
|
96
|
+
*/
|
|
97
|
+
export function selectLogoVariantForViewport(cols: number, rows: number): readonly string[] {
|
|
98
|
+
if (isNoLogoEnv()) return [];
|
|
99
|
+
if (isCompactLogoEnv()) {
|
|
100
|
+
return rows >= LOGO_COMPACT_MIN_ROWS ? LOGO_COMPACT : [];
|
|
101
|
+
}
|
|
102
|
+
for (const variant of LOGO_VARIANTS) {
|
|
103
|
+
if (cols >= variant.minCols && rows >= variant.minRows) {
|
|
104
|
+
return variant.logo;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Returns the visual column width of the widest row in a logo variant. */
|
|
111
|
+
export function getLogoWidth(logo: readonly string[]): number {
|
|
112
|
+
return logo.reduce((max, line) => Math.max(max, getTextWidth(line)), 0);
|
|
113
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { formatModeLabel } from "../../config/settings.js";
|
|
2
|
+
import type { Theme } from "../theme.js";
|
|
3
|
+
|
|
4
|
+
export interface ModeDisplaySpec {
|
|
5
|
+
label: string;
|
|
6
|
+
ringGlyph: string;
|
|
7
|
+
ringColor: string;
|
|
8
|
+
ringFill: string;
|
|
9
|
+
iconColor: string;
|
|
10
|
+
labelColor: string;
|
|
11
|
+
labelBold: boolean;
|
|
12
|
+
ringBold: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getModeDisplaySpec(mode: string, theme: Theme): ModeDisplaySpec {
|
|
16
|
+
switch (mode) {
|
|
17
|
+
case "full-auto":
|
|
18
|
+
return {
|
|
19
|
+
label: formatModeLabel(mode),
|
|
20
|
+
ringGlyph: "◉",
|
|
21
|
+
ringColor: theme.warning,
|
|
22
|
+
ringFill: theme.border,
|
|
23
|
+
iconColor: theme.warning,
|
|
24
|
+
labelColor: theme.text,
|
|
25
|
+
labelBold: true,
|
|
26
|
+
ringBold: true,
|
|
27
|
+
};
|
|
28
|
+
case "auto-edit":
|
|
29
|
+
return {
|
|
30
|
+
label: formatModeLabel(mode),
|
|
31
|
+
ringGlyph: "◎",
|
|
32
|
+
ringColor: theme.borderFocused,
|
|
33
|
+
ringFill: theme.surfaceMuted,
|
|
34
|
+
iconColor: theme.prompt,
|
|
35
|
+
labelColor: theme.text,
|
|
36
|
+
labelBold: true,
|
|
37
|
+
ringBold: false,
|
|
38
|
+
};
|
|
39
|
+
case "suggest":
|
|
40
|
+
default:
|
|
41
|
+
return {
|
|
42
|
+
label: formatModeLabel(mode),
|
|
43
|
+
ringGlyph: "○",
|
|
44
|
+
ringColor: theme.success,
|
|
45
|
+
ringFill: theme.surfaceMuted,
|
|
46
|
+
iconColor: theme.success,
|
|
47
|
+
labelColor: theme.textMuted,
|
|
48
|
+
labelBold: false,
|
|
49
|
+
ringBold: false,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isNoiseLine } from "../../core/providers/codexTranscript.js";
|
|
2
|
+
import { sanitizeTerminalOutput } from "../../core/terminal/terminalSanitize.js";
|
|
3
|
+
import { parseMarkdown, type Segment } from "./Markdown.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Sanitize: Strip ANSI escape sequences and non-printable control characters.
|
|
7
|
+
* Removes known UI chrome bleed (e.g. box drawing characters) before rendering.
|
|
8
|
+
*
|
|
9
|
+
* Note on diff colours: Diff colouring in this app is applied at the React/Ink
|
|
10
|
+
* rendering layer via getDiffTone() tones, NOT via raw ANSI escape sequences.
|
|
11
|
+
* This means sanitizeTerminalOutput() can safely strip all raw ANSI here —
|
|
12
|
+
* the correct theme colours are re-applied by buildCodePanelRows/buildMarkdownRows
|
|
13
|
+
* in timelineMeasure.ts. There is no need to preserve SGR codes at this stage.
|
|
14
|
+
*/
|
|
15
|
+
export function sanitizeOutput(raw: string): string {
|
|
16
|
+
if (!raw) return "";
|
|
17
|
+
const clean = sanitizeTerminalOutput(raw, { preserveTabs: false, tabSize: 2 });
|
|
18
|
+
return clean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sanitize streamed chunks directly (delegates to sanitizeOutput).
|
|
23
|
+
*/
|
|
24
|
+
export function sanitizeStreamChunk(chunk: string): string {
|
|
25
|
+
return sanitizeOutput(chunk);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalize: Normalizes text formatting for the box wrappers.
|
|
30
|
+
* Replaces CRLF with LF and collapses excessive blank lines to prevent
|
|
31
|
+
* vertical stretching and layout popping.
|
|
32
|
+
*/
|
|
33
|
+
export function normalizeOutput(clean: string): string {
|
|
34
|
+
let normalized = clean.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
35
|
+
// Collapse excessive vertical whitespace (4+ newlines into 3)
|
|
36
|
+
normalized = normalized.replace(/\n{4,}/g, "\n\n\n");
|
|
37
|
+
|
|
38
|
+
// Remove lines that are purely known noise prefixes
|
|
39
|
+
const lines = normalized.split("\n");
|
|
40
|
+
const filteredLines = lines.filter((line) => !isNoiseLine(line));
|
|
41
|
+
|
|
42
|
+
return filteredLines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Classify: Segments the normalized string into typed semantic blocks
|
|
47
|
+
* such as prose, code blocks, diffs, lists, and headers.
|
|
48
|
+
*
|
|
49
|
+
* Diff colouring is applied at render time via getDiffTone() in
|
|
50
|
+
* timelineMeasure.ts, which maps each diff line to a TimelineTone that
|
|
51
|
+
* the theme system resolves to the correct terminal colour.
|
|
52
|
+
*/
|
|
53
|
+
export function classifyOutput(normalized: string): Segment[] {
|
|
54
|
+
return parseMarkdown(normalized);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Format For Box: Intentional pass-through — segments are returned as-is.
|
|
59
|
+
* Width-fitting is handled downstream by the layout engine in timelineMeasure.ts.
|
|
60
|
+
* This function exists as a named pipeline stage for clarity and future extension.
|
|
61
|
+
*/
|
|
62
|
+
export function formatForBox(classified: Segment[], boxWidth: number): Segment[] {
|
|
63
|
+
return classified;
|
|
64
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { formatModeLabel, formatReasoningLabel } from "../../config/settings.js";
|
|
2
|
+
import type { RuntimeSummary } from "../../config/runtimeConfig.js";
|
|
3
|
+
import type { CodexModelCapability } from "../../core/models/codexModelCapabilities.js";
|
|
4
|
+
import type { ModelSpec } from "../../core/models/modelSpecs.js";
|
|
5
|
+
import type { ModelContextMetadata } from "../../core/providerRuntime/contextMetadata.js";
|
|
6
|
+
import { contextMetadataToModelSpec, formatContextCompact } from "../../core/providerRuntime/contextMetadata.js";
|
|
7
|
+
import type { ActiveProviderRoute } from "../../core/providerRuntime/types.js";
|
|
8
|
+
import { getAntigravityModelLabel } from "../../core/providerRuntime/antigravity.js";
|
|
9
|
+
import { CODEXA_NATIVE_MODEL_ID } from "../../core/providerRuntime/codexaNative.js";
|
|
10
|
+
|
|
11
|
+
export interface ActiveRuntimeDisplayInput {
|
|
12
|
+
route: ActiveProviderRoute;
|
|
13
|
+
reasoningLevel: string;
|
|
14
|
+
mode: string;
|
|
15
|
+
tokensUsed: number;
|
|
16
|
+
modelCapability?: CodexModelCapability | null;
|
|
17
|
+
contextMetadata?: ModelContextMetadata | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ActiveRuntimeDisplay {
|
|
21
|
+
providerLabel: string;
|
|
22
|
+
modelDisplay: string;
|
|
23
|
+
footerModelDisplay: string;
|
|
24
|
+
contextDisplay: string;
|
|
25
|
+
modeLabel: string;
|
|
26
|
+
modelSpec: ModelSpec;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const PROVIDER_DISPLAY: Record<string, string> = {
|
|
30
|
+
openai: "OpenAI Codex CLI",
|
|
31
|
+
anthropic: "Claude Code CLI",
|
|
32
|
+
google: "Gemini CLI",
|
|
33
|
+
mistral: "Mistral Vibe CLI",
|
|
34
|
+
local: "Local",
|
|
35
|
+
"codexa-native": "Codexa Native",
|
|
36
|
+
"codexa-cupy": "Codexa Native",
|
|
37
|
+
antigravity: "Antigravity CLI",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function formatContextLimit(value: number): string {
|
|
41
|
+
if (value >= 1_000_000) {
|
|
42
|
+
const millions = value / 1_000_000;
|
|
43
|
+
return Number.isInteger(millions) || Math.abs(millions - 1.048576) < 0.000001
|
|
44
|
+
? `${Math.round(millions)}M`
|
|
45
|
+
: `${millions.toFixed(1)}M`;
|
|
46
|
+
}
|
|
47
|
+
return formatContextCompact(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function formatUsedTokens(value: number): string {
|
|
51
|
+
return formatContextCompact(value).replace(/k$/, "K");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isContextForRoute(metadata: ModelContextMetadata | null | undefined, route: ActiveProviderRoute): metadata is ModelContextMetadata {
|
|
55
|
+
return metadata?.providerId === route.providerId && metadata.modelId === route.modelId;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getModelLabel(route: ActiveProviderRoute, capability?: CodexModelCapability | null): string {
|
|
59
|
+
if (route.providerId === "anthropic") {
|
|
60
|
+
return capability?.label ?? route.modelId;
|
|
61
|
+
}
|
|
62
|
+
if (route.providerId === "antigravity") {
|
|
63
|
+
return getAntigravityModelLabel(route.modelId);
|
|
64
|
+
}
|
|
65
|
+
if (route.providerId === "codexa-native") {
|
|
66
|
+
// Older persisted routes may still contain the 900M checkpoint directory
|
|
67
|
+
// name; the user-facing Codexa Native model is the canonical 1B SFT v2 ID.
|
|
68
|
+
return CODEXA_NATIVE_MODEL_ID;
|
|
69
|
+
}
|
|
70
|
+
if (route.providerId === "codexa-cupy") {
|
|
71
|
+
return route.modelId;
|
|
72
|
+
}
|
|
73
|
+
return route.modelId;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildActiveRuntimeDisplay({
|
|
77
|
+
route,
|
|
78
|
+
reasoningLevel,
|
|
79
|
+
mode,
|
|
80
|
+
tokensUsed,
|
|
81
|
+
modelCapability = null,
|
|
82
|
+
contextMetadata = null,
|
|
83
|
+
}: ActiveRuntimeDisplayInput): ActiveRuntimeDisplay {
|
|
84
|
+
const providerLabel = PROVIDER_DISPLAY[route.providerId] ?? route.providerId;
|
|
85
|
+
const rawReasoning = route.providerId === "antigravity"
|
|
86
|
+
? route.reasoning
|
|
87
|
+
: route.reasoning ?? reasoningLevel;
|
|
88
|
+
// Local runtimes own their reasoning behavior; Ubume cannot adjust it.
|
|
89
|
+
// Do not present the global fallback as if it were an active Local setting.
|
|
90
|
+
const reasoning = route.providerId !== "local" && rawReasoning
|
|
91
|
+
? formatReasoningLabel(rawReasoning)
|
|
92
|
+
: null;
|
|
93
|
+
const modelLabel = getModelLabel(route, modelCapability);
|
|
94
|
+
const validContextMetadata = isContextForRoute(contextMetadata, route) ? contextMetadata : null;
|
|
95
|
+
const contextDisplay = validContextMetadata?.contextLength != null
|
|
96
|
+
? `${formatUsedTokens(tokensUsed)} / ${validContextMetadata.confidence === "estimated" ? "~" : ""}${formatContextLimit(validContextMetadata.contextLength)}`
|
|
97
|
+
: "Unknown";
|
|
98
|
+
const modelSpec = contextMetadataToModelSpec(validContextMetadata ?? {
|
|
99
|
+
providerId: route.providerId,
|
|
100
|
+
modelId: route.modelId,
|
|
101
|
+
contextLength: null,
|
|
102
|
+
source: "unknown",
|
|
103
|
+
confidence: "unknown",
|
|
104
|
+
error: "Context length unavailable for this model.",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
providerLabel,
|
|
109
|
+
modelDisplay: reasoning
|
|
110
|
+
? `${providerLabel} / ${modelLabel} / reasoning: ${reasoning}`
|
|
111
|
+
: `${providerLabel} / ${modelLabel}`,
|
|
112
|
+
footerModelDisplay: reasoning
|
|
113
|
+
? `${providerLabel} / ${modelLabel} (${reasoning})`
|
|
114
|
+
: `${providerLabel} / ${modelLabel}`,
|
|
115
|
+
contextDisplay,
|
|
116
|
+
modeLabel: formatModeLabel(mode),
|
|
117
|
+
modelSpec,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function runtimeDisplayToSummary(display: ActiveRuntimeDisplay, base: RuntimeSummary): RuntimeSummary {
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
providerLabel: display.providerLabel,
|
|
125
|
+
modelLabel: display.modelDisplay,
|
|
126
|
+
contextLabel: display.contextDisplay,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const MARKDOWN_LINK_RE = /!?\[([^\]\n]+)\]\(([^)\n]+)\)/g;
|
|
2
|
+
const FILE_URL_RE = /\bfile:\/\/\/?[^\s`"'<>)]*/gi;
|
|
3
|
+
const WINDOWS_ABSOLUTE_PATH_RE = /\b[A-Za-z]:[\\/][^\n`"'<>)]*?\.(?:tsx?|jsx?|md|json|ya?ml|toml|css|html|py|txt|mjs|cjs)(?:#L\d+(?:-L?\d+)?)?/g;
|
|
4
|
+
const UNIX_ABSOLUTE_PATH_RE = /(?:^|[\s(])((?:\/Users|\/home|\/workspace|\/workspaces|\/mnt\/[a-z])\/[^\n`"'<>)]*?\.(?:tsx?|jsx?|md|json|ya?ml|toml|css|html|py|txt|mjs|cjs)(?:#L\d+(?:-L?\d+)?)?)/g;
|
|
5
|
+
|
|
6
|
+
const PATH_ROOT_SEGMENTS = new Set([
|
|
7
|
+
".github",
|
|
8
|
+
"app",
|
|
9
|
+
"bin",
|
|
10
|
+
"components",
|
|
11
|
+
"docs",
|
|
12
|
+
"lib",
|
|
13
|
+
"pages",
|
|
14
|
+
"preview",
|
|
15
|
+
"scripts",
|
|
16
|
+
"src",
|
|
17
|
+
"test",
|
|
18
|
+
"tests",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const ROOT_FILE_RE = /^(?:README|CHANGELOG|LICENSE|SECURITY|package|tsconfig|vite\.config|next\.config|bun\.lock)(?:\.[a-z0-9]+)?$/i;
|
|
22
|
+
|
|
23
|
+
function decodePath(value: string): string {
|
|
24
|
+
try {
|
|
25
|
+
return decodeURIComponent(value);
|
|
26
|
+
} catch {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stripLinkLabelMarkdown(label: string): string {
|
|
32
|
+
return label.trim().replace(/^`([^`]+)`$/, "$1").replace(/^\*\*([^*]+)\*\*$/, "$1");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function splitLineAnchor(value: string): { path: string; lineSuffix: string } {
|
|
36
|
+
const anchorMatch = /#L(\d+)(?:-L?(\d+))?$/i.exec(value);
|
|
37
|
+
if (!anchorMatch) {
|
|
38
|
+
return { path: value, lineSuffix: "" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const start = anchorMatch[1]!;
|
|
42
|
+
const end = anchorMatch[2];
|
|
43
|
+
return {
|
|
44
|
+
path: value.slice(0, anchorMatch.index),
|
|
45
|
+
lineSuffix: end ? `:${start}-${end}` : `:${start}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeLocalPathPathname(rawPath: string): { path: string; lineSuffix: string } {
|
|
50
|
+
let value = rawPath.trim();
|
|
51
|
+
value = value.replace(/^<|>$/g, "");
|
|
52
|
+
value = value.replace(/^file:(?:\/\/)?/i, "");
|
|
53
|
+
value = value.replace(/^\/([A-Za-z]:[\\/])/, "$1");
|
|
54
|
+
value = decodePath(value);
|
|
55
|
+
value = value.replace(/\\/g, "/");
|
|
56
|
+
value = value.replace(/[?#][^#]*$/, (suffix) => suffix.startsWith("#L") ? suffix : "");
|
|
57
|
+
return splitLineAnchor(value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function looksLikeLocalTarget(target: string): boolean {
|
|
61
|
+
const trimmed = target.trim();
|
|
62
|
+
return /^file:/i.test(trimmed)
|
|
63
|
+
|| /^[A-Za-z]:[\\/]/.test(trimmed)
|
|
64
|
+
|| /^\/(?:Users|home|workspace|workspaces|mnt\/[a-z])\//i.test(trimmed);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function looksLikePathLabel(label: string): boolean {
|
|
68
|
+
return /[\\/]/.test(label) || /^[\w.-]+\.[A-Za-z0-9]+(?::\d+(?:-\d+)?)?$/.test(label);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatLocalPathForTerminal(rawPath: string): string {
|
|
72
|
+
if (/^https?:\/\//i.test(rawPath.trim())) {
|
|
73
|
+
return rawPath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { path, lineSuffix } = normalizeLocalPathPathname(rawPath);
|
|
77
|
+
const segments = path.split("/").filter(Boolean);
|
|
78
|
+
if (segments.length === 0) {
|
|
79
|
+
return path + lineSuffix;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rootIndex = segments.findIndex((segment) => PATH_ROOT_SEGMENTS.has(segment));
|
|
83
|
+
if (rootIndex >= 0) {
|
|
84
|
+
return segments.slice(rootIndex).join("/") + lineSuffix;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fileName = segments[segments.length - 1]!;
|
|
88
|
+
if (ROOT_FILE_RE.test(fileName)) {
|
|
89
|
+
return fileName + lineSuffix;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return segments.slice(-1).join("/") + lineSuffix;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatLocalMarkdownLink(label: string, target: string): string {
|
|
96
|
+
const cleanLabel = stripLinkLabelMarkdown(label);
|
|
97
|
+
const compactTarget = formatLocalPathForTerminal(target);
|
|
98
|
+
const lineSuffix = /(:\d+(?:-\d+)?)$/.exec(compactTarget)?.[1] ?? "";
|
|
99
|
+
|
|
100
|
+
if (looksLikePathLabel(cleanLabel)) {
|
|
101
|
+
const compactLabel = formatLocalPathForTerminal(cleanLabel);
|
|
102
|
+
return lineSuffix && !compactLabel.endsWith(lineSuffix)
|
|
103
|
+
? `${compactLabel}${lineSuffix}`
|
|
104
|
+
: compactLabel;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return cleanLabel ? `${cleanLabel} (${compactTarget})` : compactTarget;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatTerminalAnswerInline(text: string): string {
|
|
111
|
+
if (!text) return text;
|
|
112
|
+
|
|
113
|
+
let formatted = text.replace(MARKDOWN_LINK_RE, (full, label: string, target: string) => {
|
|
114
|
+
if (!looksLikeLocalTarget(target)) {
|
|
115
|
+
return full;
|
|
116
|
+
}
|
|
117
|
+
return formatLocalMarkdownLink(label, target);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
formatted = formatted.replace(FILE_URL_RE, (match) => formatLocalPathForTerminal(match));
|
|
121
|
+
formatted = formatted.replace(WINDOWS_ABSOLUTE_PATH_RE, (match) => formatLocalPathForTerminal(match));
|
|
122
|
+
formatted = formatted.replace(UNIX_ABSOLUTE_PATH_RE, (full, path: string) => {
|
|
123
|
+
const prefix = full.slice(0, full.length - path.length);
|
|
124
|
+
return `${prefix}${formatLocalPathForTerminal(path)}`;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return formatted;
|
|
128
|
+
}
|