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.
Files changed (182) hide show
  1. package/README.md +88 -0
  2. package/bin/codexa-local-harness-bridge.js +3 -0
  3. package/bin/codexa.js +28 -0
  4. package/bin/ubume-local-harness-bridge.js +325 -0
  5. package/bin/ubume.js +398 -0
  6. package/package.json +66 -0
  7. package/src/app.tsx +5759 -0
  8. package/src/commands/handler.ts +889 -0
  9. package/src/config/appVersion.ts +69 -0
  10. package/src/config/buildInfo.ts +3 -0
  11. package/src/config/launchArgs.ts +196 -0
  12. package/src/config/layeredConfig.ts +853 -0
  13. package/src/config/legacyEnv.ts +16 -0
  14. package/src/config/persistence.ts +377 -0
  15. package/src/config/runtimeConfig.ts +558 -0
  16. package/src/config/settings.ts +405 -0
  17. package/src/config/toml-serialize.ts +109 -0
  18. package/src/config/trustStore.ts +84 -0
  19. package/src/config/updateCheckCache.ts +85 -0
  20. package/src/core/README.md +55 -0
  21. package/src/core/agent/loop.ts +464 -0
  22. package/src/core/agent/protocol.ts +345 -0
  23. package/src/core/agent/tools.ts +423 -0
  24. package/src/core/auth/codexAuth.ts +359 -0
  25. package/src/core/codex/codexExecArgs.ts +166 -0
  26. package/src/core/codex/codexLaunch.ts +163 -0
  27. package/src/core/codex/codexPrompt.ts +429 -0
  28. package/src/core/debug/inputDebug.ts +51 -0
  29. package/src/core/debug/localStreamDebug.ts +50 -0
  30. package/src/core/debug/modelStateDebug.ts +35 -0
  31. package/src/core/executables/antigravityExecutable.ts +48 -0
  32. package/src/core/executables/claudeExecutable.ts +63 -0
  33. package/src/core/executables/codexExecutable.ts +160 -0
  34. package/src/core/executables/executableResolver.ts +164 -0
  35. package/src/core/executables/geminiExecutable.ts +78 -0
  36. package/src/core/models/codexCapabilities.ts +97 -0
  37. package/src/core/models/codexModelCapabilities.ts +624 -0
  38. package/src/core/models/codexModelsCacheSeed.ts +153 -0
  39. package/src/core/models/modelSpecs.ts +303 -0
  40. package/src/core/models/providerModelCache.ts +94 -0
  41. package/src/core/perf/profiler.ts +125 -0
  42. package/src/core/perf/renderDebug.ts +398 -0
  43. package/src/core/process/CommandRunner.ts +280 -0
  44. package/src/core/process/processValidation.ts +111 -0
  45. package/src/core/providerLauncher/launcher.ts +220 -0
  46. package/src/core/providerLauncher/registry.ts +354 -0
  47. package/src/core/providerLauncher/types.ts +95 -0
  48. package/src/core/providerLauncher/workspaceConfig.ts +487 -0
  49. package/src/core/providerRuntime/anthropic.ts +580 -0
  50. package/src/core/providerRuntime/antigravity.ts +500 -0
  51. package/src/core/providerRuntime/capabilityProfile.ts +383 -0
  52. package/src/core/providerRuntime/claudeCodeDiscovery.ts +724 -0
  53. package/src/core/providerRuntime/claudeCodeDiscoveryDebug.ts +55 -0
  54. package/src/core/providerRuntime/codexaCupy.ts +97 -0
  55. package/src/core/providerRuntime/codexaNative.ts +425 -0
  56. package/src/core/providerRuntime/contextMetadata.ts +397 -0
  57. package/src/core/providerRuntime/gemini.ts +789 -0
  58. package/src/core/providerRuntime/lmstudio.ts +118 -0
  59. package/src/core/providerRuntime/local.ts +770 -0
  60. package/src/core/providerRuntime/localHarness/runtime.ts +1090 -0
  61. package/src/core/providerRuntime/localOutputBudget.ts +17 -0
  62. package/src/core/providerRuntime/mistralVibe.ts +667 -0
  63. package/src/core/providerRuntime/models.ts +175 -0
  64. package/src/core/providerRuntime/reasoning.ts +20 -0
  65. package/src/core/providerRuntime/registry.ts +286 -0
  66. package/src/core/providerRuntime/types.ts +142 -0
  67. package/src/core/providerRuntime/unsloth.ts +216 -0
  68. package/src/core/providers/codexJsonStream.ts +305 -0
  69. package/src/core/providers/codexSubprocess.ts +378 -0
  70. package/src/core/providers/codexTranscript.ts +695 -0
  71. package/src/core/providers/openaiNative.ts +13 -0
  72. package/src/core/providers/registry.ts +21 -0
  73. package/src/core/providers/types.ts +91 -0
  74. package/src/core/shared/attachments.ts +101 -0
  75. package/src/core/shared/cleanupFastFail.ts +67 -0
  76. package/src/core/shared/clipboard.ts +24 -0
  77. package/src/core/shared/clipboardImage.ts +111 -0
  78. package/src/core/shared/githubDiagnostics.ts +222 -0
  79. package/src/core/shared/hollowResponseFormat.ts +39 -0
  80. package/src/core/terminal/clearFrameBoundary.ts +852 -0
  81. package/src/core/terminal/frameLock.ts +110 -0
  82. package/src/core/terminal/inkRenderReset.ts +123 -0
  83. package/src/core/terminal/startupClear.ts +20 -0
  84. package/src/core/terminal/terminalCapabilities.ts +100 -0
  85. package/src/core/terminal/terminalControl.ts +169 -0
  86. package/src/core/terminal/terminalSanitize.ts +147 -0
  87. package/src/core/terminal/terminalTitle.ts +400 -0
  88. package/src/core/version/channel.ts +27 -0
  89. package/src/core/version/packageManager.ts +119 -0
  90. package/src/core/version/updateCheck.ts +203 -0
  91. package/src/core/workspace/appData.ts +107 -0
  92. package/src/core/workspace/conversationStore.ts +335 -0
  93. package/src/core/workspace/launchContext.ts +259 -0
  94. package/src/core/workspace/planStorage.ts +135 -0
  95. package/src/core/workspace/projectInstructions.ts +54 -0
  96. package/src/core/workspace/scratchDir.ts +64 -0
  97. package/src/core/workspace/workspaceActivity.ts +384 -0
  98. package/src/core/workspace/workspaceGuard.ts +377 -0
  99. package/src/core/workspace/workspaceRoot.ts +47 -0
  100. package/src/exec.ts +73 -0
  101. package/src/headless/execArgs.ts +296 -0
  102. package/src/headless/execRunner.ts +304 -0
  103. package/src/index.tsx +270 -0
  104. package/src/legacyEnvBootstrap.ts +5 -0
  105. package/src/session/appSession.ts +771 -0
  106. package/src/session/chatLifecycle.ts +994 -0
  107. package/src/session/conversation.ts +107 -0
  108. package/src/session/liveRenderScheduler.ts +214 -0
  109. package/src/session/persistedResponse.ts +93 -0
  110. package/src/session/planFlow.ts +159 -0
  111. package/src/session/planTranscript.ts +19 -0
  112. package/src/session/promptRunSchedule.ts +26 -0
  113. package/src/session/types.ts +234 -0
  114. package/src/test/runtimeTestUtils.ts +14 -0
  115. package/src/types/react-dom.d.ts +3 -0
  116. package/src/ui/chrome/ActivityBars.tsx +68 -0
  117. package/src/ui/chrome/ActivityIndicator.tsx +58 -0
  118. package/src/ui/chrome/AnimatedStatusText.tsx +69 -0
  119. package/src/ui/chrome/AppShell.tsx +474 -0
  120. package/src/ui/chrome/BottomComposer.tsx +1135 -0
  121. package/src/ui/chrome/DashCard.tsx +82 -0
  122. package/src/ui/chrome/RunFooter.tsx +65 -0
  123. package/src/ui/chrome/RuntimeStatusBar.tsx +108 -0
  124. package/src/ui/chrome/Spinner.tsx +25 -0
  125. package/src/ui/chrome/TopHeader.tsx +439 -0
  126. package/src/ui/chrome/UpdateAvailableCard.tsx +42 -0
  127. package/src/ui/chrome/busyStatusAnimation.ts +11 -0
  128. package/src/ui/input/commandNormalize.ts +66 -0
  129. package/src/ui/input/focus.ts +73 -0
  130. package/src/ui/input/imageAttachments.ts +18 -0
  131. package/src/ui/input/inputBuffer.ts +203 -0
  132. package/src/ui/input/pastedContent.ts +75 -0
  133. package/src/ui/input/rawArrowKeys.ts +28 -0
  134. package/src/ui/input/slashCommands.ts +43 -0
  135. package/src/ui/input/useStdinRawModeLease.ts +23 -0
  136. package/src/ui/layout.ts +560 -0
  137. package/src/ui/panels/AttachmentImportPanel.tsx +131 -0
  138. package/src/ui/panels/AuthPanel.tsx +149 -0
  139. package/src/ui/panels/BackendPicker.tsx +28 -0
  140. package/src/ui/panels/ModePicker.tsx +31 -0
  141. package/src/ui/panels/ModelPicker.tsx +31 -0
  142. package/src/ui/panels/ModelPickerScreen.tsx +761 -0
  143. package/src/ui/panels/ModelReasoningPicker.tsx +458 -0
  144. package/src/ui/panels/Panel.tsx +51 -0
  145. package/src/ui/panels/PermissionsPanel.tsx +78 -0
  146. package/src/ui/panels/PlanActionPicker.tsx +187 -0
  147. package/src/ui/panels/ProviderPicker.tsx +753 -0
  148. package/src/ui/panels/ProviderSetupPrompt.tsx +52 -0
  149. package/src/ui/panels/ReasoningPicker.tsx +46 -0
  150. package/src/ui/panels/ResumePicker.tsx +90 -0
  151. package/src/ui/panels/SelectionPanel.tsx +138 -0
  152. package/src/ui/panels/SettingsPanel.tsx +156 -0
  153. package/src/ui/panels/TextEntryPanel.tsx +139 -0
  154. package/src/ui/panels/ThemePicker.tsx +32 -0
  155. package/src/ui/panels/ToolApprovalPanel.tsx +46 -0
  156. package/src/ui/panels/UpdatePromptPanel.tsx +236 -0
  157. package/src/ui/panels/responsivePickerViewport.ts +64 -0
  158. package/src/ui/render/Markdown.tsx +331 -0
  159. package/src/ui/render/diffRenderer.ts +116 -0
  160. package/src/ui/render/logoVariants.ts +113 -0
  161. package/src/ui/render/modeDisplay.ts +52 -0
  162. package/src/ui/render/outputPipeline.ts +64 -0
  163. package/src/ui/render/runtimeDisplay.ts +128 -0
  164. package/src/ui/render/terminalAnswerFormat.ts +128 -0
  165. package/src/ui/render/textLayout.ts +392 -0
  166. package/src/ui/theme.tsx +274 -0
  167. package/src/ui/themeFlow.ts +41 -0
  168. package/src/ui/timeline/ActionRequiredBlock.tsx +38 -0
  169. package/src/ui/timeline/AgentBlock.tsx +130 -0
  170. package/src/ui/timeline/StaticIntroItem.tsx +54 -0
  171. package/src/ui/timeline/ThinkingBlock.tsx +100 -0
  172. package/src/ui/timeline/Timeline.tsx +1410 -0
  173. package/src/ui/timeline/TranscriptShell.tsx +302 -0
  174. package/src/ui/timeline/TurnGroup.tsx +673 -0
  175. package/src/ui/timeline/layoutListWindow.ts +145 -0
  176. package/src/ui/timeline/liveViewportWindow.ts +68 -0
  177. package/src/ui/timeline/progressEntries.ts +156 -0
  178. package/src/ui/timeline/runActivityView.ts +37 -0
  179. package/src/ui/timeline/staticTranscriptCache.ts +174 -0
  180. package/src/ui/timeline/streamCoalesce.ts +53 -0
  181. package/src/ui/timeline/timelineMeasure.ts +3273 -0
  182. package/src/ui/useThrottledValue.ts +31 -0
