@bubblebrain-ai/bubble 0.0.24 → 0.0.25
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 +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +22 -6
- package/dist/goal/format.js +34 -4
- package/dist/goal/store.d.ts +3 -0
- package/dist/goal/store.js +14 -1
- package/dist/goal/usage.d.ts +2 -0
- package/dist/goal/usage.js +3 -0
- package/dist/main.js +23 -42
- package/dist/provider.js +20 -5
- package/dist/tui/detect-theme.d.ts +1 -0
- package/dist/tui/detect-theme.js +23 -0
- package/dist/tui/image-display.d.ts +13 -0
- package/dist/tui/image-display.js +49 -0
- package/dist/tui/input-history.d.ts +37 -6
- package/dist/tui/input-history.js +194 -23
- package/dist/tui/model-switch.d.ts +42 -0
- package/dist/tui/model-switch.js +55 -0
- package/dist/tui-ink/app.d.ts +32 -2
- package/dist/tui-ink/app.js +1360 -522
- package/dist/tui-ink/approval/select.js +10 -0
- package/dist/tui-ink/detect-theme.d.ts +1 -2
- package/dist/tui-ink/detect-theme.js +1 -87
- package/dist/tui-ink/display-history.d.ts +1 -0
- package/dist/tui-ink/display-history.js +11 -0
- package/dist/tui-ink/feedback-dialog.js +10 -0
- package/dist/tui-ink/feishu-setup-picker.js +10 -0
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +8 -2
- package/dist/tui-ink/input-box.d.ts +70 -9
- package/dist/tui-ink/input-box.js +354 -120
- package/dist/tui-ink/input-history.d.ts +1 -16
- package/dist/tui-ink/input-history.js +1 -79
- package/dist/tui-ink/input-queue.d.ts +12 -0
- package/dist/tui-ink/input-queue.js +17 -0
- package/dist/tui-ink/key-events.d.ts +9 -0
- package/dist/tui-ink/key-events.js +8 -0
- package/dist/tui-ink/markdown.js +1 -1
- package/dist/tui-ink/message-list.d.ts +3 -1
- package/dist/tui-ink/message-list.js +42 -24
- package/dist/tui-ink/model-picker.d.ts +24 -2
- package/dist/tui-ink/model-picker.js +224 -20
- package/dist/tui-ink/plan-confirm.js +10 -0
- package/dist/tui-ink/question-dialog.js +10 -0
- package/dist/tui-ink/run.d.ts +10 -1
- package/dist/tui-ink/run.js +21 -28
- package/dist/tui-ink/session-picker.js +3 -0
- package/dist/tui-ink/submit-dedupe.d.ts +5 -0
- package/dist/tui-ink/submit-dedupe.js +25 -0
- package/dist/tui-ink/terminal-mouse.d.ts +13 -1
- package/dist/tui-ink/terminal-mouse.js +63 -21
- package/dist/tui-ink/theme.d.ts +6 -3
- package/dist/tui-ink/theme.js +10 -4
- package/dist/tui-ink/transcript-input.d.ts +8 -0
- package/dist/tui-ink/transcript-input.js +9 -0
- package/dist/tui-ink/transcript-viewport-math.d.ts +1 -2
- package/dist/tui-ink/transcript-viewport-math.js +1 -2
- package/dist/tui-ink/welcome.d.ts +1 -0
- package/dist/tui-ink/welcome.js +25 -28
- package/package.json +1 -5
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -53
- package/dist/tui/escape-confirmation.d.ts +0 -15
- package/dist/tui/escape-confirmation.js +0 -30
- package/dist/tui/global-key-router.d.ts +0 -3
- package/dist/tui/global-key-router.js +0 -87
- package/dist/tui/markdown-inline.d.ts +0 -22
- package/dist/tui/markdown-inline.js +0 -68
- package/dist/tui/markdown-theme-rules.d.ts +0 -23
- package/dist/tui/markdown-theme-rules.js +0 -164
- package/dist/tui/markdown-theme.d.ts +0 -5
- package/dist/tui/markdown-theme.js +0 -27
- package/dist/tui/opencode-spinner.d.ts +0 -22
- package/dist/tui/opencode-spinner.js +0 -216
- package/dist/tui/prompt-keybindings.d.ts +0 -42
- package/dist/tui/prompt-keybindings.js +0 -35
- package/dist/tui/render-signature.d.ts +0 -1
- package/dist/tui/render-signature.js +0 -7
- package/dist/tui/run.d.ts +0 -67
- package/dist/tui/run.js +0 -10166
- package/dist/tui/sidebar-mcp.d.ts +0 -31
- package/dist/tui/sidebar-mcp.js +0 -62
- package/dist/tui/sidebar-state.d.ts +0 -12
- package/dist/tui/sidebar-state.js +0 -69
- package/dist/tui/streaming-tool-args.d.ts +0 -15
- package/dist/tui/streaming-tool-args.js +0 -30
- package/dist/tui/tool-renderers/fallback.d.ts +0 -2
- package/dist/tui/tool-renderers/fallback.js +0 -75
- package/dist/tui/tool-renderers/registry.d.ts +0 -3
- package/dist/tui/tool-renderers/registry.js +0 -11
- package/dist/tui/tool-renderers/subagent.d.ts +0 -2
- package/dist/tui/tool-renderers/subagent.js +0 -135
- package/dist/tui/tool-renderers/types.d.ts +0 -36
- package/dist/tui/tool-renderers/types.js +0 -1
- package/dist/tui/tool-renderers/write-preview.d.ts +0 -12
- package/dist/tui/tool-renderers/write-preview.js +0 -32
- package/dist/tui/tool-renderers/write.d.ts +0 -6
- package/dist/tui/tool-renderers/write.js +0 -88
- package/dist/tui-opentui/app.d.ts +0 -54
- package/dist/tui-opentui/app.js +0 -1371
- package/dist/tui-opentui/approval/approval-dialog.d.ts +0 -15
- package/dist/tui-opentui/approval/approval-dialog.js +0 -155
- package/dist/tui-opentui/approval/diff-view.d.ts +0 -9
- package/dist/tui-opentui/approval/diff-view.js +0 -43
- package/dist/tui-opentui/approval/select.d.ts +0 -37
- package/dist/tui-opentui/approval/select.js +0 -91
- package/dist/tui-opentui/detect-theme.d.ts +0 -2
- package/dist/tui-opentui/detect-theme.js +0 -87
- package/dist/tui-opentui/display-history.d.ts +0 -56
- package/dist/tui-opentui/display-history.js +0 -130
- package/dist/tui-opentui/edit-diff.d.ts +0 -11
- package/dist/tui-opentui/edit-diff.js +0 -57
- package/dist/tui-opentui/feedback-dialog.d.ts +0 -21
- package/dist/tui-opentui/feedback-dialog.js +0 -164
- package/dist/tui-opentui/feishu-setup-picker.d.ts +0 -7
- package/dist/tui-opentui/feishu-setup-picker.js +0 -272
- package/dist/tui-opentui/file-mentions.d.ts +0 -29
- package/dist/tui-opentui/file-mentions.js +0 -174
- package/dist/tui-opentui/footer.d.ts +0 -26
- package/dist/tui-opentui/footer.js +0 -40
- package/dist/tui-opentui/image-paste.d.ts +0 -54
- package/dist/tui-opentui/image-paste.js +0 -288
- package/dist/tui-opentui/input-box.d.ts +0 -32
- package/dist/tui-opentui/input-box.js +0 -462
- package/dist/tui-opentui/input-history.d.ts +0 -16
- package/dist/tui-opentui/input-history.js +0 -79
- package/dist/tui-opentui/markdown.d.ts +0 -66
- package/dist/tui-opentui/markdown.js +0 -127
- package/dist/tui-opentui/message-list.d.ts +0 -31
- package/dist/tui-opentui/message-list.js +0 -131
- package/dist/tui-opentui/model-picker.d.ts +0 -63
- package/dist/tui-opentui/model-picker.js +0 -450
- package/dist/tui-opentui/plan-confirm.d.ts +0 -9
- package/dist/tui-opentui/plan-confirm.js +0 -124
- package/dist/tui-opentui/question-dialog.d.ts +0 -10
- package/dist/tui-opentui/question-dialog.js +0 -110
- package/dist/tui-opentui/recent-activity.d.ts +0 -8
- package/dist/tui-opentui/recent-activity.js +0 -71
- package/dist/tui-opentui/run-session-picker.d.ts +0 -10
- package/dist/tui-opentui/run-session-picker.js +0 -28
- package/dist/tui-opentui/run.d.ts +0 -38
- package/dist/tui-opentui/run.js +0 -48
- package/dist/tui-opentui/session-picker.d.ts +0 -12
- package/dist/tui-opentui/session-picker.js +0 -120
- package/dist/tui-opentui/theme.d.ts +0 -89
- package/dist/tui-opentui/theme.js +0 -157
- package/dist/tui-opentui/todos.d.ts +0 -9
- package/dist/tui-opentui/todos.js +0 -45
- package/dist/tui-opentui/trace-groups.d.ts +0 -27
- package/dist/tui-opentui/trace-groups.js +0 -455
- package/dist/tui-opentui/use-terminal-size.d.ts +0 -4
- package/dist/tui-opentui/use-terminal-size.js +0 -5
- package/dist/tui-opentui/welcome.d.ts +0 -25
- package/dist/tui-opentui/welcome.js +0 -77
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { Box, Text, useCursor, useInput, usePaste, useStdout } from "ink";
|
|
4
4
|
import stringWidth from "string-width";
|
|
@@ -7,12 +7,16 @@ import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
|
7
7
|
import { useTheme } from "./theme.js";
|
|
8
8
|
import { filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
9
9
|
import { bareImageFilenameFromPaste, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
10
|
-
import { appendHistoryEntry,
|
|
10
|
+
import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
|
|
11
|
+
import { isKeyReleaseEvent } from "./key-events.js";
|
|
11
12
|
import { stripTerminalMouseSequences } from "./terminal-mouse.js";
|
|
13
|
+
import { submitPayloadFingerprint } from "./submit-dedupe.js";
|
|
12
14
|
export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
|
|
13
15
|
import { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
|
|
16
|
+
import { imageDisplayLabel } from "../tui/image-display.js";
|
|
14
17
|
const MIN_VISIBLE_LINES = 3;
|
|
15
18
|
const MAX_VISIBLE_LINES = 6;
|
|
19
|
+
const CURSOR_BLINK_INTERVAL_MS = 530;
|
|
16
20
|
const PADDING_X = 1;
|
|
17
21
|
const PROMPT = " > ";
|
|
18
22
|
const MAX_VISIBLE_SUGGESTIONS = 8;
|
|
@@ -37,15 +41,40 @@ export function resolveCursorRowCompensation(input) {
|
|
|
37
41
|
export function isCtrlCInput(input, key) {
|
|
38
42
|
return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
|
|
39
43
|
}
|
|
44
|
+
export function shouldUseLineComposerFrame(_background) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
export function shouldUseHardwareComposerCursor(env = process.env) {
|
|
48
|
+
return env.BUBBLE_HARDWARE_CURSOR === "1";
|
|
49
|
+
}
|
|
50
|
+
export function composerVerticalArrowDirection(key) {
|
|
51
|
+
if (key.upArrow)
|
|
52
|
+
return "up";
|
|
53
|
+
if (key.downArrow)
|
|
54
|
+
return "down";
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
export function resolveSoftwareCursorCellStyle(input) {
|
|
58
|
+
if (input.visible) {
|
|
59
|
+
return {
|
|
60
|
+
backgroundColor: input.cursorBackground,
|
|
61
|
+
color: input.cursorForeground,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
backgroundColor: input.rowBackground,
|
|
66
|
+
color: input.textColor,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
40
69
|
/**
|
|
41
70
|
* Split a composer line around the cursor so the cell under it can render as
|
|
42
71
|
* an inverse-video software cursor. The visible cursor must not depend on the
|
|
43
72
|
* real terminal cursor: Ink only re-arms its one-shot cursor escape when the
|
|
44
73
|
* component owning useCursor re-commits, so frames produced by other
|
|
45
74
|
* components' local state (the waiting spinner, viewport scrolling) hide the
|
|
46
|
-
* hardware cursor for most of an agent run. Drawing the cell
|
|
47
|
-
*
|
|
48
|
-
* positioned for IME anchoring.
|
|
75
|
+
* hardware cursor for most of an agent run. Drawing and blinking the cell
|
|
76
|
+
* ourselves keeps it visible while preserving normal typing feedback; the real
|
|
77
|
+
* cursor is still positioned for IME anchoring.
|
|
49
78
|
*/
|
|
50
79
|
export function splitLineAtCursor(lineText, charOffset) {
|
|
51
80
|
const offset = Math.max(0, Math.min(charOffset, lineText.length));
|
|
@@ -132,6 +161,52 @@ function visualToCursor(visualLines, row, col) {
|
|
|
132
161
|
}
|
|
133
162
|
return vl.absStart + charOffset;
|
|
134
163
|
}
|
|
164
|
+
export function resolveSlashCommandHighlightRange(input, commandNames) {
|
|
165
|
+
if (!input.startsWith("/"))
|
|
166
|
+
return null;
|
|
167
|
+
const match = /^\/([^\s]+)/.exec(input);
|
|
168
|
+
if (!match)
|
|
169
|
+
return null;
|
|
170
|
+
const commandName = match[1]?.toLowerCase();
|
|
171
|
+
if (!commandName)
|
|
172
|
+
return null;
|
|
173
|
+
for (const name of commandNames) {
|
|
174
|
+
if (name.toLowerCase() === commandName) {
|
|
175
|
+
return { start: 0, end: match[0].length };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
function splitHighlightedText(text, absStart, highlight) {
|
|
181
|
+
if (!text)
|
|
182
|
+
return [];
|
|
183
|
+
if (!highlight)
|
|
184
|
+
return [{ kind: "normal", text }];
|
|
185
|
+
const start = Math.max(0, highlight.start - absStart);
|
|
186
|
+
const end = Math.min(text.length, highlight.end - absStart);
|
|
187
|
+
if (start >= end)
|
|
188
|
+
return [{ kind: "normal", text }];
|
|
189
|
+
const segments = [];
|
|
190
|
+
if (start > 0)
|
|
191
|
+
segments.push({ kind: "normal", text: text.slice(0, start) });
|
|
192
|
+
segments.push({ kind: "command", text: text.slice(start, end) });
|
|
193
|
+
if (end < text.length)
|
|
194
|
+
segments.push({ kind: "normal", text: text.slice(end) });
|
|
195
|
+
return segments;
|
|
196
|
+
}
|
|
197
|
+
export function splitComposerTextSegments(input) {
|
|
198
|
+
if (input.cursorOffset === undefined) {
|
|
199
|
+
return splitHighlightedText(input.text, input.absStart, input.highlight);
|
|
200
|
+
}
|
|
201
|
+
const cursorOffset = Math.max(0, Math.min(input.text.length, input.cursorOffset));
|
|
202
|
+
const cursorSegments = splitLineAtCursor(input.text, cursorOffset);
|
|
203
|
+
const cursorConsumesSource = cursorOffset < input.text.length;
|
|
204
|
+
return [
|
|
205
|
+
...splitHighlightedText(cursorSegments.before, input.absStart, input.highlight),
|
|
206
|
+
{ kind: "cursor", text: cursorSegments.at },
|
|
207
|
+
...splitHighlightedText(cursorSegments.after, input.absStart + cursorOffset + (cursorConsumesSource ? cursorSegments.at.length : 0), input.highlight),
|
|
208
|
+
];
|
|
209
|
+
}
|
|
135
210
|
export function shouldSubmitExactSlashSuggestion(input, suggestionName) {
|
|
136
211
|
if (!suggestionName)
|
|
137
212
|
return false;
|
|
@@ -171,18 +246,101 @@ export function insertNewlineAtCursor(text, cursor) {
|
|
|
171
246
|
cursor: clampedCursor + 1,
|
|
172
247
|
};
|
|
173
248
|
}
|
|
174
|
-
export function
|
|
249
|
+
export function previousWordBoundary(text, cursor) {
|
|
250
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
251
|
+
if (clampedCursor === 0)
|
|
252
|
+
return 0;
|
|
253
|
+
let index = clampedCursor - 1;
|
|
254
|
+
while (index > 0 && /\s/.test(text[index]))
|
|
255
|
+
index--;
|
|
256
|
+
while (index > 0 && !/\s/.test(text[index - 1]))
|
|
257
|
+
index--;
|
|
258
|
+
return index;
|
|
259
|
+
}
|
|
260
|
+
export function nextWordBoundary(text, cursor) {
|
|
261
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
262
|
+
if (clampedCursor === text.length)
|
|
263
|
+
return text.length;
|
|
264
|
+
let index = clampedCursor;
|
|
265
|
+
while (index < text.length && /\s/.test(text[index]))
|
|
266
|
+
index++;
|
|
267
|
+
while (index < text.length && !/\s/.test(text[index]))
|
|
268
|
+
index++;
|
|
269
|
+
return index;
|
|
270
|
+
}
|
|
271
|
+
export function lineStartBoundary(text, cursor) {
|
|
272
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
273
|
+
return text.lastIndexOf("\n", clampedCursor - 1) + 1;
|
|
274
|
+
}
|
|
275
|
+
export function lineEndBoundary(text, cursor) {
|
|
276
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
277
|
+
const lineEnd = text.indexOf("\n", clampedCursor);
|
|
278
|
+
return lineEnd === -1 ? text.length : lineEnd;
|
|
279
|
+
}
|
|
280
|
+
export function deleteToLineStart(text, cursor) {
|
|
281
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
282
|
+
const lineStart = lineStartBoundary(text, clampedCursor);
|
|
283
|
+
return {
|
|
284
|
+
text: text.slice(0, lineStart) + text.slice(clampedCursor),
|
|
285
|
+
cursor: lineStart,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
export function deleteToLineEnd(text, cursor) {
|
|
289
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
290
|
+
const lineEnd = lineEndBoundary(text, clampedCursor);
|
|
291
|
+
return {
|
|
292
|
+
text: text.slice(0, clampedCursor) + text.slice(lineEnd),
|
|
293
|
+
cursor: clampedCursor,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
export function deleteAtCursor(text, cursor) {
|
|
297
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
298
|
+
if (clampedCursor >= text.length)
|
|
299
|
+
return { text, cursor: clampedCursor };
|
|
300
|
+
return {
|
|
301
|
+
text: text.slice(0, clampedCursor) + text.slice(clampedCursor + 1),
|
|
302
|
+
cursor: clampedCursor,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
export function resolveComposerEditAction(input, key) {
|
|
306
|
+
if (key.home)
|
|
307
|
+
return "line-start";
|
|
308
|
+
if (key.end)
|
|
309
|
+
return "line-end";
|
|
310
|
+
const wordModifier = key.ctrl || key.meta;
|
|
311
|
+
if (wordModifier && key.leftArrow)
|
|
312
|
+
return "word-left";
|
|
313
|
+
if (wordModifier && key.rightArrow)
|
|
314
|
+
return "word-right";
|
|
315
|
+
const lowerInput = input.toLowerCase();
|
|
316
|
+
if ((key.ctrl && lowerInput === "a") || input === "\x01")
|
|
317
|
+
return "line-start";
|
|
318
|
+
if ((key.ctrl && lowerInput === "e") || input === "\x05")
|
|
319
|
+
return "line-end";
|
|
320
|
+
if ((key.ctrl && lowerInput === "u") || input === "\x15")
|
|
321
|
+
return "delete-line-start";
|
|
322
|
+
if ((key.ctrl && lowerInput === "k") || input === "\x0b")
|
|
323
|
+
return "delete-line-end";
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
|
|
175
327
|
const theme = useTheme();
|
|
176
328
|
const width = terminalColumns;
|
|
329
|
+
const historyScope = useMemo(() => ({ sessionFile, cwd }), [sessionFile, cwd]);
|
|
330
|
+
const hardwareCursorEnabled = shouldUseHardwareComposerCursor();
|
|
177
331
|
const [text, setText] = useState("");
|
|
178
332
|
const [cursor, setCursor] = useState(0);
|
|
333
|
+
const [softwareCursorVisible, setSoftwareCursorVisible] = useState(true);
|
|
179
334
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
180
335
|
const [projectFiles, setProjectFiles] = useState(null);
|
|
181
336
|
const [attachments, setAttachments] = useState([]);
|
|
337
|
+
const [imageLabelStartOverride, setImageLabelStartOverride] = useState(null);
|
|
182
338
|
const [pastedContentRefs, setPastedContentRefs] = useState([]);
|
|
183
|
-
const [history, setHistory] = useState(() =>
|
|
339
|
+
const [history, setHistory] = useState(() => loadHistoryEntriesSync({ scope: historyScope }));
|
|
184
340
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
185
341
|
const historyDraftRef = useRef("");
|
|
342
|
+
const historyScopeRef = useRef(historyScope);
|
|
343
|
+
const submittedPayloadFingerprintRef = useRef(null);
|
|
186
344
|
const loadingFilesRef = useRef(false);
|
|
187
345
|
const nextPastedContentIndexRef = useRef(1);
|
|
188
346
|
// Paste and the keystrokes that follow can arrive inside the same stdin chunk
|
|
@@ -192,6 +350,16 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
192
350
|
// This ref flips synchronously at paste-start and clears after the paste
|
|
193
351
|
// commit has been flushed — useInput's Enter handler bails while it's set.
|
|
194
352
|
const pastePendingRef = useRef(false);
|
|
353
|
+
historyScopeRef.current = historyScope;
|
|
354
|
+
const ensureImageLabelStart = React.useCallback(() => {
|
|
355
|
+
setImageLabelStartOverride((current) => current ?? nextImageLabelStart);
|
|
356
|
+
}, [nextImageLabelStart]);
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
setHistory(loadHistoryEntriesSync({ scope: historyScope }));
|
|
359
|
+
setHistoryIndex(null);
|
|
360
|
+
setImageLabelStartOverride(null);
|
|
361
|
+
historyDraftRef.current = "";
|
|
362
|
+
}, [historyScope]);
|
|
195
363
|
const isSlashContext = text.startsWith("/") && cursor > 0 && !text.includes("\n");
|
|
196
364
|
const slashPrefix = isSlashContext ? text.slice(1).toLowerCase() : "";
|
|
197
365
|
const atContext = useMemo(() => (isSlashContext ? null : findAtContext(text, cursor)), [text, cursor, isSlashContext]);
|
|
@@ -201,23 +369,31 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
201
369
|
loadingFilesRef.current = true;
|
|
202
370
|
listProjectFiles(cwd).then((files) => setProjectFiles(files), () => setProjectFiles([]));
|
|
203
371
|
}, [atContext, cwd, projectFiles]);
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
// terminal default on unmount so the user's shell isn't left with our
|
|
208
|
-
// choice sticking around.
|
|
372
|
+
// The rendered inverse-video cell below is the visible cursor. Keep Ink's
|
|
373
|
+
// terminal cursor hidden by default so it can't race the software cursor; the
|
|
374
|
+
// hardware cursor can be enabled for IME diagnostics with BUBBLE_HARDWARE_CURSOR=1.
|
|
209
375
|
useEffect(() => {
|
|
376
|
+
if (!hardwareCursorEnabled)
|
|
377
|
+
return;
|
|
210
378
|
if (!process.stdout.isTTY)
|
|
211
379
|
return;
|
|
212
|
-
process.stdout.write("\x1b[
|
|
380
|
+
process.stdout.write("\x1b[1 q"); // blinking block
|
|
213
381
|
return () => {
|
|
214
382
|
process.stdout.write("\x1b[0 q"); // reset to terminal default
|
|
215
383
|
};
|
|
216
|
-
}, []);
|
|
384
|
+
}, [hardwareCursorEnabled]);
|
|
217
385
|
const slashSuggestions = useMemo(() => {
|
|
218
386
|
if (!isSlashContext)
|
|
219
387
|
return [];
|
|
220
|
-
const
|
|
388
|
+
const commands = new Map();
|
|
389
|
+
for (const command of localSlashCommands) {
|
|
390
|
+
commands.set(command.name, command);
|
|
391
|
+
}
|
|
392
|
+
for (const command of slashRegistry.list()) {
|
|
393
|
+
if (!commands.has(command.name))
|
|
394
|
+
commands.set(command.name, command);
|
|
395
|
+
}
|
|
396
|
+
const commandSuggestions = [...commands.values()].map((command) => ({
|
|
221
397
|
type: "command",
|
|
222
398
|
name: command.name,
|
|
223
399
|
description: command.description,
|
|
@@ -229,7 +405,18 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
229
405
|
}));
|
|
230
406
|
const all = [...commandSuggestions, ...skillSuggestions];
|
|
231
407
|
return all.filter((item) => item.name.toLowerCase().startsWith(slashPrefix));
|
|
232
|
-
}, [isSlashContext, slashPrefix, skillRegistry]);
|
|
408
|
+
}, [isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
|
|
409
|
+
const knownSlashCommandNames = useMemo(() => {
|
|
410
|
+
const names = new Set();
|
|
411
|
+
for (const command of localSlashCommands)
|
|
412
|
+
names.add(command.name);
|
|
413
|
+
for (const command of slashRegistry.list())
|
|
414
|
+
names.add(command.name);
|
|
415
|
+
for (const skill of skillRegistry?.summaries() ?? [])
|
|
416
|
+
names.add(skill.name);
|
|
417
|
+
return names;
|
|
418
|
+
}, [skillRegistry, localSlashCommands]);
|
|
419
|
+
const slashCommandHighlight = useMemo(() => resolveSlashCommandHighlightRange(text, knownSlashCommandNames), [text, knownSlashCommandNames]);
|
|
233
420
|
const fileSuggestions = useMemo(() => {
|
|
234
421
|
if (!atContext || !projectFiles)
|
|
235
422
|
return [];
|
|
@@ -259,8 +446,9 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
259
446
|
setCursor((c) => c + insertion.length);
|
|
260
447
|
}, [cursor]);
|
|
261
448
|
const addAttachment = React.useCallback((att) => {
|
|
449
|
+
ensureImageLabelStart();
|
|
262
450
|
setAttachments((prev) => [...prev, att]);
|
|
263
|
-
}, []);
|
|
451
|
+
}, [ensureImageLabelStart]);
|
|
264
452
|
const notice = React.useCallback((msg) => {
|
|
265
453
|
onPasteNotice?.(msg);
|
|
266
454
|
}, [onPasteNotice]);
|
|
@@ -394,24 +582,38 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
394
582
|
if (expandedText.trim().length === 0 && attachments.length === 0)
|
|
395
583
|
return;
|
|
396
584
|
const deliver = target === "queue" && onQueue ? onQueue : onSubmit;
|
|
397
|
-
|
|
585
|
+
const payload = {
|
|
398
586
|
text: expandedText,
|
|
399
587
|
displayText: expandedText === submittedText ? undefined : submittedText,
|
|
400
588
|
images: attachments,
|
|
401
|
-
|
|
589
|
+
imageDisplayStart: attachments.length > 0 ? (imageLabelStartOverride ?? nextImageLabelStart) : undefined,
|
|
590
|
+
};
|
|
591
|
+
const fingerprint = submitPayloadFingerprint(payload);
|
|
592
|
+
if (submittedPayloadFingerprintRef.current === fingerprint)
|
|
593
|
+
return;
|
|
594
|
+
submittedPayloadFingerprintRef.current = fingerprint;
|
|
595
|
+
deliver(payload);
|
|
402
596
|
// A collapsed marker cannot be safely replayed from history once its
|
|
403
597
|
// in-memory paste reference is gone; skip those entries instead.
|
|
404
|
-
if (expandedText.trim().length > 0
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
598
|
+
if (expandedText === submittedText && (expandedText.trim().length > 0 || attachments.length > 0)) {
|
|
599
|
+
const historyEntry = {
|
|
600
|
+
text: expandedText,
|
|
601
|
+
images: attachments,
|
|
602
|
+
...(attachments.length > 0 ? { imageDisplayStart: imageLabelStartOverride ?? nextImageLabelStart } : {}),
|
|
603
|
+
};
|
|
604
|
+
setHistory((current) => {
|
|
605
|
+
const nextHistory = pushHistoryEntry(current, historyEntry);
|
|
606
|
+
if (nextHistory !== current) {
|
|
607
|
+
appendHistoryEntry(historyEntry, { scope: historyScopeRef.current });
|
|
608
|
+
}
|
|
609
|
+
return nextHistory;
|
|
610
|
+
});
|
|
410
611
|
}
|
|
411
612
|
setText("");
|
|
412
613
|
setCursor(0);
|
|
413
614
|
setSelectedIndex(0);
|
|
414
615
|
setAttachments([]);
|
|
616
|
+
setImageLabelStartOverride(null);
|
|
415
617
|
setPastedContentRefs([]);
|
|
416
618
|
nextPastedContentIndexRef.current = 1;
|
|
417
619
|
setHistoryIndex(null);
|
|
@@ -432,6 +634,8 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
432
634
|
return false;
|
|
433
635
|
};
|
|
434
636
|
useInput((input, key) => {
|
|
637
|
+
if (isKeyReleaseEvent(key))
|
|
638
|
+
return;
|
|
435
639
|
const strippedInput = stripTerminalMouseSequences(input);
|
|
436
640
|
if (strippedInput !== input && !strippedInput) {
|
|
437
641
|
return;
|
|
@@ -479,13 +683,14 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
479
683
|
submitInput(nextText);
|
|
480
684
|
return;
|
|
481
685
|
}
|
|
686
|
+
const composerArrowDirection = composerVerticalArrowDirection(key);
|
|
482
687
|
// Autocomplete navigation
|
|
483
688
|
if (showSuggestions) {
|
|
484
|
-
if (navigable &&
|
|
689
|
+
if (navigable && composerArrowDirection === "up") {
|
|
485
690
|
setSelectedIndex((i) => (i - 1 + activeCount) % activeCount);
|
|
486
691
|
return;
|
|
487
692
|
}
|
|
488
|
-
if (navigable &&
|
|
693
|
+
if (navigable && composerArrowDirection === "down") {
|
|
489
694
|
setSelectedIndex((i) => (i + 1) % activeCount);
|
|
490
695
|
return;
|
|
491
696
|
}
|
|
@@ -535,7 +740,34 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
535
740
|
submitInput(text);
|
|
536
741
|
return;
|
|
537
742
|
}
|
|
538
|
-
|
|
743
|
+
const editAction = resolveComposerEditAction(input, key);
|
|
744
|
+
if (editAction) {
|
|
745
|
+
if (editAction === "word-left") {
|
|
746
|
+
setCursor(previousWordBoundary(text, cursor));
|
|
747
|
+
}
|
|
748
|
+
else if (editAction === "word-right") {
|
|
749
|
+
setCursor(nextWordBoundary(text, cursor));
|
|
750
|
+
}
|
|
751
|
+
else if (editAction === "line-start") {
|
|
752
|
+
setCursor(lineStartBoundary(text, cursor));
|
|
753
|
+
}
|
|
754
|
+
else if (editAction === "line-end") {
|
|
755
|
+
setCursor(lineEndBoundary(text, cursor));
|
|
756
|
+
}
|
|
757
|
+
else if (editAction === "delete-line-start") {
|
|
758
|
+
const next = deleteToLineStart(text, cursor);
|
|
759
|
+
setText(next.text);
|
|
760
|
+
setCursor(next.cursor);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
const next = deleteToLineEnd(text, cursor);
|
|
764
|
+
setText(next.text);
|
|
765
|
+
setCursor(next.cursor);
|
|
766
|
+
}
|
|
767
|
+
setSelectedIndex(0);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (key.backspace) {
|
|
539
771
|
if (cursor > 0) {
|
|
540
772
|
const before = text.slice(0, cursor - 1);
|
|
541
773
|
const after = text.slice(cursor);
|
|
@@ -546,7 +778,21 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
546
778
|
else if (attachments.length > 0) {
|
|
547
779
|
// Backspace at position 0 drops the most recent attachment so users
|
|
548
780
|
// can undo a misfired paste without submitting the message.
|
|
549
|
-
setAttachments((prev) =>
|
|
781
|
+
setAttachments((prev) => {
|
|
782
|
+
const next = prev.slice(0, -1);
|
|
783
|
+
if (next.length === 0)
|
|
784
|
+
setImageLabelStartOverride(null);
|
|
785
|
+
return next;
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
if (key.delete) {
|
|
791
|
+
if (cursor < text.length) {
|
|
792
|
+
const next = deleteAtCursor(text, cursor);
|
|
793
|
+
setText(next.text);
|
|
794
|
+
setCursor(next.cursor);
|
|
795
|
+
setSelectedIndex(0);
|
|
550
796
|
}
|
|
551
797
|
return;
|
|
552
798
|
}
|
|
@@ -561,7 +807,9 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
561
807
|
return;
|
|
562
808
|
}
|
|
563
809
|
if (key.upArrow || key.downArrow) {
|
|
564
|
-
|
|
810
|
+
if (composerArrowDirection) {
|
|
811
|
+
classifyVerticalArrow(composerArrowDirection);
|
|
812
|
+
}
|
|
565
813
|
return;
|
|
566
814
|
}
|
|
567
815
|
// Ctrl/meta chords are app-level shortcuts (Ctrl+S selection mode,
|
|
@@ -619,12 +867,19 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
619
867
|
setText(draftText);
|
|
620
868
|
setCursor(draftText.length);
|
|
621
869
|
setSelectedIndex(0);
|
|
870
|
+
setAttachments([]);
|
|
871
|
+
setImageLabelStartOverride(null);
|
|
622
872
|
setPastedContentRefs([]);
|
|
623
873
|
nextPastedContentIndexRef.current = 1;
|
|
624
874
|
setHistoryIndex(null);
|
|
625
875
|
historyDraftRef.current = "";
|
|
626
876
|
onDraftApplied?.();
|
|
627
877
|
}, [draftEpoch, draftText, onDraftApplied]);
|
|
878
|
+
useEffect(() => {
|
|
879
|
+
if (text || attachments.length > 0) {
|
|
880
|
+
submittedPayloadFingerprintRef.current = null;
|
|
881
|
+
}
|
|
882
|
+
}, [text, attachments.length]);
|
|
628
883
|
// After a terminal resize the previous-frame refs reference a layout that no
|
|
629
884
|
// longer exists; carrying them forward makes `needsCursorRowCompensation`
|
|
630
885
|
// compare new yoga heights against stale ones and offsets the cursor by a
|
|
@@ -642,40 +897,49 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
642
897
|
}, [width]);
|
|
643
898
|
const contentWidth = Math.max(1, width - PADDING_X * 2);
|
|
644
899
|
const lineWidth = Math.max(1, contentWidth - PROMPT.length);
|
|
645
|
-
const
|
|
646
|
-
const
|
|
900
|
+
const imageLabelStart = imageLabelStartOverride ?? nextImageLabelStart;
|
|
901
|
+
const attachmentLabels = useMemo(() => attachments.map((_, index) => imageDisplayLabel(imageLabelStart + index)), [attachments, imageLabelStart]);
|
|
902
|
+
const imageInlinePrefix = attachmentLabels.length > 0 ? `${attachmentLabels.join(" ")} ` : "";
|
|
903
|
+
const displayText = imageInlinePrefix + text;
|
|
904
|
+
const displayCursor = cursor + imageInlinePrefix.length;
|
|
905
|
+
const displayCursorToSourceCursor = (value) => Math.max(0, Math.min(text.length, value - imageInlinePrefix.length));
|
|
906
|
+
useEffect(() => {
|
|
907
|
+
if (disabled) {
|
|
908
|
+
setSoftwareCursorVisible(false);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
setSoftwareCursorVisible(true);
|
|
912
|
+
const timer = setInterval(() => {
|
|
913
|
+
setSoftwareCursorVisible((visible) => !visible);
|
|
914
|
+
}, CURSOR_BLINK_INTERVAL_MS);
|
|
915
|
+
return () => clearInterval(timer);
|
|
916
|
+
}, [disabled, displayCursor, displayText]);
|
|
917
|
+
const visualLines = useMemo(() => computeVisualLines(displayText, lineWidth), [displayText, lineWidth]);
|
|
918
|
+
const { row: cursorVisualRow, col: cursorVisualCol } = cursorToVisual(visualLines, displayCursor);
|
|
647
919
|
// ---- Wheel-vs-keyboard classification for Up/Down arrows ----
|
|
648
920
|
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
// screen. Those synthetic arrows must scroll the transcript, not move the
|
|
652
|
-
// composer cursor or browse history. Three signals, in priority order:
|
|
653
|
-
// 1. kitty keyboard protocol: real key presses carry `eventType`;
|
|
654
|
-
// synthetic wheel arrows are bare legacy sequences. Once one enhanced
|
|
655
|
-
// arrow is seen, bare arrows are classified as wheel with no delay.
|
|
656
|
-
// 2. burst heuristic (non-kitty terminals): a wheel notch delivers
|
|
657
|
-
// several arrows within a few ms; a keypress delivers one. The first
|
|
658
|
-
// arrow is briefly deferred to see whether siblings follow.
|
|
659
|
-
// 3. wheel session: shortly after a wheel burst, single arrows continue
|
|
660
|
-
// to scroll, so slow trackpad scrolling doesn't fall back to history.
|
|
921
|
+
// Up/Down reaching the composer are keyboard navigation: move within
|
|
922
|
+
// multiline input first, then browse prompt history at the top/bottom edge.
|
|
661
923
|
const performVerticalArrowRef = useRef(() => { });
|
|
662
924
|
performVerticalArrowRef.current = (direction) => {
|
|
663
925
|
if (direction === "up") {
|
|
664
926
|
if (cursorVisualRow > 0) {
|
|
665
|
-
setCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
|
|
927
|
+
setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol)));
|
|
666
928
|
return;
|
|
667
929
|
}
|
|
668
930
|
}
|
|
669
931
|
else {
|
|
670
932
|
if (cursorVisualRow < visualLines.length - 1) {
|
|
671
|
-
setCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
|
|
933
|
+
setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol)));
|
|
672
934
|
return;
|
|
673
935
|
}
|
|
674
936
|
}
|
|
675
|
-
const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, text);
|
|
937
|
+
const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, { text, images: attachments });
|
|
676
938
|
if (result.changed) {
|
|
677
939
|
setText(result.text);
|
|
678
940
|
setCursor(result.text.length);
|
|
941
|
+
setAttachments(result.images ?? []);
|
|
942
|
+
setImageLabelStartOverride(result.imageDisplayStart ?? null);
|
|
679
943
|
setHistoryIndex(result.index);
|
|
680
944
|
historyDraftRef.current = result.draft;
|
|
681
945
|
setSelectedIndex(0);
|
|
@@ -683,66 +947,13 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
683
947
|
nextPastedContentIndexRef.current = 1;
|
|
684
948
|
}
|
|
685
949
|
};
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
const lastWheelFlushRef = useRef(0);
|
|
689
|
-
const ARROW_BURST_WINDOW_MS = 20;
|
|
690
|
-
const WHEEL_SESSION_MS = 300;
|
|
691
|
-
const flushArrowBurst = (burst) => {
|
|
692
|
-
if (burst.count > 1 && onWheelScroll) {
|
|
693
|
-
lastWheelFlushRef.current = Date.now();
|
|
694
|
-
onWheelScroll(burst.direction, burst.count);
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
performVerticalArrowRef.current(burst.direction);
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
const classifyVerticalArrow = (direction, eventType) => {
|
|
701
|
-
if (eventType) {
|
|
702
|
-
kittyArrowsSeenRef.current = true;
|
|
703
|
-
performVerticalArrowRef.current(direction);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
if (!onWheelScroll) {
|
|
707
|
-
performVerticalArrowRef.current(direction);
|
|
708
|
-
return;
|
|
709
|
-
}
|
|
710
|
-
if (kittyArrowsSeenRef.current) {
|
|
711
|
-
lastWheelFlushRef.current = Date.now();
|
|
712
|
-
onWheelScroll(direction, 1);
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
const pending = arrowBurstRef.current;
|
|
716
|
-
if (pending) {
|
|
717
|
-
if (pending.direction === direction) {
|
|
718
|
-
pending.count += 1;
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
clearTimeout(pending.timer);
|
|
722
|
-
arrowBurstRef.current = null;
|
|
723
|
-
flushArrowBurst(pending);
|
|
724
|
-
}
|
|
725
|
-
if (Date.now() - lastWheelFlushRef.current < WHEEL_SESSION_MS) {
|
|
726
|
-
lastWheelFlushRef.current = Date.now();
|
|
727
|
-
onWheelScroll(direction, 1);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
const burst = {
|
|
731
|
-
direction,
|
|
732
|
-
count: 1,
|
|
733
|
-
timer: setTimeout(() => {
|
|
734
|
-
arrowBurstRef.current = null;
|
|
735
|
-
flushArrowBurst(burst);
|
|
736
|
-
}, ARROW_BURST_WINDOW_MS),
|
|
737
|
-
};
|
|
738
|
-
arrowBurstRef.current = burst;
|
|
950
|
+
const classifyVerticalArrow = (direction) => {
|
|
951
|
+
performVerticalArrowRef.current(direction);
|
|
739
952
|
};
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
clearTimeout(arrowBurstRef.current.timer);
|
|
743
|
-
}, []);
|
|
953
|
+
const lineFrame = shouldUseLineComposerFrame(theme.background);
|
|
954
|
+
const minVisibleLines = lineFrame ? 1 : MIN_VISIBLE_LINES;
|
|
744
955
|
const totalLines = Math.max(visualLines.length, 1);
|
|
745
|
-
const visibleLines = Math.min(Math.max(totalLines,
|
|
956
|
+
const visibleLines = Math.min(Math.max(totalLines, minVisibleLines), MAX_VISIBLE_LINES);
|
|
746
957
|
let scrollOffset = 0;
|
|
747
958
|
if (totalLines > visibleLines) {
|
|
748
959
|
scrollOffset = Math.min(Math.max(cursorVisualRow - Math.floor(visibleLines / 2), 0), totalLines - visibleLines);
|
|
@@ -772,6 +983,7 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
772
983
|
const inputFrameSignature = [
|
|
773
984
|
disabled ? "disabled" : "active",
|
|
774
985
|
text,
|
|
986
|
+
imageInlinePrefix,
|
|
775
987
|
scrollOffset.toString(),
|
|
776
988
|
visibleLines.toString(),
|
|
777
989
|
attachments.map((att) => `${att.filename ?? "clipboard"}:${att.bytes}`).join(","),
|
|
@@ -791,6 +1003,13 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
791
1003
|
// flicker every time streaming output above it re-lays out the frame, so
|
|
792
1004
|
// we hide it entirely until input is active again.
|
|
793
1005
|
useLayoutEffect(() => {
|
|
1006
|
+
if (!hardwareCursorEnabled) {
|
|
1007
|
+
if (lastCursorRef.current !== null) {
|
|
1008
|
+
lastCursorRef.current = null;
|
|
1009
|
+
}
|
|
1010
|
+
setCursorPosition(undefined);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
794
1013
|
let node = cursorLineRef.current ?? undefined;
|
|
795
1014
|
if (!node?.yogaNode) {
|
|
796
1015
|
if (disabled && lastCursorRef.current !== null) {
|
|
@@ -867,37 +1086,52 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
867
1086
|
// Reference cursorTick so the effect re-runs on the forced render pass.
|
|
868
1087
|
void cursorTick;
|
|
869
1088
|
const inputBg = disabled ? theme.inputBgDisabled : theme.inputBg;
|
|
1089
|
+
const rowBg = lineFrame ? undefined : inputBg;
|
|
1090
|
+
const cursorFg = lineFrame ? theme.background : inputBg;
|
|
1091
|
+
const cursorCellStyle = resolveSoftwareCursorCellStyle({
|
|
1092
|
+
visible: softwareCursorVisible,
|
|
1093
|
+
cursorBackground: theme.inputText,
|
|
1094
|
+
cursorForeground: cursorFg,
|
|
1095
|
+
textColor: theme.inputText,
|
|
1096
|
+
rowBackground: rowBg,
|
|
1097
|
+
});
|
|
870
1098
|
const moreBelow = totalLines - scrollOffset - visibleLines;
|
|
871
1099
|
const filledLine = (value) => {
|
|
872
1100
|
const visibleWidth = stringWidth(value);
|
|
873
1101
|
return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
|
|
874
1102
|
};
|
|
875
|
-
return (_jsxs(Box, { flexDirection: "column",
|
|
876
|
-
const label = att.filename || "clipboard";
|
|
877
|
-
const kb = Math.max(1, Math.round(att.bytes / 1024));
|
|
878
|
-
return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: theme.accent, children: `[img${attachments.length > 1 ? ` ${i + 1}` : ""}: ${label} · ${kb}KB]` }) }, i));
|
|
879
|
-
}) })), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, children: [hasMoreAbove && (_jsx(Text, { backgroundColor: inputBg, color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
|
|
1103
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [lineFrame && (_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) })), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, width: width, backgroundColor: inputBg, children: [hasMoreAbove && (_jsx(Text, { backgroundColor: rowBg, color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
|
|
880
1104
|
if (row.kind === "pad") {
|
|
881
|
-
return (_jsx(Text, { backgroundColor:
|
|
1105
|
+
return (_jsx(Text, { backgroundColor: rowBg, children: " ".repeat(contentWidth) }, row.key));
|
|
882
1106
|
}
|
|
883
1107
|
const { text: line, visualIdx } = row;
|
|
1108
|
+
const visualLine = visualLines[visualIdx];
|
|
884
1109
|
const lineText = line.length === 0 ? " " : line;
|
|
885
1110
|
const isFirst = visualIdx === 0;
|
|
886
1111
|
const isCursorLine = visualIdx === cursorVisualRow;
|
|
887
1112
|
const prompt = isFirst ? PROMPT : " ".repeat(PROMPT.length);
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
:
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
:
|
|
1113
|
+
const highlight = imageInlinePrefix ? null : slashCommandHighlight;
|
|
1114
|
+
const renderedSegments = splitComposerTextSegments({
|
|
1115
|
+
text: lineText,
|
|
1116
|
+
absStart: visualLine?.absStart ?? 0,
|
|
1117
|
+
highlight,
|
|
1118
|
+
cursorOffset: isCursorLine && !disabled
|
|
1119
|
+
? displayCursor - (visualLines[cursorVisualRow]?.absStart ?? 0)
|
|
1120
|
+
: undefined,
|
|
1121
|
+
});
|
|
1122
|
+
const renderedLine = renderedSegments.map((segment) => segment.text).join("");
|
|
894
1123
|
const fill = " ".repeat(Math.max(0, lineWidth - stringWidth(renderedLine)));
|
|
895
|
-
return (_jsxs(Box, { height: 1, overflow: "hidden", ref: isCursorLine
|
|
1124
|
+
return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: inputBg, ref: isCursorLine
|
|
896
1125
|
? (el) => {
|
|
897
1126
|
cursorLineRef.current = el;
|
|
898
1127
|
}
|
|
899
|
-
: undefined, children: [_jsx(Text, { backgroundColor:
|
|
900
|
-
|
|
1128
|
+
: undefined, children: [_jsx(Text, { backgroundColor: rowBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
|
|
1129
|
+
if (segment.kind === "cursor") {
|
|
1130
|
+
return (_jsx(Text, { backgroundColor: cursorCellStyle.backgroundColor, color: cursorCellStyle.color, children: segment.text }, index));
|
|
1131
|
+
}
|
|
1132
|
+
return (_jsx(Text, { backgroundColor: rowBg, color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
|
|
1133
|
+
}), _jsx(Text, { backgroundColor: rowBg, children: fill })] }, visualIdx));
|
|
1134
|
+
}), hasMoreBelow && (_jsx(Text, { backgroundColor: rowBg, color: theme.muted, dimColor: true, children: filledLine(` ↓ ${moreBelow} more`) }))] }), lineFrame && (_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) })), showSuggestions && mode === "slash" && (_jsxs(Box, { flexDirection: "column", marginTop: 1, paddingLeft: 4, children: [slashSuggestions
|
|
901
1135
|
.slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
|
|
902
1136
|
.map((cmd, visibleIndex) => {
|
|
903
1137
|
const i = suggestionOffset + visibleIndex;
|