@bubblebrain-ai/bubble 0.0.34 → 0.0.36
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 +27 -7
- package/dist/agent/abort-errors.d.ts +2 -2
- package/dist/agent/budget-ledger.d.ts +4 -45
- package/dist/agent/budget-ledger.js +5 -75
- package/dist/agent/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/child-runner.d.ts +1 -3
- package/dist/agent/child-runner.js +6 -37
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -2
- package/dist/agent/profiles.js +25 -14
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/subagent-control.d.ts +5 -12
- package/dist/agent/subagent-control.js +0 -2
- package/dist/agent/subagent-store.d.ts +1 -1
- package/dist/agent/subagent-store.js +1 -3
- package/dist/agent/workflow/control.d.ts +21 -0
- package/dist/agent/workflow/control.js +50 -8
- package/dist/agent/workflow/runtime.d.ts +15 -2
- package/dist/agent/workflow/runtime.js +112 -30
- package/dist/agent.d.ts +82 -48
- package/dist/agent.js +360 -232
- package/dist/approval/controller.js +28 -0
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +21 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +2 -1
- package/dist/config.d.ts +5 -3
- package/dist/config.js +10 -5
- package/dist/context/compact-llm.d.ts +2 -2
- package/dist/external-runtime/grok-acp.d.ts +56 -0
- package/dist/external-runtime/grok-acp.js +386 -0
- package/dist/external-runtime/grok-binary.d.ts +25 -0
- package/dist/external-runtime/grok-binary.js +108 -0
- package/dist/external-runtime/grok-errors.d.ts +7 -0
- package/dist/external-runtime/grok-errors.js +15 -0
- package/dist/external-runtime/grok-input-policy.d.ts +58 -0
- package/dist/external-runtime/grok-input-policy.js +52 -0
- package/dist/external-runtime/grok-ndjson.d.ts +14 -0
- package/dist/external-runtime/grok-ndjson.js +193 -0
- package/dist/external-runtime/grok-network.d.ts +15 -0
- package/dist/external-runtime/grok-network.js +81 -0
- package/dist/external-runtime/grok-preflight.d.ts +28 -0
- package/dist/external-runtime/grok-preflight.js +194 -0
- package/dist/external-runtime/grok-process.d.ts +26 -0
- package/dist/external-runtime/grok-process.js +184 -0
- package/dist/external-runtime/grok-profile.d.ts +32 -0
- package/dist/external-runtime/grok-profile.js +419 -0
- package/dist/external-runtime/grok-provider.d.ts +31 -0
- package/dist/external-runtime/grok-provider.js +41 -0
- package/dist/external-runtime/grok-runtime.d.ts +91 -0
- package/dist/external-runtime/grok-runtime.js +882 -0
- package/dist/external-runtime/index.d.ts +13 -0
- package/dist/external-runtime/index.js +13 -0
- package/dist/external-runtime/session-policy.d.ts +11 -0
- package/dist/external-runtime/session-policy.js +30 -0
- package/dist/external-runtime/types.d.ts +65 -0
- package/dist/external-runtime/types.js +1 -0
- package/dist/feishu/agent-host/approval-card.js +21 -0
- package/dist/feishu/agent-host/run-driver.js +19 -3
- package/dist/feishu/serve.js +1 -0
- package/dist/goal/tools.d.ts +6 -3
- package/dist/goal/tools.js +7 -19
- package/dist/main.js +114 -28
- package/dist/memory/db.d.ts +2 -0
- package/dist/memory/db.js +10 -0
- package/dist/memory/index.d.ts +1 -0
- package/dist/memory/index.js +1 -0
- package/dist/memory/phase1.js +14 -2
- package/dist/memory/phase2.js +21 -2
- package/dist/memory/prompts.js +1 -1
- package/dist/memory/session-policy.d.ts +25 -0
- package/dist/memory/session-policy.js +55 -0
- package/dist/memory/storage.d.ts +1 -0
- package/dist/memory/storage.js +13 -0
- package/dist/model-catalog.d.ts +18 -0
- package/dist/model-catalog.js +92 -52
- package/dist/model-config.d.ts +3 -1
- package/dist/model-config.js +11 -0
- package/dist/oauth/grok.d.ts +25 -0
- package/dist/oauth/grok.js +218 -0
- package/dist/oauth/index.d.ts +1 -0
- package/dist/oauth/index.js +1 -0
- package/dist/oauth/storage.d.ts +9 -0
- package/dist/oauth/storage.js +25 -0
- package/dist/orchestrator/default-hooks.js +23 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +1 -1
- package/dist/prompt/delegation.js +41 -8
- package/dist/prompt/environment.js +0 -4
- package/dist/prompt/reminders.d.ts +0 -1
- package/dist/prompt/reminders.js +1 -11
- package/dist/prompt/routing.d.ts +14 -0
- package/dist/prompt/routing.js +71 -0
- package/dist/prompt/runtime.js +1 -1
- package/dist/prompt/task-reminders.d.ts +20 -0
- package/dist/prompt/task-reminders.js +73 -1
- package/dist/provider-ai-sdk.js +2 -2
- package/dist/provider-grok.d.ts +26 -0
- package/dist/provider-grok.js +59 -0
- package/dist/provider-openai-codex.d.ts +17 -1
- package/dist/provider-openai-codex.js +101 -29
- package/dist/provider-registry.d.ts +72 -0
- package/dist/provider-registry.js +336 -59
- package/dist/provider-transform.d.ts +1 -1
- package/dist/provider-transform.js +35 -17
- package/dist/provider.d.ts +3 -0
- package/dist/provider.js +14 -0
- package/dist/session-types.d.ts +19 -1
- package/dist/session.d.ts +1 -0
- package/dist/session.js +4 -0
- package/dist/slash-commands/commands.js +224 -14
- package/dist/slash-commands/types.d.ts +14 -1
- package/dist/system-prompt.d.ts +2 -0
- package/dist/tools/agent-lifecycle.d.ts +1 -9
- package/dist/tools/agent-lifecycle.js +42 -272
- package/dist/tools/bash.js +1 -1
- package/dist/tools/child-tools.js +2 -0
- package/dist/tools/index.d.ts +2 -2
- package/dist/tools/index.js +7 -5
- package/dist/tools/lsp.js +1 -0
- package/dist/tools/memory.d.ts +7 -2
- package/dist/tools/memory.js +28 -43
- package/dist/tools/server.js +4 -0
- package/dist/tools/tool-search.d.ts +1 -1
- package/dist/tools/tool-search.js +23 -3
- package/dist/tui/model-switch.d.ts +7 -0
- package/dist/tui/model-switch.js +15 -5
- package/dist/tui-ink/app.d.ts +3 -1
- package/dist/tui-ink/app.js +672 -243
- package/dist/tui-ink/approval/approval-dialog.js +37 -1
- package/dist/tui-ink/approval/diff-view.js +4 -4
- package/dist/tui-ink/approval/select.js +2 -2
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- package/dist/tui-ink/feedback-dialog.js +2 -2
- package/dist/tui-ink/feishu-setup-picker.js +3 -3
- package/dist/tui-ink/footer.d.ts +1 -0
- package/dist/tui-ink/footer.js +7 -2
- package/dist/tui-ink/input-box.d.ts +36 -7
- package/dist/tui-ink/input-box.js +229 -157
- package/dist/tui-ink/markdown.js +11 -9
- package/dist/tui-ink/message-list.d.ts +5 -1
- package/dist/tui-ink/message-list.js +15 -2
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +464 -128
- package/dist/tui-ink/paste-operation-tracker.d.ts +13 -0
- package/dist/tui-ink/paste-operation-tracker.js +25 -0
- package/dist/tui-ink/plan-confirm.js +1 -1
- package/dist/tui-ink/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +34 -1
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/tui-ink/theme.d.ts +19 -2
- package/dist/tui-ink/theme.js +25 -8
- package/dist/tui-ink/welcome.d.ts +3 -2
- package/dist/tui-ink/welcome.js +10 -6
- package/dist/types.d.ts +8 -31
- package/dist/types.js +1 -1
- package/dist/variant/thinking-level.d.ts +2 -2
- package/dist/variant/thinking-level.js +2 -1
- package/dist/variant/variant-resolver.d.ts +22 -0
- package/dist/variant/variant-resolver.js +43 -0
- package/package.json +6 -2
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
|
@@ -6,15 +6,76 @@ import { appendFileSync } from "node:fs";
|
|
|
6
6
|
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
|
-
import { bareImageFilenameFromPaste, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
9
|
+
import { bareImageFilenameFromPaste, extractImagePathTokens, ingestClipboardImage, ingestImagePath, isImageFilePath, isScreenshotTempPath, splitPastedPaths, } from "./image-paste.js";
|
|
10
10
|
import { appendHistoryEntry, loadHistoryEntriesSync, pushHistoryEntry, stepHistory, } from "./input-history.js";
|
|
11
|
+
import { cloneComposerBuffer, createComposerBuffer, deleteComposerBackward, deleteComposerForward, expandComposerBuffer, hasActiveComposerPastes, insertComposerPaste, insertComposerText, moveComposerCursor, replaceComposerRange, } from "./composer-buffer.js";
|
|
11
12
|
import { isKeyReleaseEvent } from "./key-events.js";
|
|
13
|
+
import { PasteOperationTracker } from "./paste-operation-tracker.js";
|
|
12
14
|
import { stripTerminalMouseSequences } from "./terminal-mouse.js";
|
|
13
15
|
import { submitPayloadFingerprint } from "./submit-dedupe.js";
|
|
14
16
|
export { createPastedContentMarker, expandPastedContentMarkers, shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
|
|
15
|
-
import {
|
|
17
|
+
import { shouldCollapsePastedContent, } from "../tui/paste-placeholder.js";
|
|
16
18
|
import { imageDisplayLabel, stripInlineImageLabels } from "../tui/image-display.js";
|
|
17
|
-
|
|
19
|
+
export function resolveComposerAtContext(text, cursor, isSlashContext, allowWorkspaceMentions = true) {
|
|
20
|
+
return !allowWorkspaceMentions || isSlashContext ? null : findAtContext(text, cursor);
|
|
21
|
+
}
|
|
22
|
+
export function createComposerDraftSnapshot(input) {
|
|
23
|
+
return {
|
|
24
|
+
buffer: cloneComposerBuffer(input.buffer),
|
|
25
|
+
attachments: input.attachments.map((attachment) => ({ ...attachment })),
|
|
26
|
+
imageLabelStartOverride: input.imageLabelStartOverride,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Browse persisted history without turning marker-shaped text into live paste
|
|
31
|
+
* tokens, while preserving the exact unsent draft in memory for the final
|
|
32
|
+
* Down transition back out of history.
|
|
33
|
+
*/
|
|
34
|
+
export function stepComposerHistory(state, history, direction) {
|
|
35
|
+
const plainDraft = state.draftSnapshot
|
|
36
|
+
? {
|
|
37
|
+
text: state.draftSnapshot.buffer.text,
|
|
38
|
+
images: state.draftSnapshot.attachments,
|
|
39
|
+
...(state.draftSnapshot.imageLabelStartOverride !== null
|
|
40
|
+
? { imageDisplayStart: state.draftSnapshot.imageLabelStartOverride }
|
|
41
|
+
: {}),
|
|
42
|
+
}
|
|
43
|
+
: { text: "", images: [] };
|
|
44
|
+
const result = stepHistory({ history, index: state.historyIndex, draft: plainDraft }, direction, {
|
|
45
|
+
text: state.buffer.text,
|
|
46
|
+
images: state.attachments,
|
|
47
|
+
...(state.imageLabelStartOverride !== null
|
|
48
|
+
? { imageDisplayStart: state.imageLabelStartOverride }
|
|
49
|
+
: {}),
|
|
50
|
+
});
|
|
51
|
+
if (!result.changed)
|
|
52
|
+
return { ...state, changed: false };
|
|
53
|
+
let draftSnapshot = state.draftSnapshot;
|
|
54
|
+
if (state.historyIndex === null && result.index !== null && draftSnapshot === null) {
|
|
55
|
+
draftSnapshot = createComposerDraftSnapshot({
|
|
56
|
+
buffer: state.buffer,
|
|
57
|
+
attachments: state.attachments,
|
|
58
|
+
imageLabelStartOverride: state.imageLabelStartOverride,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (result.index === null && draftSnapshot) {
|
|
62
|
+
const restored = createComposerDraftSnapshot(draftSnapshot);
|
|
63
|
+
return {
|
|
64
|
+
...restored,
|
|
65
|
+
historyIndex: null,
|
|
66
|
+
draftSnapshot: null,
|
|
67
|
+
changed: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
buffer: createComposerBuffer(result.text),
|
|
72
|
+
attachments: (result.images ?? []).map((attachment) => ({ ...attachment })),
|
|
73
|
+
imageLabelStartOverride: result.imageDisplayStart ?? null,
|
|
74
|
+
historyIndex: result.index,
|
|
75
|
+
draftSnapshot: result.index === null ? null : draftSnapshot,
|
|
76
|
+
changed: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
18
79
|
const MAX_VISIBLE_LINES = 6;
|
|
19
80
|
const PADDING_X = 1;
|
|
20
81
|
const PROMPT = " > ";
|
|
@@ -47,12 +108,6 @@ export function isCtrlLetterInput(input, key, letter) {
|
|
|
47
108
|
export function isCtrlCInput(input, key) {
|
|
48
109
|
return isCtrlLetterInput(input, key, "c");
|
|
49
110
|
}
|
|
50
|
-
export function shouldUseLineComposerFrame(_background) {
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
export function composerSurfaceBackground(lineFrame, background, inputBg) {
|
|
54
|
-
return lineFrame ? background : inputBg;
|
|
55
|
-
}
|
|
56
111
|
export function shouldUseHardwareComposerCursor(env = process.env) {
|
|
57
112
|
return env.BUBBLE_HARDWARE_CURSOR === "1";
|
|
58
113
|
}
|
|
@@ -64,16 +119,10 @@ export function composerVerticalArrowDirection(key) {
|
|
|
64
119
|
return undefined;
|
|
65
120
|
}
|
|
66
121
|
export function resolveSoftwareCursorCellStyle(input) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
return {
|
|
74
|
-
backgroundColor: input.rowBackground,
|
|
75
|
-
color: input.textColor,
|
|
76
|
-
};
|
|
122
|
+
// Inverse video swaps the cell's real foreground/background at the terminal
|
|
123
|
+
// level, so the cursor block stays visible on any terminal palette without
|
|
124
|
+
// guessing a contrasting pair.
|
|
125
|
+
return { inverse: input.visible, color: input.textColor };
|
|
77
126
|
}
|
|
78
127
|
/**
|
|
79
128
|
* Split a composer line around the cursor so the cell under it can render as
|
|
@@ -249,6 +298,9 @@ export function resolveInkEnterIntent(input, key) {
|
|
|
249
298
|
return "newline";
|
|
250
299
|
return "submit";
|
|
251
300
|
}
|
|
301
|
+
export function shouldDeferComposerCommitWhilePastePending(hasPendingPaste, enterIntent, key) {
|
|
302
|
+
return hasPendingPaste && (enterIntent === "submit" || key.tab === true);
|
|
303
|
+
}
|
|
252
304
|
export function insertNewlineAtCursor(text, cursor) {
|
|
253
305
|
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
254
306
|
return {
|
|
@@ -333,49 +385,69 @@ export function resolveComposerEditAction(input, key) {
|
|
|
333
385
|
return "delete-line-end";
|
|
334
386
|
return null;
|
|
335
387
|
}
|
|
336
|
-
export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDownAtBottom, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
|
|
388
|
+
export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDownAtBottom, cursorResetEpoch = 0, draftText, draftEpoch = 0, onDraftApplied, skillRegistry, localSlashCommands = [], allowedSlashCommands, allowWorkspaceMentions = true, allowImageAttachments = true, terminalColumns, cwd, sessionFile, nextImageLabelStart = 1, }) {
|
|
337
389
|
const theme = useTheme();
|
|
338
390
|
const width = terminalColumns;
|
|
339
391
|
const historyScope = useMemo(() => ({ sessionFile, cwd }), [sessionFile, cwd]);
|
|
340
392
|
const hardwareCursorEnabled = shouldUseHardwareComposerCursor();
|
|
341
|
-
const [
|
|
342
|
-
const
|
|
393
|
+
const [composerBuffer, setComposerBuffer] = useState(() => createComposerBuffer());
|
|
394
|
+
const composerBufferRef = useRef(composerBuffer);
|
|
395
|
+
composerBufferRef.current = composerBuffer;
|
|
396
|
+
const updateComposerBuffer = React.useCallback((transition) => {
|
|
397
|
+
const next = transition(composerBufferRef.current);
|
|
398
|
+
composerBufferRef.current = next;
|
|
399
|
+
setComposerBuffer(next);
|
|
400
|
+
return next;
|
|
401
|
+
}, []);
|
|
402
|
+
const { text, cursor } = composerBuffer;
|
|
343
403
|
const [softwareCursorVisible, setSoftwareCursorVisible] = useState(true);
|
|
344
404
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
345
405
|
const [projectFiles, setProjectFiles] = useState(null);
|
|
346
406
|
const [attachments, setAttachments] = useState([]);
|
|
347
407
|
const [imageLabelStartOverride, setImageLabelStartOverride] = useState(null);
|
|
348
|
-
const [pastedContentRefs, setPastedContentRefs] = useState([]);
|
|
349
408
|
const [history, setHistory] = useState(() => loadHistoryEntriesSync({ scope: historyScope }));
|
|
350
409
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
351
|
-
const historyDraftRef = useRef(
|
|
410
|
+
const historyDraftRef = useRef(null);
|
|
352
411
|
const historyScopeRef = useRef(historyScope);
|
|
412
|
+
const historyScopeKey = `${historyScope.cwd}\0${historyScope.sessionFile ?? ""}`;
|
|
413
|
+
const previousHistoryScopeKeyRef = useRef(historyScopeKey);
|
|
414
|
+
const draftIdentityEpochRef = useRef(0);
|
|
415
|
+
const pasteOperationsRef = useRef(new PasteOperationTracker());
|
|
353
416
|
const submittedPayloadFingerprintRef = useRef(null);
|
|
354
417
|
const loadingFilesRef = useRef(false);
|
|
355
|
-
const nextPastedContentIndexRef = useRef(1);
|
|
356
418
|
// Kept equal to attachments.length so a synchronous multi-image paste loop
|
|
357
419
|
// assigns each image its correct (distinct) inline label index.
|
|
358
420
|
const attachmentCountRef = useRef(0);
|
|
359
|
-
// Paste and the keystrokes that follow can arrive inside the same stdin chunk
|
|
360
|
-
// and dispatch within one discreteUpdates batch. If the Enter that a user
|
|
361
|
-
// typed after a paste fires before React commits the paste-driven setState,
|
|
362
|
-
// useInput's Enter branch reads stale `text` and submits without the paste.
|
|
363
|
-
// This ref flips synchronously at paste-start and clears after the paste
|
|
364
|
-
// commit has been flushed — useInput's Enter handler bails while it's set.
|
|
365
|
-
const pastePendingRef = useRef(false);
|
|
366
421
|
historyScopeRef.current = historyScope;
|
|
422
|
+
const invalidateDraftAsyncWork = React.useCallback(() => {
|
|
423
|
+
draftIdentityEpochRef.current += 1;
|
|
424
|
+
pasteOperationsRef.current.invalidateAll();
|
|
425
|
+
}, []);
|
|
367
426
|
const ensureImageLabelStart = React.useCallback(() => {
|
|
368
427
|
setImageLabelStartOverride((current) => current ?? nextImageLabelStart);
|
|
369
428
|
}, [nextImageLabelStart]);
|
|
370
429
|
useEffect(() => {
|
|
430
|
+
const scopeChanged = previousHistoryScopeKeyRef.current !== historyScopeKey;
|
|
431
|
+
previousHistoryScopeKeyRef.current = historyScopeKey;
|
|
371
432
|
setHistory(loadHistoryEntriesSync({ scope: historyScope }));
|
|
372
433
|
setHistoryIndex(null);
|
|
373
434
|
setImageLabelStartOverride(null);
|
|
374
|
-
historyDraftRef.current =
|
|
375
|
-
|
|
435
|
+
historyDraftRef.current = null;
|
|
436
|
+
if (scopeChanged) {
|
|
437
|
+
updateComposerBuffer(() => createComposerBuffer());
|
|
438
|
+
setAttachments([]);
|
|
439
|
+
attachmentCountRef.current = 0;
|
|
440
|
+
setProjectFiles(null);
|
|
441
|
+
loadingFilesRef.current = false;
|
|
442
|
+
}
|
|
443
|
+
invalidateDraftAsyncWork();
|
|
444
|
+
}, [historyScope, historyScopeKey, invalidateDraftAsyncWork, updateComposerBuffer]);
|
|
445
|
+
useEffect(() => () => {
|
|
446
|
+
invalidateDraftAsyncWork();
|
|
447
|
+
}, [invalidateDraftAsyncWork]);
|
|
376
448
|
const isSlashContext = text.startsWith("/") && cursor > 0 && !text.includes("\n");
|
|
377
449
|
const slashPrefix = isSlashContext ? text.slice(1).toLowerCase() : "";
|
|
378
|
-
const atContext = useMemo(() => (
|
|
450
|
+
const atContext = useMemo(() => resolveComposerAtContext(text, cursor, isSlashContext, allowWorkspaceMentions), [allowWorkspaceMentions, text, cursor, isSlashContext]);
|
|
379
451
|
useEffect(() => {
|
|
380
452
|
if (!atContext || projectFiles !== null || loadingFilesRef.current)
|
|
381
453
|
return;
|
|
@@ -403,6 +475,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
403
475
|
commands.set(command.name, command);
|
|
404
476
|
}
|
|
405
477
|
for (const command of slashRegistry.list()) {
|
|
478
|
+
if (allowedSlashCommands && !allowedSlashCommands.has(command.name))
|
|
479
|
+
continue;
|
|
406
480
|
if (!commands.has(command.name))
|
|
407
481
|
commands.set(command.name, command);
|
|
408
482
|
}
|
|
@@ -411,24 +485,28 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
411
485
|
name: command.name,
|
|
412
486
|
description: command.description,
|
|
413
487
|
}));
|
|
414
|
-
const skillSuggestions = (skillRegistry?.summaries() ?? []).map((skill) => ({
|
|
488
|
+
const skillSuggestions = (allowedSlashCommands ? [] : (skillRegistry?.summaries() ?? [])).map((skill) => ({
|
|
415
489
|
type: "skill",
|
|
416
490
|
name: skill.name,
|
|
417
491
|
description: skill.description,
|
|
418
492
|
}));
|
|
419
493
|
const all = [...commandSuggestions, ...skillSuggestions];
|
|
420
494
|
return all.filter((item) => item.name.toLowerCase().startsWith(slashPrefix));
|
|
421
|
-
}, [isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
|
|
495
|
+
}, [allowedSlashCommands, isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
|
|
422
496
|
const knownSlashCommandNames = useMemo(() => {
|
|
423
497
|
const names = new Set();
|
|
424
498
|
for (const command of localSlashCommands)
|
|
425
499
|
names.add(command.name);
|
|
426
|
-
for (const command of slashRegistry.list())
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
500
|
+
for (const command of slashRegistry.list()) {
|
|
501
|
+
if (!allowedSlashCommands || allowedSlashCommands.has(command.name))
|
|
502
|
+
names.add(command.name);
|
|
503
|
+
}
|
|
504
|
+
if (!allowedSlashCommands) {
|
|
505
|
+
for (const skill of skillRegistry?.summaries() ?? [])
|
|
506
|
+
names.add(skill.name);
|
|
507
|
+
}
|
|
430
508
|
return names;
|
|
431
|
-
}, [skillRegistry, localSlashCommands]);
|
|
509
|
+
}, [allowedSlashCommands, skillRegistry, localSlashCommands]);
|
|
432
510
|
const slashCommandHighlight = useMemo(() => resolveSlashCommandHighlightRange(text, knownSlashCommandNames), [text, knownSlashCommandNames]);
|
|
433
511
|
const fileSuggestions = useMemo(() => {
|
|
434
512
|
if (!atContext || !projectFiles)
|
|
@@ -450,14 +528,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
450
528
|
const insertTextAtCursor = React.useCallback((insertion) => {
|
|
451
529
|
if (!insertion)
|
|
452
530
|
return;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const before = prev.slice(0, c);
|
|
456
|
-
const after = prev.slice(c);
|
|
457
|
-
return before + insertion + after;
|
|
458
|
-
});
|
|
459
|
-
setCursor((c) => c + insertion.length);
|
|
460
|
-
}, [cursor]);
|
|
531
|
+
updateComposerBuffer((current) => insertComposerText(current, insertion));
|
|
532
|
+
}, [updateComposerBuffer]);
|
|
461
533
|
const addAttachment = React.useCallback((att) => {
|
|
462
534
|
const base = imageLabelStartOverride ?? nextImageLabelStart;
|
|
463
535
|
const index = attachmentCountRef.current;
|
|
@@ -476,8 +548,10 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
476
548
|
// terminal has nothing textual to deliver. Probe the clipboard; if it yields
|
|
477
549
|
// an image, treat the paste as an image attachment. macOS only — Linux/Win
|
|
478
550
|
// terminals don't reliably emit empty pastes on image-only clipboards.
|
|
479
|
-
const tryClipboardImage = React.useCallback(async () => {
|
|
551
|
+
const tryClipboardImage = React.useCallback(async (expectedDraftEpoch) => {
|
|
480
552
|
const { attachment, error } = await ingestClipboardImage();
|
|
553
|
+
if (draftIdentityEpochRef.current !== expectedDraftEpoch)
|
|
554
|
+
return false;
|
|
481
555
|
if (attachment) {
|
|
482
556
|
addAttachment(attachment);
|
|
483
557
|
return true;
|
|
@@ -488,12 +562,13 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
488
562
|
return false;
|
|
489
563
|
}, [addAttachment, notice]);
|
|
490
564
|
usePaste((pasted) => {
|
|
491
|
-
|
|
565
|
+
const pasteOperationId = pasteOperationsRef.current.begin();
|
|
566
|
+
const pasteDraftEpoch = draftIdentityEpochRef.current;
|
|
492
567
|
// Clear the ref after React has committed the paste-driven setState.
|
|
493
568
|
// setTimeout with 0 runs after the current discreteUpdates batch flushes.
|
|
494
569
|
const clearPending = () => {
|
|
495
570
|
setTimeout(() => {
|
|
496
|
-
|
|
571
|
+
pasteOperationsRef.current.finish(pasteOperationId);
|
|
497
572
|
}, 0);
|
|
498
573
|
};
|
|
499
574
|
// Strip orphaned focus-event tails that can appear if focus reporting
|
|
@@ -506,10 +581,29 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
506
581
|
.replace(/\x1b\[I$/, "")
|
|
507
582
|
.replace(/\x1b\[O$/, "")
|
|
508
583
|
.replace(/\r\n?/g, "\n");
|
|
584
|
+
if (!allowImageAttachments) {
|
|
585
|
+
// A constrained text-only runtime must not probe the clipboard or read
|
|
586
|
+
// pasted filesystem paths. Do not leak image path strings as chat text.
|
|
587
|
+
const containsImagePath = clean.length > 0 && (bareImageFilenameFromPaste(clean) !== null
|
|
588
|
+
|| extractImagePathTokens(clean).length > 0);
|
|
589
|
+
if (clean.length === 0 || containsImagePath) {
|
|
590
|
+
notice("image attachments are disabled in this runtime session");
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
if (shouldCollapsePastedContent(clean)) {
|
|
594
|
+
updateComposerBuffer((current) => insertComposerPaste(current, clean));
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
insertTextAtCursor(clean);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
clearPending();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
509
603
|
// Empty paste on macOS usually means "Cmd+V with an image on the clipboard".
|
|
510
604
|
if (clean.length === 0) {
|
|
511
605
|
if (process.platform === "darwin") {
|
|
512
|
-
void tryClipboardImage().finally(clearPending);
|
|
606
|
+
void tryClipboardImage(pasteDraftEpoch).finally(clearPending);
|
|
513
607
|
}
|
|
514
608
|
else {
|
|
515
609
|
clearPending();
|
|
@@ -522,10 +616,11 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
522
616
|
// quietly.
|
|
523
617
|
const bareName = bareImageFilenameFromPaste(clean);
|
|
524
618
|
if (bareName && process.platform === "darwin") {
|
|
525
|
-
void tryClipboardImage()
|
|
619
|
+
void tryClipboardImage(pasteDraftEpoch)
|
|
526
620
|
.then((attached) => {
|
|
527
|
-
if (!attached)
|
|
621
|
+
if (draftIdentityEpochRef.current === pasteDraftEpoch && !attached) {
|
|
528
622
|
insertTextAtCursor(clean);
|
|
623
|
+
}
|
|
529
624
|
})
|
|
530
625
|
.finally(clearPending);
|
|
531
626
|
return;
|
|
@@ -538,9 +633,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
538
633
|
if (imageTokens.length === 0) {
|
|
539
634
|
// Plain text paste — insert into the input at the cursor.
|
|
540
635
|
if (shouldCollapsePastedContent(clean)) {
|
|
541
|
-
|
|
542
|
-
setPastedContentRefs((prev) => [...prev, { marker, content: clean }]);
|
|
543
|
-
insertTextAtCursor(marker);
|
|
636
|
+
updateComposerBuffer((current) => insertComposerPaste(current, clean));
|
|
544
637
|
}
|
|
545
638
|
else {
|
|
546
639
|
insertTextAtCursor(clean);
|
|
@@ -550,6 +643,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
550
643
|
}
|
|
551
644
|
const handle = async () => {
|
|
552
645
|
const results = await Promise.all(imageTokens.map((t) => ingestImagePath(t)));
|
|
646
|
+
if (draftIdentityEpochRef.current !== pasteDraftEpoch)
|
|
647
|
+
return;
|
|
553
648
|
const successful = [];
|
|
554
649
|
const errors = [];
|
|
555
650
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -567,10 +662,12 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
567
662
|
if (successful.length === 0 &&
|
|
568
663
|
process.platform === "darwin" &&
|
|
569
664
|
imageTokens.some(isScreenshotTempPath)) {
|
|
570
|
-
const clipOk = await tryClipboardImage();
|
|
665
|
+
const clipOk = await tryClipboardImage(pasteDraftEpoch);
|
|
571
666
|
if (clipOk)
|
|
572
667
|
return;
|
|
573
668
|
}
|
|
669
|
+
if (draftIdentityEpochRef.current !== pasteDraftEpoch)
|
|
670
|
+
return;
|
|
574
671
|
for (const att of successful)
|
|
575
672
|
addAttachment(att);
|
|
576
673
|
const nonImageLines = tokens.filter((t) => !isImageFilePath(t));
|
|
@@ -589,20 +686,16 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
589
686
|
const applyFileSuggestion = (selectedPath) => {
|
|
590
687
|
if (!atContext)
|
|
591
688
|
return;
|
|
592
|
-
const before = text.slice(0, atContext.start);
|
|
593
|
-
const after = text.slice(atContext.end);
|
|
594
689
|
const insert = `@${selectedPath} `;
|
|
595
|
-
|
|
596
|
-
setText(newText);
|
|
597
|
-
setCursor(before.length + insert.length);
|
|
690
|
+
updateComposerBuffer((current) => replaceComposerRange(current, atContext.start, atContext.end, insert));
|
|
598
691
|
setSelectedIndex(0);
|
|
599
692
|
};
|
|
600
|
-
const submitInput = (
|
|
693
|
+
const submitInput = (submittedBuffer, target = "submit") => {
|
|
601
694
|
const labelStartForSubmit = imageLabelStartOverride ?? nextImageLabelStart;
|
|
602
695
|
const inlineLabels = attachments.map((_, index) => imageDisplayLabel(labelStartForSubmit + index));
|
|
603
696
|
// Text-paste markers expand to their content (not replayable); image labels
|
|
604
697
|
// are a composer-only affordance stripped here (replayable via attachments).
|
|
605
|
-
const pasteExpanded =
|
|
698
|
+
const pasteExpanded = expandComposerBuffer(submittedBuffer);
|
|
606
699
|
const expandedText = stripInlineImageLabels(pasteExpanded, inlineLabels);
|
|
607
700
|
if (expandedText.trim().length === 0 && attachments.length === 0)
|
|
608
701
|
return;
|
|
@@ -626,7 +719,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
626
719
|
// A collapsed text-paste marker cannot be safely replayed once its
|
|
627
720
|
// in-memory reference is gone; skip those. Image-label stripping is fine to
|
|
628
721
|
// replay (the attachments are stored on the history entry).
|
|
629
|
-
if (
|
|
722
|
+
if (!hasActiveComposerPastes(submittedBuffer) && (expandedText.trim().length > 0 || attachments.length > 0)) {
|
|
630
723
|
const historyEntry = {
|
|
631
724
|
text: expandedText,
|
|
632
725
|
images: attachments,
|
|
@@ -640,26 +733,23 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
640
733
|
return nextHistory;
|
|
641
734
|
});
|
|
642
735
|
}
|
|
643
|
-
|
|
644
|
-
setCursor(0);
|
|
736
|
+
updateComposerBuffer(() => createComposerBuffer());
|
|
645
737
|
setSelectedIndex(0);
|
|
646
738
|
setAttachments([]);
|
|
647
739
|
attachmentCountRef.current = 0;
|
|
648
740
|
setImageLabelStartOverride(null);
|
|
649
|
-
setPastedContentRefs([]);
|
|
650
|
-
nextPastedContentIndexRef.current = 1;
|
|
651
741
|
setHistoryIndex(null);
|
|
652
|
-
historyDraftRef.current =
|
|
742
|
+
historyDraftRef.current = null;
|
|
743
|
+
invalidateDraftAsyncWork();
|
|
653
744
|
};
|
|
654
|
-
const applySlashEnterAction = (
|
|
655
|
-
const action = resolveSlashEnterAction(
|
|
745
|
+
const applySlashEnterAction = (submittedBuffer) => {
|
|
746
|
+
const action = resolveSlashEnterAction(submittedBuffer.text, slashSuggestions, selectedIndex);
|
|
656
747
|
if (action.kind === "submit") {
|
|
657
|
-
submitInput(
|
|
748
|
+
submitInput(submittedBuffer);
|
|
658
749
|
return true;
|
|
659
750
|
}
|
|
660
751
|
if (action.kind === "complete") {
|
|
661
|
-
|
|
662
|
-
setCursor(action.text.length);
|
|
752
|
+
updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, action.text));
|
|
663
753
|
setSelectedIndex(0);
|
|
664
754
|
return true;
|
|
665
755
|
}
|
|
@@ -689,18 +779,19 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
689
779
|
catch { }
|
|
690
780
|
}
|
|
691
781
|
const enterIntent = resolveInkEnterIntent(input, key);
|
|
782
|
+
if (shouldDeferComposerCommitWhilePastePending(pasteOperationsRef.current.hasPending, enterIntent, key)) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
692
785
|
if (enterIntent === "newline") {
|
|
693
|
-
|
|
694
|
-
setText(next.text);
|
|
695
|
-
setCursor(next.cursor);
|
|
786
|
+
updateComposerBuffer((current) => insertComposerText(current, "\n"));
|
|
696
787
|
setSelectedIndex(0);
|
|
697
788
|
return;
|
|
698
789
|
}
|
|
699
790
|
if (enterIntent === "submit" && input && /[\r\n]/.test(input)) {
|
|
700
791
|
const beforeReturn = input.split(/[\r\n]/)[0] ?? "";
|
|
701
|
-
const
|
|
792
|
+
const nextBuffer = insertComposerText(composerBufferRef.current, beforeReturn);
|
|
702
793
|
if (showSuggestions) {
|
|
703
|
-
if (mode === "slash" && navigable && applySlashEnterAction(
|
|
794
|
+
if (mode === "slash" && navigable && applySlashEnterAction(nextBuffer)) {
|
|
704
795
|
return;
|
|
705
796
|
}
|
|
706
797
|
if (mode === "file") {
|
|
@@ -712,7 +803,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
712
803
|
return;
|
|
713
804
|
}
|
|
714
805
|
}
|
|
715
|
-
submitInput(
|
|
806
|
+
submitInput(nextBuffer);
|
|
716
807
|
return;
|
|
717
808
|
}
|
|
718
809
|
const composerArrowDirection = composerVerticalArrowDirection(key);
|
|
@@ -733,13 +824,12 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
733
824
|
if (key.return || key.tab) {
|
|
734
825
|
if (mode === "slash" && navigable) {
|
|
735
826
|
if (key.return)
|
|
736
|
-
applySlashEnterAction(
|
|
827
|
+
applySlashEnterAction(composerBufferRef.current);
|
|
737
828
|
if (key.tab) {
|
|
738
829
|
const suggestion = slashSuggestions[selectedIndex];
|
|
739
830
|
if (suggestion) {
|
|
740
831
|
const newText = `/${suggestion.name} `;
|
|
741
|
-
|
|
742
|
-
setCursor(newText.length);
|
|
832
|
+
updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, newText));
|
|
743
833
|
setSelectedIndex(0);
|
|
744
834
|
}
|
|
745
835
|
}
|
|
@@ -759,55 +849,44 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
759
849
|
// While the agent runs, Tab queues the composer content for the next
|
|
760
850
|
// turn (Enter steers — handled by the app-level submit routing).
|
|
761
851
|
if (key.tab && !key.shift && onQueue && !showSuggestions) {
|
|
762
|
-
|
|
763
|
-
return;
|
|
764
|
-
submitInput(text, "queue");
|
|
852
|
+
submitInput(composerBufferRef.current, "queue");
|
|
765
853
|
return;
|
|
766
854
|
}
|
|
767
855
|
if (enterIntent === "submit") {
|
|
768
856
|
// A paste is still mid-flight — dropping this Enter avoids submitting
|
|
769
857
|
// an input state that doesn't yet include the paste.
|
|
770
|
-
|
|
771
|
-
return;
|
|
772
|
-
submitInput(text);
|
|
858
|
+
submitInput(composerBufferRef.current);
|
|
773
859
|
return;
|
|
774
860
|
}
|
|
775
861
|
const editAction = resolveComposerEditAction(input, key);
|
|
776
862
|
if (editAction) {
|
|
777
863
|
if (editAction === "word-left") {
|
|
778
|
-
|
|
864
|
+
updateComposerBuffer((current) => moveComposerCursor(current, previousWordBoundary(current.text, current.cursor), "left"));
|
|
779
865
|
}
|
|
780
866
|
else if (editAction === "word-right") {
|
|
781
|
-
|
|
867
|
+
updateComposerBuffer((current) => moveComposerCursor(current, nextWordBoundary(current.text, current.cursor), "right"));
|
|
782
868
|
}
|
|
783
869
|
else if (editAction === "line-start") {
|
|
784
|
-
|
|
870
|
+
updateComposerBuffer((current) => moveComposerCursor(current, lineStartBoundary(current.text, current.cursor), "left"));
|
|
785
871
|
}
|
|
786
872
|
else if (editAction === "line-end") {
|
|
787
|
-
|
|
873
|
+
updateComposerBuffer((current) => moveComposerCursor(current, lineEndBoundary(current.text, current.cursor), "right"));
|
|
788
874
|
}
|
|
789
875
|
else if (editAction === "delete-line-start") {
|
|
790
|
-
|
|
791
|
-
setText(next.text);
|
|
792
|
-
setCursor(next.cursor);
|
|
876
|
+
updateComposerBuffer((current) => replaceComposerRange(current, lineStartBoundary(current.text, current.cursor), current.cursor));
|
|
793
877
|
}
|
|
794
878
|
else {
|
|
795
|
-
|
|
796
|
-
setText(next.text);
|
|
797
|
-
setCursor(next.cursor);
|
|
879
|
+
updateComposerBuffer((current) => replaceComposerRange(current, current.cursor, lineEndBoundary(current.text, current.cursor)));
|
|
798
880
|
}
|
|
799
881
|
setSelectedIndex(0);
|
|
800
882
|
return;
|
|
801
883
|
}
|
|
802
884
|
if (key.backspace) {
|
|
803
|
-
if (cursor > 0) {
|
|
804
|
-
|
|
805
|
-
const after = text.slice(cursor);
|
|
806
|
-
setText(before + after);
|
|
807
|
-
setCursor(cursor - 1);
|
|
885
|
+
if (composerBufferRef.current.cursor > 0) {
|
|
886
|
+
updateComposerBuffer((current) => deleteComposerBackward(current));
|
|
808
887
|
setSelectedIndex(0);
|
|
809
888
|
}
|
|
810
|
-
else if (
|
|
889
|
+
else if (attachmentCountRef.current > 0) {
|
|
811
890
|
// Backspace at position 0 drops the most recent attachment so users
|
|
812
891
|
// can undo a misfired paste without submitting the message.
|
|
813
892
|
setAttachments((prev) => {
|
|
@@ -821,21 +900,17 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
821
900
|
return;
|
|
822
901
|
}
|
|
823
902
|
if (key.delete) {
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
setText(next.text);
|
|
827
|
-
setCursor(next.cursor);
|
|
828
|
-
setSelectedIndex(0);
|
|
829
|
-
}
|
|
903
|
+
updateComposerBuffer((current) => deleteComposerForward(current));
|
|
904
|
+
setSelectedIndex(0);
|
|
830
905
|
return;
|
|
831
906
|
}
|
|
832
907
|
if (key.leftArrow) {
|
|
833
|
-
|
|
908
|
+
updateComposerBuffer((current) => moveComposerCursor(current, current.cursor - 1, "left"));
|
|
834
909
|
setSelectedIndex(0);
|
|
835
910
|
return;
|
|
836
911
|
}
|
|
837
912
|
if (key.rightArrow) {
|
|
838
|
-
|
|
913
|
+
updateComposerBuffer((current) => moveComposerCursor(current, current.cursor + 1, "right"));
|
|
839
914
|
setSelectedIndex(0);
|
|
840
915
|
return;
|
|
841
916
|
}
|
|
@@ -854,10 +929,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
854
929
|
const printable = input.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
855
930
|
if (!printable)
|
|
856
931
|
return;
|
|
857
|
-
|
|
858
|
-
const after = text.slice(cursor);
|
|
859
|
-
setText(before + printable + after);
|
|
860
|
-
setCursor(cursor + printable.length);
|
|
932
|
+
updateComposerBuffer((current) => insertComposerText(current, printable));
|
|
861
933
|
setSelectedIndex(0);
|
|
862
934
|
}
|
|
863
935
|
});
|
|
@@ -895,18 +967,16 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
895
967
|
if (lastDraftEpochRef.current === draftEpoch)
|
|
896
968
|
return;
|
|
897
969
|
lastDraftEpochRef.current = draftEpoch;
|
|
898
|
-
if (
|
|
970
|
+
if (draftText === undefined)
|
|
899
971
|
return;
|
|
900
|
-
|
|
901
|
-
setCursor(draftText.length);
|
|
972
|
+
updateComposerBuffer(() => createComposerBuffer(draftText));
|
|
902
973
|
setSelectedIndex(0);
|
|
903
974
|
setAttachments([]);
|
|
904
975
|
attachmentCountRef.current = 0;
|
|
905
976
|
setImageLabelStartOverride(null);
|
|
906
|
-
setPastedContentRefs([]);
|
|
907
|
-
nextPastedContentIndexRef.current = 1;
|
|
908
977
|
setHistoryIndex(null);
|
|
909
|
-
historyDraftRef.current =
|
|
978
|
+
historyDraftRef.current = null;
|
|
979
|
+
invalidateDraftAsyncWork();
|
|
910
980
|
onDraftApplied?.();
|
|
911
981
|
}, [draftEpoch, draftText, onDraftApplied]);
|
|
912
982
|
useEffect(() => {
|
|
@@ -961,30 +1031,40 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
961
1031
|
// bottom edge (Down → next message, then back to the in-progress draft).
|
|
962
1032
|
const performVerticalArrowRef = useRef(() => { });
|
|
963
1033
|
performVerticalArrowRef.current = (direction) => {
|
|
1034
|
+
// A paste and the following key can arrive in one stdin batch. Wait for
|
|
1035
|
+
// that paste transition to commit before moving or snapshotting history.
|
|
1036
|
+
if (pasteOperationsRef.current.hasPending)
|
|
1037
|
+
return;
|
|
964
1038
|
if (direction === "up") {
|
|
965
1039
|
if (cursorVisualRow > 0) {
|
|
966
|
-
|
|
1040
|
+
const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
|
|
1041
|
+
updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
|
|
967
1042
|
return;
|
|
968
1043
|
}
|
|
969
1044
|
}
|
|
970
1045
|
else {
|
|
971
1046
|
if (cursorVisualRow < visualLines.length - 1) {
|
|
972
|
-
|
|
1047
|
+
const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
|
|
1048
|
+
updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
|
|
973
1049
|
return;
|
|
974
1050
|
}
|
|
975
1051
|
}
|
|
976
|
-
const result =
|
|
1052
|
+
const result = stepComposerHistory({
|
|
1053
|
+
buffer: composerBufferRef.current,
|
|
1054
|
+
attachments,
|
|
1055
|
+
imageLabelStartOverride,
|
|
1056
|
+
historyIndex,
|
|
1057
|
+
draftSnapshot: historyDraftRef.current,
|
|
1058
|
+
}, history, direction);
|
|
977
1059
|
if (result.changed) {
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
historyDraftRef.current = result.draft;
|
|
1060
|
+
updateComposerBuffer(() => result.buffer);
|
|
1061
|
+
setAttachments(result.attachments);
|
|
1062
|
+
attachmentCountRef.current = result.attachments.length;
|
|
1063
|
+
setImageLabelStartOverride(result.imageLabelStartOverride);
|
|
1064
|
+
setHistoryIndex(result.historyIndex);
|
|
1065
|
+
historyDraftRef.current = result.draftSnapshot;
|
|
985
1066
|
setSelectedIndex(0);
|
|
986
|
-
|
|
987
|
-
nextPastedContentIndexRef.current = 1;
|
|
1067
|
+
invalidateDraftAsyncWork();
|
|
988
1068
|
}
|
|
989
1069
|
else if (direction === "down") {
|
|
990
1070
|
// At the bottom edge with nothing newer in history: hand Down to the
|
|
@@ -995,8 +1075,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
995
1075
|
const classifyVerticalArrow = (direction) => {
|
|
996
1076
|
performVerticalArrowRef.current(direction);
|
|
997
1077
|
};
|
|
998
|
-
const
|
|
999
|
-
const minVisibleLines = lineFrame ? 1 : MIN_VISIBLE_LINES;
|
|
1078
|
+
const minVisibleLines = 1;
|
|
1000
1079
|
const totalLines = Math.max(visualLines.length, 1);
|
|
1001
1080
|
const visibleLines = Math.min(Math.max(totalLines, minVisibleLines), MAX_VISIBLE_LINES);
|
|
1002
1081
|
let scrollOffset = 0;
|
|
@@ -1130,25 +1209,18 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
1130
1209
|
});
|
|
1131
1210
|
// Reference cursorTick so the effect re-runs on the forced render pass.
|
|
1132
1211
|
void cursorTick;
|
|
1133
|
-
const inputBg = disabled ? theme.inputBgDisabled : theme.inputBg;
|
|
1134
|
-
const composerBg = composerSurfaceBackground(lineFrame, theme.background, inputBg);
|
|
1135
|
-
const rowBg = lineFrame ? undefined : inputBg;
|
|
1136
|
-
const cursorFg = lineFrame ? theme.background : inputBg;
|
|
1137
1212
|
const cursorCellStyle = resolveSoftwareCursorCellStyle({
|
|
1138
1213
|
visible: softwareCursorVisible,
|
|
1139
|
-
cursorBackground: theme.inputText,
|
|
1140
|
-
cursorForeground: cursorFg,
|
|
1141
1214
|
textColor: theme.inputText,
|
|
1142
|
-
rowBackground: rowBg,
|
|
1143
1215
|
});
|
|
1144
1216
|
const moreBelow = totalLines - scrollOffset - visibleLines;
|
|
1145
1217
|
const filledLine = (value) => {
|
|
1146
1218
|
const visibleWidth = visualWidth(value);
|
|
1147
1219
|
return value + " ".repeat(Math.max(0, contentWidth - visibleWidth));
|
|
1148
1220
|
};
|
|
1149
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [
|
|
1221
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, backgroundColor: theme.background, children: [_jsx(Box, { paddingX: PADDING_X, children: _jsx(Text, { color: theme.border, children: "─".repeat(contentWidth) }) }), _jsxs(Box, { flexDirection: "column", paddingX: PADDING_X, width: width, backgroundColor: theme.background, children: [hasMoreAbove && (_jsx(Text, { color: theme.muted, dimColor: true, children: filledLine(` ↑ ${scrollOffset} more`) })), displayedLines.map((row) => {
|
|
1150
1222
|
if (row.kind === "pad") {
|
|
1151
|
-
return (_jsx(Text, {
|
|
1223
|
+
return (_jsx(Text, { children: " ".repeat(contentWidth) }, row.key));
|
|
1152
1224
|
}
|
|
1153
1225
|
const { text: line, visualIdx } = row;
|
|
1154
1226
|
const visualLine = visualLines[visualIdx];
|
|
@@ -1167,17 +1239,17 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
1167
1239
|
});
|
|
1168
1240
|
const renderedLine = renderedSegments.map((segment) => segment.text).join("");
|
|
1169
1241
|
const fill = " ".repeat(Math.max(0, lineWidth - visualWidth(renderedLine)));
|
|
1170
|
-
return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor:
|
|
1242
|
+
return (_jsxs(Box, { height: 1, overflow: "hidden", backgroundColor: theme.background, ref: isCursorLine
|
|
1171
1243
|
? (el) => {
|
|
1172
1244
|
cursorLineRef.current = el;
|
|
1173
1245
|
}
|
|
1174
|
-
: undefined, children: [_jsx(Text, {
|
|
1246
|
+
: undefined, children: [_jsx(Text, { color: isFirst ? theme.accent : theme.inputText, children: prompt }), renderedSegments.map((segment, index) => {
|
|
1175
1247
|
if (segment.kind === "cursor") {
|
|
1176
|
-
return (_jsx(Text, {
|
|
1248
|
+
return (_jsx(Text, { inverse: cursorCellStyle.inverse, color: cursorCellStyle.color, children: segment.text }, index));
|
|
1177
1249
|
}
|
|
1178
|
-
return (_jsx(Text, {
|
|
1179
|
-
}), _jsx(Text, {
|
|
1180
|
-
}), hasMoreBelow && (_jsx(Text, {
|
|
1250
|
+
return (_jsx(Text, { color: segment.kind === "command" ? theme.accent : theme.inputText, bold: segment.kind === "command", children: segment.text }, index));
|
|
1251
|
+
}), _jsx(Text, { children: fill })] }, visualIdx));
|
|
1252
|
+
}), hasMoreBelow && (_jsx(Text, { color: theme.muted, dimColor: true, children: filledLine(` ↓ ${moreBelow} more`) }))] }), _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
|
|
1181
1253
|
.slice(suggestionOffset, suggestionOffset + MAX_VISIBLE_SUGGESTIONS)
|
|
1182
1254
|
.map((cmd, visibleIndex) => {
|
|
1183
1255
|
const i = suggestionOffset + visibleIndex;
|
|
@@ -1190,6 +1262,6 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
1190
1262
|
const i = suggestionOffset + visibleIndex;
|
|
1191
1263
|
const maxWidth = Math.max(10, Math.min(80, contentWidth - 2));
|
|
1192
1264
|
const label = s.path.length > maxWidth ? "…" + s.path.slice(-(maxWidth - 1)) : s.path;
|
|
1193
|
-
return (_jsx(Box, { height: 1, children: i === selectedIndex ? (_jsx(Text, {
|
|
1265
|
+
return (_jsx(Box, { height: 1, children: i === selectedIndex ? (_jsx(Text, { color: theme.accent, bold: true, children: `› ${label}` })) : (_jsx(Text, { children: ` ${label}` })) }, s.path));
|
|
1194
1266
|
}), fileSuggestions.length > MAX_VISIBLE_SUGGESTIONS && (_jsx(Text, { color: theme.muted, children: `Showing ${suggestionOffset + 1}-${Math.min(suggestionOffset + MAX_VISIBLE_SUGGESTIONS, fileSuggestions.length)} of ${fileSuggestions.length}` }))] }))] }));
|
|
1195
1267
|
}
|