@@ -0,0 +1,203 @@
1
+ import type { WrappedTextRow } from "../render/textLayout.js";
2
+ import { getTextWidth, normalizeLineBreaks, wrapTextRows } from "../render/textLayout.js";
3
+ import { sanitizeTerminalInput } from "../../core/terminal/terminalSanitize.js";
4
+
5
+ export type WrappedInputRow = WrappedTextRow;
6
+
7
+ export interface InputViewport {
8
+ rows: WrappedInputRow[];
9
+ visibleRows: WrappedInputRow[];
10
+ cursorRow: number;
11
+ cursorColumn: number;
12
+ scrollRow: number;
13
+ }
14
+
15
+ function isHighSurrogate(code: number): boolean {
16
+ return code >= 0xd800 && code <= 0xdbff;
17
+ }
18
+
19
+ function isLowSurrogate(code: number): boolean {
20
+ return code >= 0xdc00 && code <= 0xdfff;
21
+ }
22
+
23
+ const LEAKED_SGR_MOUSE_PATTERN = /(?:\x1b)?\[<\d+;\d+;\d+[Mm]/g;
24
+
25
+ export function normalizeInputText(text: string): string {
26
+ if (!text) return "";
27
+ // Strip out leaked SGR mouse coordinates that might have been emitted by
28
+ // the terminal without their ESC prefix (swallowed by readline).
29
+ const withoutMouse = text.replace(LEAKED_SGR_MOUSE_PATTERN, "");
30
+ return normalizeLineBreaks(sanitizeTerminalInput(withoutMouse));
31
+ }
32
+
33
+ export function stripMouseEscapes(input: string): string {
34
+ return input.replace(LEAKED_SGR_MOUSE_PATTERN, "");
35
+ }
36
+
37
+ export function normalizeCursorOffset(text: string, cursorOffset: number): number {
38
+ const safeCursor = Math.max(0, Math.min(cursorOffset, text.length));
39
+
40
+ if (
41
+ safeCursor > 0
42
+ && safeCursor < text.length
43
+ && isHighSurrogate(text.charCodeAt(safeCursor - 1))
44
+ && isLowSurrogate(text.charCodeAt(safeCursor))
45
+ ) {
46
+ return safeCursor - 1;
47
+ }
48
+
49
+ return safeCursor;
50
+ }
51
+
52
+ // ─── Cursor movement ─────────────────────────────────────────────────────────
53
+
54
+ export function moveCursorLeft(text: string, cursorOffset: number): number {
55
+ const safeCursor = normalizeCursorOffset(text, cursorOffset);
56
+ if (safeCursor <= 0) return 0;
57
+
58
+ let nextCursor = safeCursor - 1;
59
+ if (nextCursor > 0 && isLowSurrogate(text.charCodeAt(nextCursor)) && isHighSurrogate(text.charCodeAt(nextCursor - 1))) {
60
+ nextCursor -= 1;
61
+ }
62
+
63
+ return nextCursor;
64
+ }
65
+
66
+ export function moveCursorRight(text: string, cursorOffset: number): number {
67
+ const safeCursor = normalizeCursorOffset(text, cursorOffset);
68
+ if (safeCursor >= text.length) return text.length;
69
+
70
+ if (
71
+ isHighSurrogate(text.charCodeAt(safeCursor))
72
+ && safeCursor + 1 < text.length
73
+ && isLowSurrogate(text.charCodeAt(safeCursor + 1))
74
+ ) {
75
+ return safeCursor + 2;
76
+ }
77
+
78
+ return safeCursor + 1;
79
+ }
80
+
81
+ // ─── Text mutations ───────────────────────────────────────────────────────────
82
+
83
+ export function insertInputText(params: {
84
+ value: string;
85
+ cursorOffset: number;
86
+ text: string;
87
+ }): { value: string; cursorOffset: number } {
88
+ const value = normalizeInputText(params.value);
89
+ const safeCursor = normalizeCursorOffset(value, params.cursorOffset);
90
+ const insertedText = normalizeInputText(params.text);
91
+ return {
92
+ value: value.slice(0, safeCursor) + insertedText + value.slice(safeCursor),
93
+ cursorOffset: safeCursor + insertedText.length,
94
+ };
95
+ }
96
+
97
+ export function deleteInputBackward(params: {
98
+ value: string;
99
+ cursorOffset: number;
100
+ }): { value: string; cursorOffset: number } {
101
+ const value = normalizeInputText(params.value);
102
+ const safeCursor = normalizeCursorOffset(value, params.cursorOffset);
103
+
104
+ if (safeCursor <= 0) {
105
+ return { value, cursorOffset: 0 };
106
+ }
107
+
108
+ const previousCursor = moveCursorLeft(value, safeCursor);
109
+ return {
110
+ value: value.slice(0, previousCursor) + value.slice(safeCursor),
111
+ cursorOffset: previousCursor,
112
+ };
113
+ }
114
+
115
+ export function deleteInputForward(params: {
116
+ value: string;
117
+ cursorOffset: number;
118
+ }): { value: string; cursorOffset: number } {
119
+ const value = normalizeInputText(params.value);
120
+ const safeCursor = normalizeCursorOffset(value, params.cursorOffset);
121
+
122
+ if (safeCursor >= value.length) {
123
+ return { value, cursorOffset: safeCursor };
124
+ }
125
+
126
+ const nextCursor = moveCursorRight(value, safeCursor);
127
+ return {
128
+ value: value.slice(0, safeCursor) + value.slice(nextCursor),
129
+ cursorOffset: safeCursor,
130
+ };
131
+ }
132
+
133
+ // ─── Viewport ────────────────────────────────────────────────────────────────
134
+
135
+ export function wrapInputRows(text: string, width: number): WrappedInputRow[] {
136
+ return wrapTextRows(normalizeInputText(text), width);
137
+ }
138
+
139
+ export function locateCursor(rows: WrappedInputRow[], cursorOffset: number) {
140
+ const safeCursor = Math.max(0, cursorOffset);
141
+
142
+ for (let index = 0; index < rows.length; index += 1) {
143
+ const row = rows[index]!;
144
+ if (safeCursor <= row.end) {
145
+ const relative = Math.max(0, Math.min(safeCursor, row.end) - row.start);
146
+ return {
147
+ row: index,
148
+ column: getTextWidth(row.text.slice(0, relative)),
149
+ };
150
+ }
151
+ }
152
+
153
+ const lastRow = rows[rows.length - 1]!;
154
+ return {
155
+ row: rows.length - 1,
156
+ column: getTextWidth(lastRow.text),
157
+ };
158
+ }
159
+
160
+ export function clampScrollToCursor(
161
+ scrollRow: number,
162
+ cursorRow: number,
163
+ visibleRowCount: number,
164
+ totalRowCount = cursorRow + 1,
165
+ ): number {
166
+ const safeVisibleRows = Math.max(1, visibleRowCount);
167
+ const maxScroll = Math.max(0, totalRowCount - safeVisibleRows);
168
+ let nextScrollRow = Math.max(0, Math.min(scrollRow, maxScroll));
169
+
170
+ if (cursorRow < nextScrollRow) {
171
+ nextScrollRow = cursorRow;
172
+ }
173
+
174
+ if (cursorRow >= nextScrollRow + safeVisibleRows) {
175
+ nextScrollRow = cursorRow - safeVisibleRows + 1;
176
+ }
177
+
178
+ return Math.max(0, Math.min(nextScrollRow, maxScroll));
179
+ }
180
+
181
+ export function createInputViewport(params: {
182
+ text: string;
183
+ cursorOffset: number;
184
+ width: number;
185
+ maxVisibleRows: number;
186
+ scrollRow?: number;
187
+ }): InputViewport {
188
+ const rows = wrapInputRows(params.text, params.width);
189
+ const cursor = locateCursor(rows, params.cursorOffset);
190
+ const nextScrollRow = clampScrollToCursor(params.scrollRow ?? 0, cursor.row, params.maxVisibleRows, rows.length);
191
+
192
+ return {
193
+ rows,
194
+ visibleRows: rows.slice(nextScrollRow, nextScrollRow + Math.max(1, params.maxVisibleRows)),
195
+ cursorRow: cursor.row,
196
+ cursorColumn: cursor.column,
197
+ scrollRow: nextScrollRow,
198
+ };
199
+ }
200
+
201
+ export function getComposerBodyWidth(totalWidth: number): number {
202
+ return Math.max(4, totalWidth - 4);
203
+ }
@@ -0,0 +1,75 @@
1
+ export const LARGE_PASTE_THRESHOLD = 1_000;
2
+
3
+ export const PASTED_CONTENT_PATTERN = /\[Pasted Content ([\d,]+) chars\](?:\u2063[\uFE00-\uFE09]+\u2063)?/g;
4
+ export const IMAGE_ATTACHMENT_PATTERN = /\[Image: ([^\]\n]+)\](?:\u2063[\uFE00-\uFE09]+\u2063)?/g;
5
+ const ATOMIC_CONTENT_PATTERN = /(?:\[Pasted Content [\d,]+ chars\]|\[Image: [^\]\n]+\])(?:\u2063[\uFE00-\uFE09]+\u2063)?/g;
6
+
7
+ let nextPasteId = 1;
8
+
9
+ export function countCharacters(value: string): number {
10
+ return Array.from(value).length;
11
+ }
12
+
13
+ export function createPastedContentLabel(value: string): string {
14
+ return `[Pasted Content ${countCharacters(value).toLocaleString("en-US")} chars]`;
15
+ }
16
+
17
+ function encodeInvisibleId(value: number): string {
18
+ return String(value).split("").map((digit) => String.fromCharCode(0xFE00 + Number(digit))).join("");
19
+ }
20
+
21
+ export function createAtomicContentToken(label: string): string {
22
+ const id = encodeInvisibleId(nextPasteId++);
23
+ return `${label}\u2063${id}\u2063`;
24
+ }
25
+
26
+ export function createPastedContentToken(value: string): string {
27
+ return createAtomicContentToken(createPastedContentLabel(value));
28
+ }
29
+
30
+ export function isLargePaste(value: string): boolean {
31
+ return countCharacters(value) >= LARGE_PASTE_THRESHOLD;
32
+ }
33
+
34
+ export type PastedContentRegistry = Map<string, string[]>;
35
+
36
+ export function expandPastedContent(value: string, registry: PastedContentRegistry): string {
37
+ const offsets = new Map<string, number>();
38
+ return value.replace(PASTED_CONTENT_PATTERN, (label) => {
39
+ const values = registry.get(label);
40
+ const offset = offsets.get(label) ?? 0;
41
+ offsets.set(label, offset + 1);
42
+ return values?.[offset] ?? label;
43
+ });
44
+ }
45
+
46
+ export function findPastedContentSpan(value: string, cursor: number) {
47
+ ATOMIC_CONTENT_PATTERN.lastIndex = 0;
48
+ for (const match of value.matchAll(ATOMIC_CONTENT_PATTERN)) {
49
+ const start = match.index ?? 0;
50
+ const end = start + match[0].length;
51
+ if (cursor >= start && cursor <= end) return { start, end };
52
+ }
53
+ return null;
54
+ }
55
+
56
+ export function moveAcrossPastedContent(value: string, cursor: number, direction: "left" | "right"): number | null {
57
+ const span = findPastedContentSpan(value, cursor);
58
+ if (!span) return null;
59
+ if (direction === "left" && cursor > span.start) return span.start;
60
+ if (direction === "right" && cursor < span.end) return span.end;
61
+ return null;
62
+ }
63
+
64
+ export function deleteAdjacentPastedContent(value: string, cursor: number, direction: "backward" | "forward") {
65
+ ATOMIC_CONTENT_PATTERN.lastIndex = 0;
66
+ for (const match of value.matchAll(ATOMIC_CONTENT_PATTERN)) {
67
+ const start = match.index ?? 0;
68
+ const end = start + match[0].length;
69
+ const adjacent = direction === "backward" ? cursor === end : cursor === start;
70
+ if (adjacent || (cursor > start && cursor < end)) {
71
+ return { value: value.slice(0, start) + value.slice(end), cursorOffset: start };
72
+ }
73
+ }
74
+ return null;
75
+ }
@@ -0,0 +1,28 @@
1
+ export type ArrowDirection = "up" | "down" | "left" | "right";
2
+ export type HorizontalArrowDirection = "left" | "right";
3
+
4
+ const ESC = String.fromCharCode(27);
5
+
6
+ const FINAL_BYTE_DIRECTIONS: Record<string, ArrowDirection> = {
7
+ A: "up",
8
+ B: "down",
9
+ C: "right",
10
+ D: "left",
11
+ };
12
+
13
+ // CSI form (`ESC [ C`, `ESC [ 1;5C`) and SS3/application-cursor form (`ESC O C`).
14
+ // Terminals emit either depending on DECCKM, and modifier presses add parameters.
15
+ // Kitty encodes arrows the same way; its CSI-u codepoint space does not cover them.
16
+ const ARROW_PATTERN = new RegExp(`${ESC}(?:\\[[0-9;]*|O)([ABCD])`);
17
+
18
+ /** Resolve an arrow key from a raw stdin chunk, or null when it holds none. */
19
+ export function getArrowDirection(raw: string): ArrowDirection | null {
20
+ const finalByte = ARROW_PATTERN.exec(raw)?.[1];
21
+ return finalByte ? FINAL_BYTE_DIRECTIONS[finalByte] ?? null : null;
22
+ }
23
+
24
+ /** Horizontal-only variant for menus that ignore vertical movement. */
25
+ export function getHorizontalArrowDirection(raw: string): HorizontalArrowDirection | null {
26
+ const direction = getArrowDirection(raw);
27
+ return direction === "left" || direction === "right" ? direction : null;
28
+ }
@@ -0,0 +1,43 @@
1
+ export interface SlashCommandSuggestion {
2
+ cmd: string;
3
+ desc: string;
4
+ aliases?: readonly string[];
5
+ }
6
+
7
+ export const SLASH_COMMANDS = [
8
+ { cmd: "/help", desc: "Show available commands" },
9
+ { cmd: "/clear", desc: "Clear chat and cancel active run" },
10
+ { cmd: "/resume", desc: "Resume a previous conversation" },
11
+ { cmd: "/providers", desc: "Open AI provider picker", aliases: ["/provider"] },
12
+ { cmd: "/model", desc: "Change active model" },
13
+ { cmd: "/models", desc: "Open model picker" },
14
+ { cmd: "/mode", desc: "Change execution mode" },
15
+ { cmd: "/route", desc: "Show active chat route" },
16
+ { cmd: "/backend", desc: "Change active backend" },
17
+ { cmd: "/reasoning", desc: "Change reasoning level" },
18
+ { cmd: "/plan", desc: "Show or toggle session plan mode" },
19
+ { cmd: "/settings", desc: "Open the settings picker", aliases: ["/setting"] },
20
+ { cmd: "/status", desc: "Show effective runtime configuration" },
21
+ { cmd: "/permissions", desc: "Inspect or update permissions and sandbox controls" },
22
+ { cmd: "/runtime", desc: "Compatibility runtime policy controls" },
23
+ { cmd: "/themes", desc: "Open visual theme picker" },
24
+ { cmd: "/verbose", desc: "Toggle verbose mode (detailed processing info)" },
25
+ { cmd: "/auth", desc: "Manage authentication" },
26
+ { cmd: "/workspace", desc: "Show the locked workspace" },
27
+ { cmd: "/copy", desc: "Copy the full conversation transcript to clipboard" },
28
+ { cmd: "/paste-image", desc: "Attach the image currently on the clipboard" },
29
+ { cmd: "/update", desc: "Check for updates and install the latest Ubume" },
30
+ { cmd: "/exit", desc: "Quit the application" },
31
+ ] as const satisfies readonly SlashCommandSuggestion[];
32
+
33
+ export type CommandSuggestion = (typeof SLASH_COMMANDS)[number];
34
+
35
+ function matchesPrefix(suggestion: SlashCommandSuggestion, prefix: string): boolean {
36
+ if (suggestion.cmd.startsWith(prefix)) return true;
37
+ return suggestion.aliases?.some((alias) => alias.startsWith(prefix)) ?? false;
38
+ }
39
+
40
+ export function getSlashCommandSuggestions(prefix: string): readonly CommandSuggestion[] {
41
+ const normalized = prefix.toLowerCase();
42
+ return SLASH_COMMANDS.filter((command) => matchesPrefix(command, normalized)).slice(0, 5);
43
+ }
@@ -0,0 +1,23 @@
1
+ import { useEffect } from "react";
2
+ import { useStdin } from "ink";
3
+
4
+ /**
5
+ * Hold one raw-mode reference for the lifetime of the mounted tree.
6
+ *
7
+ * Ink refcounts raw-mode consumers (useFocus / active useInput). When the last
8
+ * consumer unmounts and a replacement mounts in the same commit — e.g. the
9
+ * composer moving between AppShell and TranscriptShell on overlay exit — Ink
10
+ * removes and re-adds its 'readable' stdin listener. Two such cycles in one
11
+ * tick, combined with a raw 'data' listener (BottomComposer's key sniffer),
12
+ * make Node/Bun streams switch stdin to flowing mode, after which 'readable'
13
+ * never fires and Ink stops receiving input entirely (no keys, no Ctrl+C).
14
+ * Keeping the count above zero at the root prevents the listener churn.
15
+ */
16
+ export function useStdinRawModeLease(): void {
17
+ const { isRawModeSupported, setRawMode } = useStdin();
18
+ useEffect(() => {
19
+ if (!isRawModeSupported) return;
20
+ setRawMode(true);
21
+ return () => setRawMode(false);
22
+ }, [isRawModeSupported, setRawMode]);
23
+ }