@bubblebrain-ai/bubble 0.0.24 → 0.0.26
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 +5 -3
- package/dist/agent.js +1 -1
- package/dist/clipboard.d.ts +14 -0
- package/dist/clipboard.js +132 -0
- 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/model-catalog.d.ts +3 -1
- package/dist/model-catalog.js +17 -28
- package/dist/prompt/compose.js +1 -1
- package/dist/provider-anthropic.d.ts +4 -0
- package/dist/provider-anthropic.js +31 -0
- package/dist/provider-ark-responses.d.ts +17 -0
- package/dist/provider-ark-responses.js +462 -0
- package/dist/provider-transform.js +7 -0
- package/dist/provider.d.ts +7 -0
- package/dist/provider.js +170 -27
- package/dist/slash-commands/commands.js +22 -0
- package/dist/tools/todo.js +22 -38
- 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 +1409 -549
- 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 +71 -9
- package/dist/tui-ink/input-box.js +359 -121
- 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 +19 -1
- package/dist/tui-ink/message-list.js +111 -32
- package/dist/tui-ink/model-picker.d.ts +25 -2
- package/dist/tui-ink/model-picker.js +237 -20
- package/dist/tui-ink/plan-confirm.js +10 -0
- package/dist/tui-ink/question-dialog.js +46 -10
- package/dist/tui-ink/run.d.ts +10 -1
- package/dist/tui-ink/run.js +27 -42
- 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 +24 -1
- package/dist/tui-ink/terminal-mouse.js +76 -21
- package/dist/tui-ink/theme.d.ts +6 -3
- package/dist/tui-ink/theme.js +10 -4
- package/dist/tui-ink/welcome.d.ts +1 -0
- package/dist/tui-ink/welcome.js +34 -27
- package/dist/variant/variant-resolver.js +4 -1
- 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/transcript-scroll.d.ts +0 -25
- package/dist/tui/transcript-scroll.js +0 -20
- package/dist/tui-ink/transcript-viewport-math.d.ts +0 -11
- package/dist/tui-ink/transcript-viewport-math.js +0 -17
- package/dist/tui-ink/transcript-viewport.d.ts +0 -24
- package/dist/tui-ink/transcript-viewport.js +0 -83
- 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,10 +7,13 @@ 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;
|
|
16
19
|
const PADDING_X = 1;
|
|
@@ -37,15 +40,43 @@ export function resolveCursorRowCompensation(input) {
|
|
|
37
40
|
export function isCtrlCInput(input, key) {
|
|
38
41
|
return input === "\x03" || (key.ctrl === true && input.toLowerCase() === "c");
|
|
39
42
|
}
|
|
43
|
+
export function shouldUseLineComposerFrame(_background) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
export function composerSurfaceBackground(lineFrame, background, inputBg) {
|
|
47
|
+
return lineFrame ? background : inputBg;
|
|
48
|
+
}
|
|
49
|
+
export function shouldUseHardwareComposerCursor(env = process.env) {
|
|
50
|
+
return env.BUBBLE_HARDWARE_CURSOR === "1";
|
|
51
|
+
}
|
|
52
|
+
export function composerVerticalArrowDirection(key) {
|
|
53
|
+
if (key.upArrow)
|
|
54
|
+
return "up";
|
|
55
|
+
if (key.downArrow)
|
|
56
|
+
return "down";
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
export function resolveSoftwareCursorCellStyle(input) {
|
|
60
|
+
if (input.visible) {
|
|
61
|
+
return {
|
|
62
|
+
backgroundColor: input.cursorBackground,
|
|
63
|
+
color: input.cursorForeground,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
backgroundColor: input.rowBackground,
|
|
68
|
+
color: input.textColor,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
40
71
|
/**
|
|
41
72
|
* Split a composer line around the cursor so the cell under it can render as
|
|
42
73
|
* an inverse-video software cursor. The visible cursor must not depend on the
|
|
43
74
|
* real terminal cursor: Ink only re-arms its one-shot cursor escape when the
|
|
44
75
|
* component owning useCursor re-commits, so frames produced by other
|
|
45
76
|
* 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.
|
|
77
|
+
* hardware cursor for most of an agent run. Drawing and blinking the cell
|
|
78
|
+
* ourselves keeps it visible while preserving normal typing feedback; the real
|
|
79
|
+
* cursor is still positioned for IME anchoring.
|
|
49
80
|
*/
|
|
50
81
|
export function splitLineAtCursor(lineText, charOffset) {
|
|
51
82
|
const offset = Math.max(0, Math.min(charOffset, lineText.length));
|
|
@@ -132,6 +163,52 @@ function visualToCursor(visualLines, row, col) {
|
|
|
132
163
|
}
|
|
133
164
|
return vl.absStart + charOffset;
|
|
134
165
|
}
|
|
166
|
+
export function resolveSlashCommandHighlightRange(input, commandNames) {
|
|
167
|
+
if (!input.startsWith("/"))
|
|
168
|
+
return null;
|
|
169
|
+
const match = /^\/([^\s]+)/.exec(input);
|
|
170
|
+
if (!match)
|
|
171
|
+
return null;
|
|
172
|
+
const commandName = match[1]?.toLowerCase();
|
|
173
|
+
if (!commandName)
|
|
174
|
+
return null;
|
|
175
|
+
for (const name of commandNames) {
|
|
176
|
+
if (name.toLowerCase() === commandName) {
|
|
177
|
+
return { start: 0, end: match[0].length };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
function splitHighlightedText(text, absStart, highlight) {
|
|
183
|
+
if (!text)
|
|
184
|
+
return [];
|
|
185
|
+
if (!highlight)
|
|
186
|
+
return [{ kind: "normal", text }];
|
|
187
|
+
const start = Math.max(0, highlight.start - absStart);
|
|
188
|
+
const end = Math.min(text.length, highlight.end - absStart);
|
|
189
|
+
if (start >= end)
|
|
190
|
+
return [{ kind: "normal", text }];
|
|
191
|
+
const segments = [];
|
|
192
|
+
if (start > 0)
|
|
193
|
+
segments.push({ kind: "normal", text: text.slice(0, start) });
|
|
194
|
+
segments.push({ kind: "command", text: text.slice(start, end) });
|
|
195
|
+
if (end < text.length)
|
|
196
|
+
segments.push({ kind: "normal", text: text.slice(end) });
|
|
197
|
+
return segments;
|
|
198
|
+
}
|
|
199
|
+
export function splitComposerTextSegments(input) {
|
|
200
|
+
if (input.cursorOffset === undefined) {
|
|
201
|
+
return splitHighlightedText(input.text, input.absStart, input.highlight);
|
|
202
|
+
}
|
|
203
|
+
const cursorOffset = Math.max(0, Math.min(input.text.length, input.cursorOffset));
|
|
204
|
+
const cursorSegments = splitLineAtCursor(input.text, cursorOffset);
|
|
205
|
+
const cursorConsumesSource = cursorOffset < input.text.length;
|
|
206
|
+
return [
|
|
207
|
+
...splitHighlightedText(cursorSegments.before, input.absStart, input.highlight),
|
|
208
|
+
{ kind: "cursor", text: cursorSegments.at },
|
|
209
|
+
...splitHighlightedText(cursorSegments.after, input.absStart + cursorOffset + (cursorConsumesSource ? cursorSegments.at.length : 0), input.highlight),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
135
212
|
export function shouldSubmitExactSlashSuggestion(input, suggestionName) {
|
|
136
213
|
if (!suggestionName)
|
|
137
214
|
return false;
|
|
@@ -171,18 +248,101 @@ export function insertNewlineAtCursor(text, cursor) {
|
|
|
171
248
|
cursor: clampedCursor + 1,
|
|
172
249
|
};
|
|
173
250
|
}
|
|
174
|
-
export function
|
|
251
|
+
export function previousWordBoundary(text, cursor) {
|
|
252
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
253
|
+
if (clampedCursor === 0)
|
|
254
|
+
return 0;
|
|
255
|
+
let index = clampedCursor - 1;
|
|
256
|
+
while (index > 0 && /\s/.test(text[index]))
|
|
257
|
+
index--;
|
|
258
|
+
while (index > 0 && !/\s/.test(text[index - 1]))
|
|
259
|
+
index--;
|
|
260
|
+
return index;
|
|
261
|
+
}
|
|
262
|
+
export function nextWordBoundary(text, cursor) {
|
|
263
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
264
|
+
if (clampedCursor === text.length)
|
|
265
|
+
return text.length;
|
|
266
|
+
let index = clampedCursor;
|
|
267
|
+
while (index < text.length && /\s/.test(text[index]))
|
|
268
|
+
index++;
|
|
269
|
+
while (index < text.length && !/\s/.test(text[index]))
|
|
270
|
+
index++;
|
|
271
|
+
return index;
|
|
272
|
+
}
|
|
273
|
+
export function lineStartBoundary(text, cursor) {
|
|
274
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
275
|
+
return text.lastIndexOf("\n", clampedCursor - 1) + 1;
|
|
276
|
+
}
|
|
277
|
+
export function lineEndBoundary(text, cursor) {
|
|
278
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
279
|
+
const lineEnd = text.indexOf("\n", clampedCursor);
|
|
280
|
+
return lineEnd === -1 ? text.length : lineEnd;
|
|
281
|
+
}
|
|
282
|
+
export function deleteToLineStart(text, cursor) {
|
|
283
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
284
|
+
const lineStart = lineStartBoundary(text, clampedCursor);
|
|
285
|
+
return {
|
|
286
|
+
text: text.slice(0, lineStart) + text.slice(clampedCursor),
|
|
287
|
+
cursor: lineStart,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
export function deleteToLineEnd(text, cursor) {
|
|
291
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
292
|
+
const lineEnd = lineEndBoundary(text, clampedCursor);
|
|
293
|
+
return {
|
|
294
|
+
text: text.slice(0, clampedCursor) + text.slice(lineEnd),
|
|
295
|
+
cursor: clampedCursor,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
export function deleteAtCursor(text, cursor) {
|
|
299
|
+
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
300
|
+
if (clampedCursor >= text.length)
|
|
301
|
+
return { text, cursor: clampedCursor };
|
|
302
|
+
return {
|
|
303
|
+
text: text.slice(0, clampedCursor) + text.slice(clampedCursor + 1),
|
|
304
|
+
cursor: clampedCursor,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
export function resolveComposerEditAction(input, key) {
|
|
308
|
+
if (key.home)
|
|
309
|
+
return "line-start";
|
|
310
|
+
if (key.end)
|
|
311
|
+
return "line-end";
|
|
312
|
+
const wordModifier = key.ctrl || key.meta;
|
|
313
|
+
if (wordModifier && key.leftArrow)
|
|
314
|
+
return "word-left";
|
|
315
|
+
if (wordModifier && key.rightArrow)
|
|
316
|
+
return "word-right";
|
|
317
|
+
const lowerInput = input.toLowerCase();
|
|
318
|
+
if ((key.ctrl && lowerInput === "a") || input === "\x01")
|
|
319
|
+
return "line-start";
|
|
320
|
+
if ((key.ctrl && lowerInput === "e") || input === "\x05")
|
|
321
|
+
return "line-end";
|
|
322
|
+
if ((key.ctrl && lowerInput === "u") || input === "\x15")
|
|
323
|
+
return "delete-line-start";
|
|
324
|
+
if ((key.ctrl && lowerInput === "k") || input === "\x0b")
|
|
325
|
+
return "delete-line-end";
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
|
|
175
329
|
const theme = useTheme();
|
|
176
330
|
const width = terminalColumns;
|
|
331
|
+
const historyScope = useMemo(() => ({ sessionFile, cwd }), [sessionFile, cwd]);
|
|
332
|
+
const hardwareCursorEnabled = shouldUseHardwareComposerCursor();
|
|
177
333
|
const [text, setText] = useState("");
|
|
178
334
|
const [cursor, setCursor] = useState(0);
|
|
335
|
+
const [softwareCursorVisible, setSoftwareCursorVisible] = useState(true);
|
|
179
336
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
180
337
|
const [projectFiles, setProjectFiles] = useState(null);
|
|
181
338
|
const [attachments, setAttachments] = useState([]);
|
|
339
|
+
const [imageLabelStartOverride, setImageLabelStartOverride] = useState(null);
|
|
182
340
|
const [pastedContentRefs, setPastedContentRefs] = useState([]);
|
|
183
|
-
const [history, setHistory] = useState(() =>
|
|
341
|
+
const [history, setHistory] = useState(() => loadHistoryEntriesSync({ scope: historyScope }));
|
|
184
342
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
185
343
|
const historyDraftRef = useRef("");
|
|
344
|
+
const historyScopeRef = useRef(historyScope);
|
|
345
|
+
const submittedPayloadFingerprintRef = useRef(null);
|
|
186
346
|
const loadingFilesRef = useRef(false);
|
|
187
347
|
const nextPastedContentIndexRef = useRef(1);
|
|
188
348
|
// Paste and the keystrokes that follow can arrive inside the same stdin chunk
|
|
@@ -192,6 +352,16 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
192
352
|
// This ref flips synchronously at paste-start and clears after the paste
|
|
193
353
|
// commit has been flushed — useInput's Enter handler bails while it's set.
|
|
194
354
|
const pastePendingRef = useRef(false);
|
|
355
|
+
historyScopeRef.current = historyScope;
|
|
356
|
+
const ensureImageLabelStart = React.useCallback(() => {
|
|
357
|
+
setImageLabelStartOverride((current) => current ?? nextImageLabelStart);
|
|
358
|
+
}, [nextImageLabelStart]);
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
setHistory(loadHistoryEntriesSync({ scope: historyScope }));
|
|
361
|
+
setHistoryIndex(null);
|
|
362
|
+
setImageLabelStartOverride(null);
|
|
363
|
+
historyDraftRef.current = "";
|
|
364
|
+
}, [historyScope]);
|
|
195
365
|
const isSlashContext = text.startsWith("/") && cursor > 0 && !text.includes("\n");
|
|
196
366
|
const slashPrefix = isSlashContext ? text.slice(1).toLowerCase() : "";
|
|
197
367
|
const atContext = useMemo(() => (isSlashContext ? null : findAtContext(text, cursor)), [text, cursor, isSlashContext]);
|
|
@@ -201,23 +371,31 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
201
371
|
loadingFilesRef.current = true;
|
|
202
372
|
listProjectFiles(cwd).then((files) => setProjectFiles(files), () => setProjectFiles([]));
|
|
203
373
|
}, [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.
|
|
374
|
+
// The rendered inverse-video cell below is the visible cursor. Keep Ink's
|
|
375
|
+
// terminal cursor hidden by default so it can't race the software cursor; the
|
|
376
|
+
// hardware cursor can be enabled for IME diagnostics with BUBBLE_HARDWARE_CURSOR=1.
|
|
209
377
|
useEffect(() => {
|
|
378
|
+
if (!hardwareCursorEnabled)
|
|
379
|
+
return;
|
|
210
380
|
if (!process.stdout.isTTY)
|
|
211
381
|
return;
|
|
212
|
-
process.stdout.write("\x1b[
|
|
382
|
+
process.stdout.write("\x1b[1 q"); // blinking block
|
|
213
383
|
return () => {
|
|
214
384
|
process.stdout.write("\x1b[0 q"); // reset to terminal default
|
|
215
385
|
};
|
|
216
|
-
}, []);
|
|
386
|
+
}, [hardwareCursorEnabled]);
|
|
217
387
|
const slashSuggestions = useMemo(() => {
|
|
218
388
|
if (!isSlashContext)
|
|
219
389
|
return [];
|
|
220
|
-
const
|
|
390
|
+
const commands = new Map();
|
|
391
|
+
for (const command of localSlashCommands) {
|
|
392
|
+
commands.set(command.name, command);
|
|
393
|
+
}
|
|
394
|
+
for (const command of slashRegistry.list()) {
|
|
395
|
+
if (!commands.has(command.name))
|
|
396
|
+
commands.set(command.name, command);
|
|
397
|
+
}
|
|
398
|
+
const commandSuggestions = [...commands.values()].map((command) => ({
|
|
221
399
|
type: "command",
|
|
222
400
|
name: command.name,
|
|
223
401
|
description: command.description,
|
|
@@ -229,7 +407,18 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
229
407
|
}));
|
|
230
408
|
const all = [...commandSuggestions, ...skillSuggestions];
|
|
231
409
|
return all.filter((item) => item.name.toLowerCase().startsWith(slashPrefix));
|
|
232
|
-
}, [isSlashContext, slashPrefix, skillRegistry]);
|
|
410
|
+
}, [isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
|
|
411
|
+
const knownSlashCommandNames = useMemo(() => {
|
|
412
|
+
const names = new Set();
|
|
413
|
+
for (const command of localSlashCommands)
|
|
414
|
+
names.add(command.name);
|
|
415
|
+
for (const command of slashRegistry.list())
|
|
416
|
+
names.add(command.name);
|
|
417
|
+
for (const skill of skillRegistry?.summaries() ?? [])
|
|
418
|
+
names.add(skill.name);
|
|
419
|
+
return names;
|
|
420
|
+
}, [skillRegistry, localSlashCommands]);
|
|
421
|
+
const slashCommandHighlight = useMemo(() => resolveSlashCommandHighlightRange(text, knownSlashCommandNames), [text, knownSlashCommandNames]);
|
|
233
422
|
const fileSuggestions = useMemo(() => {
|
|
234
423
|
if (!atContext || !projectFiles)
|
|
235
424
|
return [];
|
|
@@ -259,8 +448,9 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
259
448
|
setCursor((c) => c + insertion.length);
|
|
260
449
|
}, [cursor]);
|
|
261
450
|
const addAttachment = React.useCallback((att) => {
|
|
451
|
+
ensureImageLabelStart();
|
|
262
452
|
setAttachments((prev) => [...prev, att]);
|
|
263
|
-
}, []);
|
|
453
|
+
}, [ensureImageLabelStart]);
|
|
264
454
|
const notice = React.useCallback((msg) => {
|
|
265
455
|
onPasteNotice?.(msg);
|
|
266
456
|
}, [onPasteNotice]);
|
|
@@ -394,24 +584,38 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
394
584
|
if (expandedText.trim().length === 0 && attachments.length === 0)
|
|
395
585
|
return;
|
|
396
586
|
const deliver = target === "queue" && onQueue ? onQueue : onSubmit;
|
|
397
|
-
|
|
587
|
+
const payload = {
|
|
398
588
|
text: expandedText,
|
|
399
589
|
displayText: expandedText === submittedText ? undefined : submittedText,
|
|
400
590
|
images: attachments,
|
|
401
|
-
|
|
591
|
+
imageDisplayStart: attachments.length > 0 ? (imageLabelStartOverride ?? nextImageLabelStart) : undefined,
|
|
592
|
+
};
|
|
593
|
+
const fingerprint = submitPayloadFingerprint(payload);
|
|
594
|
+
if (submittedPayloadFingerprintRef.current === fingerprint)
|
|
595
|
+
return;
|
|
596
|
+
submittedPayloadFingerprintRef.current = fingerprint;
|
|
597
|
+
deliver(payload);
|
|
402
598
|
// A collapsed marker cannot be safely replayed from history once its
|
|
403
599
|
// in-memory paste reference is gone; skip those entries instead.
|
|
404
|
-
if (expandedText.trim().length > 0
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
}
|
|
600
|
+
if (expandedText === submittedText && (expandedText.trim().length > 0 || attachments.length > 0)) {
|
|
601
|
+
const historyEntry = {
|
|
602
|
+
text: expandedText,
|
|
603
|
+
images: attachments,
|
|
604
|
+
...(attachments.length > 0 ? { imageDisplayStart: imageLabelStartOverride ?? nextImageLabelStart } : {}),
|
|
605
|
+
};
|
|
606
|
+
setHistory((current) => {
|
|
607
|
+
const nextHistory = pushHistoryEntry(current, historyEntry);
|
|
608
|
+
if (nextHistory !== current) {
|
|
609
|
+
appendHistoryEntry(historyEntry, { scope: historyScopeRef.current });
|
|
610
|
+
}
|
|
611
|
+
return nextHistory;
|
|
612
|
+
});
|
|
410
613
|
}
|
|
411
614
|
setText("");
|
|
412
615
|
setCursor(0);
|
|
413
616
|
setSelectedIndex(0);
|
|
414
617
|
setAttachments([]);
|
|
618
|
+
setImageLabelStartOverride(null);
|
|
415
619
|
setPastedContentRefs([]);
|
|
416
620
|
nextPastedContentIndexRef.current = 1;
|
|
417
621
|
setHistoryIndex(null);
|
|
@@ -432,6 +636,8 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
432
636
|
return false;
|
|
433
637
|
};
|
|
434
638
|
useInput((input, key) => {
|
|
639
|
+
if (isKeyReleaseEvent(key))
|
|
640
|
+
return;
|
|
435
641
|
const strippedInput = stripTerminalMouseSequences(input);
|
|
436
642
|
if (strippedInput !== input && !strippedInput) {
|
|
437
643
|
return;
|
|
@@ -479,13 +685,14 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
479
685
|
submitInput(nextText);
|
|
480
686
|
return;
|
|
481
687
|
}
|
|
688
|
+
const composerArrowDirection = composerVerticalArrowDirection(key);
|
|
482
689
|
// Autocomplete navigation
|
|
483
690
|
if (showSuggestions) {
|
|
484
|
-
if (navigable &&
|
|
691
|
+
if (navigable && composerArrowDirection === "up") {
|
|
485
692
|
setSelectedIndex((i) => (i - 1 + activeCount) % activeCount);
|
|
486
693
|
return;
|
|
487
694
|
}
|
|
488
|
-
if (navigable &&
|
|
695
|
+
if (navigable && composerArrowDirection === "down") {
|
|
489
696
|
setSelectedIndex((i) => (i + 1) % activeCount);
|
|
490
697
|
return;
|
|
491
698
|
}
|
|
@@ -535,7 +742,34 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
535
742
|
submitInput(text);
|
|
536
743
|
return;
|
|
537
744
|
}
|
|
538
|
-
|
|
745
|
+
const editAction = resolveComposerEditAction(input, key);
|
|
746
|
+
if (editAction) {
|
|
747
|
+
if (editAction === "word-left") {
|
|
748
|
+
setCursor(previousWordBoundary(text, cursor));
|
|
749
|
+
}
|
|
750
|
+
else if (editAction === "word-right") {
|
|
751
|
+
setCursor(nextWordBoundary(text, cursor));
|
|
752
|
+
}
|
|
753
|
+
else if (editAction === "line-start") {
|
|
754
|
+
setCursor(lineStartBoundary(text, cursor));
|
|
755
|
+
}
|
|
756
|
+
else if (editAction === "line-end") {
|
|
757
|
+
setCursor(lineEndBoundary(text, cursor));
|
|
758
|
+
}
|
|
759
|
+
else if (editAction === "delete-line-start") {
|
|
760
|
+
const next = deleteToLineStart(text, cursor);
|
|
761
|
+
setText(next.text);
|
|
762
|
+
setCursor(next.cursor);
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
const next = deleteToLineEnd(text, cursor);
|
|
766
|
+
setText(next.text);
|
|
767
|
+
setCursor(next.cursor);
|
|
768
|
+
}
|
|
769
|
+
setSelectedIndex(0);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (key.backspace) {
|
|
539
773
|
if (cursor > 0) {
|
|
540
774
|
const before = text.slice(0, cursor - 1);
|
|
541
775
|
const after = text.slice(cursor);
|
|
@@ -546,7 +780,21 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
546
780
|
else if (attachments.length > 0) {
|
|
547
781
|
// Backspace at position 0 drops the most recent attachment so users
|
|
548
782
|
// can undo a misfired paste without submitting the message.
|
|
549
|
-
setAttachments((prev) =>
|
|
783
|
+
setAttachments((prev) => {
|
|
784
|
+
const next = prev.slice(0, -1);
|
|
785
|
+
if (next.length === 0)
|
|
786
|
+
setImageLabelStartOverride(null);
|
|
787
|
+
return next;
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
if (key.delete) {
|
|
793
|
+
if (cursor < text.length) {
|
|
794
|
+
const next = deleteAtCursor(text, cursor);
|
|
795
|
+
setText(next.text);
|
|
796
|
+
setCursor(next.cursor);
|
|
797
|
+
setSelectedIndex(0);
|
|
550
798
|
}
|
|
551
799
|
return;
|
|
552
800
|
}
|
|
@@ -561,7 +809,9 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
561
809
|
return;
|
|
562
810
|
}
|
|
563
811
|
if (key.upArrow || key.downArrow) {
|
|
564
|
-
|
|
812
|
+
if (composerArrowDirection) {
|
|
813
|
+
classifyVerticalArrow(composerArrowDirection);
|
|
814
|
+
}
|
|
565
815
|
return;
|
|
566
816
|
}
|
|
567
817
|
// Ctrl/meta chords are app-level shortcuts (Ctrl+S selection mode,
|
|
@@ -619,12 +869,19 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
619
869
|
setText(draftText);
|
|
620
870
|
setCursor(draftText.length);
|
|
621
871
|
setSelectedIndex(0);
|
|
872
|
+
setAttachments([]);
|
|
873
|
+
setImageLabelStartOverride(null);
|
|
622
874
|
setPastedContentRefs([]);
|
|
623
875
|
nextPastedContentIndexRef.current = 1;
|
|
624
876
|
setHistoryIndex(null);
|
|
625
877
|
historyDraftRef.current = "";
|
|
626
878
|
onDraftApplied?.();
|
|
627
879
|
}, [draftEpoch, draftText, onDraftApplied]);
|
|
880
|
+
useEffect(() => {
|
|
881
|
+
if (text || attachments.length > 0) {
|
|
882
|
+
submittedPayloadFingerprintRef.current = null;
|
|
883
|
+
}
|
|
884
|
+
}, [text, attachments.length]);
|
|
628
885
|
// After a terminal resize the previous-frame refs reference a layout that no
|
|
629
886
|
// longer exists; carrying them forward makes `needsCursorRowCompensation`
|
|
630
887
|
// compare new yoga heights against stale ones and offsets the cursor by a
|
|
@@ -642,40 +899,50 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
642
899
|
}, [width]);
|
|
643
900
|
const contentWidth = Math.max(1, width - PADDING_X * 2);
|
|
644
901
|
const lineWidth = Math.max(1, contentWidth - PROMPT.length);
|
|
645
|
-
const
|
|
646
|
-
const
|
|
647
|
-
|
|
902
|
+
const imageLabelStart = imageLabelStartOverride ?? nextImageLabelStart;
|
|
903
|
+
const attachmentLabels = useMemo(() => attachments.map((_, index) => imageDisplayLabel(imageLabelStart + index)), [attachments, imageLabelStart]);
|
|
904
|
+
const imageInlinePrefix = attachmentLabels.length > 0 ? `${attachmentLabels.join(" ")} ` : "";
|
|
905
|
+
const displayText = imageInlinePrefix + text;
|
|
906
|
+
const displayCursor = cursor + imageInlinePrefix.length;
|
|
907
|
+
const displayCursorToSourceCursor = (value) => Math.max(0, Math.min(text.length, value - imageInlinePrefix.length));
|
|
908
|
+
// Steady (non-blinking) cursor on purpose. The composer lives in the live
|
|
909
|
+
// (repainting) region; a blink timer would rewrite these rows ~twice a second
|
|
910
|
+
// even at idle, and the terminal drops any in-progress text selection every
|
|
911
|
+
// time the underlying cells are rewritten — which is why composer text could
|
|
912
|
+
// not be highlighted/copied while agent answers (committed to <Static>, never
|
|
913
|
+
// repainted) could. Keeping the cursor steady leaves the idle composer frame
|
|
914
|
+
// static, so native selection works. We still hide it while disabled.
|
|
915
|
+
useEffect(() => {
|
|
916
|
+
setSoftwareCursorVisible(!disabled);
|
|
917
|
+
}, [disabled]);
|
|
918
|
+
const visualLines = useMemo(() => computeVisualLines(displayText, lineWidth), [displayText, lineWidth]);
|
|
919
|
+
const { row: cursorVisualRow, col: cursorVisualCol } = cursorToVisual(visualLines, displayCursor);
|
|
920
|
+
// ---- Up/Down arrow handling in the composer ----
|
|
648
921
|
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
//
|
|
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.
|
|
922
|
+
// Scrolling is the terminal's job now (native scrollback), so the composer
|
|
923
|
+
// owns Up/Down unconditionally: move within multiline input first, then
|
|
924
|
+
// browse prompt history at the top edge (Up → previous sent message) or the
|
|
925
|
+
// bottom edge (Down → next message, then back to the in-progress draft).
|
|
661
926
|
const performVerticalArrowRef = useRef(() => { });
|
|
662
927
|
performVerticalArrowRef.current = (direction) => {
|
|
663
928
|
if (direction === "up") {
|
|
664
929
|
if (cursorVisualRow > 0) {
|
|
665
|
-
setCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
|
|
930
|
+
setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol)));
|
|
666
931
|
return;
|
|
667
932
|
}
|
|
668
933
|
}
|
|
669
934
|
else {
|
|
670
935
|
if (cursorVisualRow < visualLines.length - 1) {
|
|
671
|
-
setCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
|
|
936
|
+
setCursor(displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol)));
|
|
672
937
|
return;
|
|
673
938
|
}
|
|
674
939
|
}
|
|
675
|
-
const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, text);
|
|
940
|
+
const result = stepHistory({ history, index: historyIndex, draft: historyDraftRef.current }, direction, { text, images: attachments });
|
|
676
941
|
if (result.changed) {
|
|
677
942
|
setText(result.text);
|
|
678
943
|
setCursor(result.text.length);
|
|
944
|
+
setAttachments(result.images ?? []);
|
|
945
|
+
setImageLabelStartOverride(result.imageDisplayStart ?? null);
|
|
679
946
|
setHistoryIndex(result.index);
|
|
680
947
|
historyDraftRef.current = result.draft;
|
|
681
948
|
setSelectedIndex(0);
|
|
@@ -683,66 +950,13 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
683
950
|
nextPastedContentIndexRef.current = 1;
|
|
684
951
|
}
|
|
685
952
|
};
|
|
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;
|
|
953
|
+
const classifyVerticalArrow = (direction) => {
|
|
954
|
+
performVerticalArrowRef.current(direction);
|
|
739
955
|
};
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
clearTimeout(arrowBurstRef.current.timer);
|
|
743
|
-
}, []);
|
|
956
|
+
const lineFrame = shouldUseLineComposerFrame(theme.background);
|
|
957
|
+
const minVisibleLines = lineFrame ? 1 : MIN_VISIBLE_LINES;
|
|
744
958
|
const totalLines = Math.max(visualLines.length, 1);
|
|
745
|
-
const visibleLines = Math.min(Math.max(totalLines,
|
|
959
|
+
const visibleLines = Math.min(Math.max(totalLines, minVisibleLines), MAX_VISIBLE_LINES);
|
|
746
960
|
let scrollOffset = 0;
|
|
747
961
|
if (totalLines > visibleLines) {
|
|
748
962
|
scrollOffset = Math.min(Math.max(cursorVisualRow - Math.floor(visibleLines / 2), 0), totalLines - visibleLines);
|
|
@@ -772,6 +986,7 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
772
986
|
const inputFrameSignature = [
|
|
773
987
|
disabled ? "disabled" : "active",
|
|
774
988
|
text,
|
|
989
|
+
imageInlinePrefix,
|
|
775
990
|
scrollOffset.toString(),
|
|
776
991
|
visibleLines.toString(),
|
|
777
992
|
attachments.map((att) => `${att.filename ?? "clipboard"}:${att.bytes}`).join(","),
|
|
@@ -791,6 +1006,13 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
791
1006
|
// flicker every time streaming output above it re-lays out the frame, so
|
|
792
1007
|
// we hide it entirely until input is active again.
|
|
793
1008
|
useLayoutEffect(() => {
|
|
1009
|
+
if (!hardwareCursorEnabled) {
|
|
1010
|
+
if (lastCursorRef.current !== null) {
|
|
1011
|
+
lastCursorRef.current = null;
|
|
1012
|
+
}
|
|
1013
|
+
setCursorPosition(undefined);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
794
1016
|
let node = cursorLineRef.current ?? undefined;
|
|
795
1017
|
if (!node?.yogaNode) {
|
|
796
1018
|
if (disabled && lastCursorRef.current !== null) {
|
|
@@ -867,37 +1089,53 @@ export function InputBox({ onSubmit, onQueue, onWheelScroll, onPasteNotice, disa
|
|
|
867
1089
|
// Reference cursorTick so the effect re-runs on the forced render pass.
|
|
868
1090
|
void cursorTick;
|
|
869
1091
|
const inputBg = disabled ? theme.inputBgDisabled : theme.inputBg;
|
|
1092
|
+
const composerBg = composerSurfaceBackground(lineFrame, theme.background, inputBg);
|
|
1093
|
+
const rowBg = lineFrame ? undefined : inputBg;
|
|
1094
|
+
const cursorFg = lineFrame ? theme.background : inputBg;
|
|
1095
|
+
const cursorCellStyle = resolveSoftwareCursorCellStyle({
|
|
1096
|
+
visible: softwareCursorVisible,
|
|
1097
|
+
cursorBackground: theme.inputText,
|
|
1098
|
+
cursorForeground: cursorFg,
|
|
1099
|
+
textColor: theme.inputText,
|
|
1100
|
+
rowBackground: rowBg,
|
|
1101
|
+
});
|
|
870
1102
|
const moreBelow = totalLines - scrollOffset - visibleLines;
|
|
871
1103
|
const filledLine = (value) => {
|
|
872
1104
|
const visibleWidth = stringWidth(value);
|
|
873
1105
|
return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
|
|
874
1106
|
};
|
|
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) => {
|
|
1107
|
+
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: composerBg, children: [hasMoreAbove && (_jsx(Text, { backgroundColor: rowBg, color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
|
|
880
1108
|
if (row.kind === "pad") {
|
|
881
|
-
return (_jsx(Text, { backgroundColor:
|
|
1109
|
+
return (_jsx(Text, { backgroundColor: rowBg, children: " ".repeat(contentWidth) }, row.key));
|
|
882
1110
|
}
|
|
883
1111
|
const { text: line, visualIdx } = row;
|
|
1112
|
+
const visualLine = visualLines[visualIdx];
|
|
884
1113
|
const lineText = line.length === 0 ? " " : line;
|
|
885
1114
|
const isFirst = visualIdx === 0;
|
|
886
1115
|
const isCursorLine = visualIdx === cursorVisualRow;
|
|
887
1116
|
const prompt = isFirst ? PROMPT : " ".repeat(PROMPT.length);
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
:
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
:
|
|
1117
|
+
const highlight = imageInlinePrefix ? null : slashCommandHighlight;
|
|
1118
|
+
const renderedSegments = splitComposerTextSegments({
|
|
1119
|
+
text: lineText,
|
|
1120
|
+
absStart: visualLine?.absStart ?? 0,
|
|
1121
|
+
highlight,
|
|
1122
|
+
cursorOffset: isCursorLine && !disabled
|
|
1123
|
+
? displayCursor - (visualLines[cursorVisualRow]?.absStart ?? 0)
|
|
1124
|
+
: undefined,
|
|
1125
|
+
});
|
|
1126
|
+
const renderedLine = renderedSegments.map((segment) => segment.text).join("");
|
|
894
1127
|
const fill = " ".repeat(Math.max(0, lineWidth - stringWidth(renderedLine)));
|
|
895
|
-
return (_jsxs(Box, { height: 1, overflow: "hidden", ref: isCursorLine
|
|
1128
|
+
return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: composerBg, ref: isCursorLine
|
|
896
1129
|
? (el) => {
|
|
897
1130
|
cursorLineRef.current = el;
|
|
898
1131
|
}
|
|
899
|
-
: undefined, children: [_jsx(Text, { backgroundColor:
|
|
900
|
-
|
|
1132
|
+
: undefined, children: [_jsx(Text, { backgroundColor: rowBg, color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
|
|
1133
|
+
if (segment.kind === "cursor") {
|
|
1134
|
+
return (_jsx(Text, { backgroundColor: cursorCellStyle.backgroundColor, color: cursorCellStyle.color, children: segment.text }, index));
|
|
1135
|
+
}
|
|
1136
|
+
return (_jsx(Text, { backgroundColor: rowBg, color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
|
|
1137
|
+
}), _jsx(Text, { backgroundColor: rowBg, children: fill })] }, visualIdx));
|
|
1138
|
+
}), 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
1139
|
.slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
|
|
902
1140
|
.map((cmd, visibleIndex) => {
|
|
903
1141
|
const i = suggestionOffset + visibleIndex;
|