@bubblebrain-ai/bubble 0.0.35 → 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/categories.d.ts +66 -2
- package/dist/agent/categories.js +120 -19
- package/dist/agent/discovery-barrier.js +1 -1
- package/dist/agent/profiles.d.ts +1 -0
- package/dist/agent/profiles.js +25 -11
- package/dist/agent/routing-catalog.d.ts +113 -0
- package/dist/agent/routing-catalog.js +240 -0
- package/dist/agent/workflow/control.d.ts +14 -0
- package/dist/agent/workflow/control.js +30 -9
- package/dist/agent.d.ts +75 -5
- package/dist/agent.js +313 -61
- 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 +4 -0
- package/dist/config.js +4 -0
- 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 +105 -27
- 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 +87 -41
- 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 +17 -7
- package/dist/prompt/compose.d.ts +3 -0
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.js +22 -3
- 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 +19 -0
- package/dist/prompt/task-reminders.js +49 -0
- 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 +28 -5
- 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.js +9 -11
- 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 +648 -232
- package/dist/tui-ink/approval/approval-dialog.js +36 -0
- package/dist/tui-ink/composer-buffer.d.ts +55 -0
- package/dist/tui-ink/composer-buffer.js +317 -0
- 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 +35 -1
- package/dist/tui-ink/input-box.js +215 -122
- package/dist/tui-ink/model-picker.d.ts +61 -4
- package/dist/tui-ink/model-picker.js +453 -123
- 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/run.d.ts +3 -0
- package/dist/tui-ink/run.js +1 -1
- package/dist/tui-ink/subagent-view.d.ts +31 -0
- package/dist/tui-ink/subagent-view.js +82 -1
- package/dist/types.d.ts +5 -5
- 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 +9 -0
- package/dist/variant/variant-resolver.js +17 -0
- package/package.json +2 -1
- package/dist/tools/task.d.ts +0 -2
- package/dist/tools/task.js +0 -58
|
@@ -6,14 +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";
|
|
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
|
+
}
|
|
17
79
|
const MAX_VISIBLE_LINES = 6;
|
|
18
80
|
const PADDING_X = 1;
|
|
19
81
|
const PROMPT = " > ";
|
|
@@ -236,6 +298,9 @@ export function resolveInkEnterIntent(input, key) {
|
|
|
236
298
|
return "newline";
|
|
237
299
|
return "submit";
|
|
238
300
|
}
|
|
301
|
+
export function shouldDeferComposerCommitWhilePastePending(hasPendingPaste, enterIntent, key) {
|
|
302
|
+
return hasPendingPaste && (enterIntent === "submit" || key.tab === true);
|
|
303
|
+
}
|
|
239
304
|
export function insertNewlineAtCursor(text, cursor) {
|
|
240
305
|
const clampedCursor = Math.max(0, Math.min(text.length, cursor));
|
|
241
306
|
return {
|
|
@@ -320,49 +385,69 @@ export function resolveComposerEditAction(input, key) {
|
|
|
320
385
|
return "delete-line-end";
|
|
321
386
|
return null;
|
|
322
387
|
}
|
|
323
|
-
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, }) {
|
|
324
389
|
const theme = useTheme();
|
|
325
390
|
const width = terminalColumns;
|
|
326
391
|
const historyScope = useMemo(() => ({ sessionFile, cwd }), [sessionFile, cwd]);
|
|
327
392
|
const hardwareCursorEnabled = shouldUseHardwareComposerCursor();
|
|
328
|
-
const [
|
|
329
|
-
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;
|
|
330
403
|
const [softwareCursorVisible, setSoftwareCursorVisible] = useState(true);
|
|
331
404
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
332
405
|
const [projectFiles, setProjectFiles] = useState(null);
|
|
333
406
|
const [attachments, setAttachments] = useState([]);
|
|
334
407
|
const [imageLabelStartOverride, setImageLabelStartOverride] = useState(null);
|
|
335
|
-
const [pastedContentRefs, setPastedContentRefs] = useState([]);
|
|
336
408
|
const [history, setHistory] = useState(() => loadHistoryEntriesSync({ scope: historyScope }));
|
|
337
409
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
338
|
-
const historyDraftRef = useRef(
|
|
410
|
+
const historyDraftRef = useRef(null);
|
|
339
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());
|
|
340
416
|
const submittedPayloadFingerprintRef = useRef(null);
|
|
341
417
|
const loadingFilesRef = useRef(false);
|
|
342
|
-
const nextPastedContentIndexRef = useRef(1);
|
|
343
418
|
// Kept equal to attachments.length so a synchronous multi-image paste loop
|
|
344
419
|
// assigns each image its correct (distinct) inline label index.
|
|
345
420
|
const attachmentCountRef = useRef(0);
|
|
346
|
-
// Paste and the keystrokes that follow can arrive inside the same stdin chunk
|
|
347
|
-
// and dispatch within one discreteUpdates batch. If the Enter that a user
|
|
348
|
-
// typed after a paste fires before React commits the paste-driven setState,
|
|
349
|
-
// useInput's Enter branch reads stale `text` and submits without the paste.
|
|
350
|
-
// This ref flips synchronously at paste-start and clears after the paste
|
|
351
|
-
// commit has been flushed — useInput's Enter handler bails while it's set.
|
|
352
|
-
const pastePendingRef = useRef(false);
|
|
353
421
|
historyScopeRef.current = historyScope;
|
|
422
|
+
const invalidateDraftAsyncWork = React.useCallback(() => {
|
|
423
|
+
draftIdentityEpochRef.current += 1;
|
|
424
|
+
pasteOperationsRef.current.invalidateAll();
|
|
425
|
+
}, []);
|
|
354
426
|
const ensureImageLabelStart = React.useCallback(() => {
|
|
355
427
|
setImageLabelStartOverride((current) => current ?? nextImageLabelStart);
|
|
356
428
|
}, [nextImageLabelStart]);
|
|
357
429
|
useEffect(() => {
|
|
430
|
+
const scopeChanged = previousHistoryScopeKeyRef.current !== historyScopeKey;
|
|
431
|
+
previousHistoryScopeKeyRef.current = historyScopeKey;
|
|
358
432
|
setHistory(loadHistoryEntriesSync({ scope: historyScope }));
|
|
359
433
|
setHistoryIndex(null);
|
|
360
434
|
setImageLabelStartOverride(null);
|
|
361
|
-
historyDraftRef.current =
|
|
362
|
-
|
|
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]);
|
|
363
448
|
const isSlashContext = text.startsWith("/") && cursor > 0 && !text.includes("\n");
|
|
364
449
|
const slashPrefix = isSlashContext ? text.slice(1).toLowerCase() : "";
|
|
365
|
-
const atContext = useMemo(() => (
|
|
450
|
+
const atContext = useMemo(() => resolveComposerAtContext(text, cursor, isSlashContext, allowWorkspaceMentions), [allowWorkspaceMentions, text, cursor, isSlashContext]);
|
|
366
451
|
useEffect(() => {
|
|
367
452
|
if (!atContext || projectFiles !== null || loadingFilesRef.current)
|
|
368
453
|
return;
|
|
@@ -390,6 +475,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
390
475
|
commands.set(command.name, command);
|
|
391
476
|
}
|
|
392
477
|
for (const command of slashRegistry.list()) {
|
|
478
|
+
if (allowedSlashCommands && !allowedSlashCommands.has(command.name))
|
|
479
|
+
continue;
|
|
393
480
|
if (!commands.has(command.name))
|
|
394
481
|
commands.set(command.name, command);
|
|
395
482
|
}
|
|
@@ -398,24 +485,28 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
398
485
|
name: command.name,
|
|
399
486
|
description: command.description,
|
|
400
487
|
}));
|
|
401
|
-
const skillSuggestions = (skillRegistry?.summaries() ?? []).map((skill) => ({
|
|
488
|
+
const skillSuggestions = (allowedSlashCommands ? [] : (skillRegistry?.summaries() ?? [])).map((skill) => ({
|
|
402
489
|
type: "skill",
|
|
403
490
|
name: skill.name,
|
|
404
491
|
description: skill.description,
|
|
405
492
|
}));
|
|
406
493
|
const all = [...commandSuggestions, ...skillSuggestions];
|
|
407
494
|
return all.filter((item) => item.name.toLowerCase().startsWith(slashPrefix));
|
|
408
|
-
}, [isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
|
|
495
|
+
}, [allowedSlashCommands, isSlashContext, slashPrefix, skillRegistry, localSlashCommands]);
|
|
409
496
|
const knownSlashCommandNames = useMemo(() => {
|
|
410
497
|
const names = new Set();
|
|
411
498
|
for (const command of localSlashCommands)
|
|
412
499
|
names.add(command.name);
|
|
413
|
-
for (const command of slashRegistry.list())
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
+
}
|
|
417
508
|
return names;
|
|
418
|
-
}, [skillRegistry, localSlashCommands]);
|
|
509
|
+
}, [allowedSlashCommands, skillRegistry, localSlashCommands]);
|
|
419
510
|
const slashCommandHighlight = useMemo(() => resolveSlashCommandHighlightRange(text, knownSlashCommandNames), [text, knownSlashCommandNames]);
|
|
420
511
|
const fileSuggestions = useMemo(() => {
|
|
421
512
|
if (!atContext || !projectFiles)
|
|
@@ -437,14 +528,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
437
528
|
const insertTextAtCursor = React.useCallback((insertion) => {
|
|
438
529
|
if (!insertion)
|
|
439
530
|
return;
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const before = prev.slice(0, c);
|
|
443
|
-
const after = prev.slice(c);
|
|
444
|
-
return before + insertion + after;
|
|
445
|
-
});
|
|
446
|
-
setCursor((c) => c + insertion.length);
|
|
447
|
-
}, [cursor]);
|
|
531
|
+
updateComposerBuffer((current) => insertComposerText(current, insertion));
|
|
532
|
+
}, [updateComposerBuffer]);
|
|
448
533
|
const addAttachment = React.useCallback((att) => {
|
|
449
534
|
const base = imageLabelStartOverride ?? nextImageLabelStart;
|
|
450
535
|
const index = attachmentCountRef.current;
|
|
@@ -463,8 +548,10 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
463
548
|
// terminal has nothing textual to deliver. Probe the clipboard; if it yields
|
|
464
549
|
// an image, treat the paste as an image attachment. macOS only — Linux/Win
|
|
465
550
|
// terminals don't reliably emit empty pastes on image-only clipboards.
|
|
466
|
-
const tryClipboardImage = React.useCallback(async () => {
|
|
551
|
+
const tryClipboardImage = React.useCallback(async (expectedDraftEpoch) => {
|
|
467
552
|
const { attachment, error } = await ingestClipboardImage();
|
|
553
|
+
if (draftIdentityEpochRef.current !== expectedDraftEpoch)
|
|
554
|
+
return false;
|
|
468
555
|
if (attachment) {
|
|
469
556
|
addAttachment(attachment);
|
|
470
557
|
return true;
|
|
@@ -475,12 +562,13 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
475
562
|
return false;
|
|
476
563
|
}, [addAttachment, notice]);
|
|
477
564
|
usePaste((pasted) => {
|
|
478
|
-
|
|
565
|
+
const pasteOperationId = pasteOperationsRef.current.begin();
|
|
566
|
+
const pasteDraftEpoch = draftIdentityEpochRef.current;
|
|
479
567
|
// Clear the ref after React has committed the paste-driven setState.
|
|
480
568
|
// setTimeout with 0 runs after the current discreteUpdates batch flushes.
|
|
481
569
|
const clearPending = () => {
|
|
482
570
|
setTimeout(() => {
|
|
483
|
-
|
|
571
|
+
pasteOperationsRef.current.finish(pasteOperationId);
|
|
484
572
|
}, 0);
|
|
485
573
|
};
|
|
486
574
|
// Strip orphaned focus-event tails that can appear if focus reporting
|
|
@@ -493,10 +581,29 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
493
581
|
.replace(/\x1b\[I$/, "")
|
|
494
582
|
.replace(/\x1b\[O$/, "")
|
|
495
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
|
+
}
|
|
496
603
|
// Empty paste on macOS usually means "Cmd+V with an image on the clipboard".
|
|
497
604
|
if (clean.length === 0) {
|
|
498
605
|
if (process.platform === "darwin") {
|
|
499
|
-
void tryClipboardImage().finally(clearPending);
|
|
606
|
+
void tryClipboardImage(pasteDraftEpoch).finally(clearPending);
|
|
500
607
|
}
|
|
501
608
|
else {
|
|
502
609
|
clearPending();
|
|
@@ -509,10 +616,11 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
509
616
|
// quietly.
|
|
510
617
|
const bareName = bareImageFilenameFromPaste(clean);
|
|
511
618
|
if (bareName && process.platform === "darwin") {
|
|
512
|
-
void tryClipboardImage()
|
|
619
|
+
void tryClipboardImage(pasteDraftEpoch)
|
|
513
620
|
.then((attached) => {
|
|
514
|
-
if (!attached)
|
|
621
|
+
if (draftIdentityEpochRef.current === pasteDraftEpoch && !attached) {
|
|
515
622
|
insertTextAtCursor(clean);
|
|
623
|
+
}
|
|
516
624
|
})
|
|
517
625
|
.finally(clearPending);
|
|
518
626
|
return;
|
|
@@ -525,9 +633,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
525
633
|
if (imageTokens.length === 0) {
|
|
526
634
|
// Plain text paste — insert into the input at the cursor.
|
|
527
635
|
if (shouldCollapsePastedContent(clean)) {
|
|
528
|
-
|
|
529
|
-
setPastedContentRefs((prev) => [...prev, { marker, content: clean }]);
|
|
530
|
-
insertTextAtCursor(marker);
|
|
636
|
+
updateComposerBuffer((current) => insertComposerPaste(current, clean));
|
|
531
637
|
}
|
|
532
638
|
else {
|
|
533
639
|
insertTextAtCursor(clean);
|
|
@@ -537,6 +643,8 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
537
643
|
}
|
|
538
644
|
const handle = async () => {
|
|
539
645
|
const results = await Promise.all(imageTokens.map((t) => ingestImagePath(t)));
|
|
646
|
+
if (draftIdentityEpochRef.current !== pasteDraftEpoch)
|
|
647
|
+
return;
|
|
540
648
|
const successful = [];
|
|
541
649
|
const errors = [];
|
|
542
650
|
for (let i = 0; i < results.length; i++) {
|
|
@@ -554,10 +662,12 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
554
662
|
if (successful.length === 0 &&
|
|
555
663
|
process.platform === "darwin" &&
|
|
556
664
|
imageTokens.some(isScreenshotTempPath)) {
|
|
557
|
-
const clipOk = await tryClipboardImage();
|
|
665
|
+
const clipOk = await tryClipboardImage(pasteDraftEpoch);
|
|
558
666
|
if (clipOk)
|
|
559
667
|
return;
|
|
560
668
|
}
|
|
669
|
+
if (draftIdentityEpochRef.current !== pasteDraftEpoch)
|
|
670
|
+
return;
|
|
561
671
|
for (const att of successful)
|
|
562
672
|
addAttachment(att);
|
|
563
673
|
const nonImageLines = tokens.filter((t) => !isImageFilePath(t));
|
|
@@ -576,20 +686,16 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
576
686
|
const applyFileSuggestion = (selectedPath) => {
|
|
577
687
|
if (!atContext)
|
|
578
688
|
return;
|
|
579
|
-
const before = text.slice(0, atContext.start);
|
|
580
|
-
const after = text.slice(atContext.end);
|
|
581
689
|
const insert = `@${selectedPath} `;
|
|
582
|
-
|
|
583
|
-
setText(newText);
|
|
584
|
-
setCursor(before.length + insert.length);
|
|
690
|
+
updateComposerBuffer((current) => replaceComposerRange(current, atContext.start, atContext.end, insert));
|
|
585
691
|
setSelectedIndex(0);
|
|
586
692
|
};
|
|
587
|
-
const submitInput = (
|
|
693
|
+
const submitInput = (submittedBuffer, target = "submit") => {
|
|
588
694
|
const labelStartForSubmit = imageLabelStartOverride ?? nextImageLabelStart;
|
|
589
695
|
const inlineLabels = attachments.map((_, index) => imageDisplayLabel(labelStartForSubmit + index));
|
|
590
696
|
// Text-paste markers expand to their content (not replayable); image labels
|
|
591
697
|
// are a composer-only affordance stripped here (replayable via attachments).
|
|
592
|
-
const pasteExpanded =
|
|
698
|
+
const pasteExpanded = expandComposerBuffer(submittedBuffer);
|
|
593
699
|
const expandedText = stripInlineImageLabels(pasteExpanded, inlineLabels);
|
|
594
700
|
if (expandedText.trim().length === 0 && attachments.length === 0)
|
|
595
701
|
return;
|
|
@@ -613,7 +719,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
613
719
|
// A collapsed text-paste marker cannot be safely replayed once its
|
|
614
720
|
// in-memory reference is gone; skip those. Image-label stripping is fine to
|
|
615
721
|
// replay (the attachments are stored on the history entry).
|
|
616
|
-
if (
|
|
722
|
+
if (!hasActiveComposerPastes(submittedBuffer) && (expandedText.trim().length > 0 || attachments.length > 0)) {
|
|
617
723
|
const historyEntry = {
|
|
618
724
|
text: expandedText,
|
|
619
725
|
images: attachments,
|
|
@@ -627,26 +733,23 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
627
733
|
return nextHistory;
|
|
628
734
|
});
|
|
629
735
|
}
|
|
630
|
-
|
|
631
|
-
setCursor(0);
|
|
736
|
+
updateComposerBuffer(() => createComposerBuffer());
|
|
632
737
|
setSelectedIndex(0);
|
|
633
738
|
setAttachments([]);
|
|
634
739
|
attachmentCountRef.current = 0;
|
|
635
740
|
setImageLabelStartOverride(null);
|
|
636
|
-
setPastedContentRefs([]);
|
|
637
|
-
nextPastedContentIndexRef.current = 1;
|
|
638
741
|
setHistoryIndex(null);
|
|
639
|
-
historyDraftRef.current =
|
|
742
|
+
historyDraftRef.current = null;
|
|
743
|
+
invalidateDraftAsyncWork();
|
|
640
744
|
};
|
|
641
|
-
const applySlashEnterAction = (
|
|
642
|
-
const action = resolveSlashEnterAction(
|
|
745
|
+
const applySlashEnterAction = (submittedBuffer) => {
|
|
746
|
+
const action = resolveSlashEnterAction(submittedBuffer.text, slashSuggestions, selectedIndex);
|
|
643
747
|
if (action.kind === "submit") {
|
|
644
|
-
submitInput(
|
|
748
|
+
submitInput(submittedBuffer);
|
|
645
749
|
return true;
|
|
646
750
|
}
|
|
647
751
|
if (action.kind === "complete") {
|
|
648
|
-
|
|
649
|
-
setCursor(action.text.length);
|
|
752
|
+
updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, action.text));
|
|
650
753
|
setSelectedIndex(0);
|
|
651
754
|
return true;
|
|
652
755
|
}
|
|
@@ -676,18 +779,19 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
676
779
|
catch { }
|
|
677
780
|
}
|
|
678
781
|
const enterIntent = resolveInkEnterIntent(input, key);
|
|
782
|
+
if (shouldDeferComposerCommitWhilePastePending(pasteOperationsRef.current.hasPending, enterIntent, key)) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
679
785
|
if (enterIntent === "newline") {
|
|
680
|
-
|
|
681
|
-
setText(next.text);
|
|
682
|
-
setCursor(next.cursor);
|
|
786
|
+
updateComposerBuffer((current) => insertComposerText(current, "\n"));
|
|
683
787
|
setSelectedIndex(0);
|
|
684
788
|
return;
|
|
685
789
|
}
|
|
686
790
|
if (enterIntent === "submit" && input && /[\r\n]/.test(input)) {
|
|
687
791
|
const beforeReturn = input.split(/[\r\n]/)[0] ?? "";
|
|
688
|
-
const
|
|
792
|
+
const nextBuffer = insertComposerText(composerBufferRef.current, beforeReturn);
|
|
689
793
|
if (showSuggestions) {
|
|
690
|
-
if (mode === "slash" && navigable && applySlashEnterAction(
|
|
794
|
+
if (mode === "slash" && navigable && applySlashEnterAction(nextBuffer)) {
|
|
691
795
|
return;
|
|
692
796
|
}
|
|
693
797
|
if (mode === "file") {
|
|
@@ -699,7 +803,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
699
803
|
return;
|
|
700
804
|
}
|
|
701
805
|
}
|
|
702
|
-
submitInput(
|
|
806
|
+
submitInput(nextBuffer);
|
|
703
807
|
return;
|
|
704
808
|
}
|
|
705
809
|
const composerArrowDirection = composerVerticalArrowDirection(key);
|
|
@@ -720,13 +824,12 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
720
824
|
if (key.return || key.tab) {
|
|
721
825
|
if (mode === "slash" && navigable) {
|
|
722
826
|
if (key.return)
|
|
723
|
-
applySlashEnterAction(
|
|
827
|
+
applySlashEnterAction(composerBufferRef.current);
|
|
724
828
|
if (key.tab) {
|
|
725
829
|
const suggestion = slashSuggestions[selectedIndex];
|
|
726
830
|
if (suggestion) {
|
|
727
831
|
const newText = `/${suggestion.name} `;
|
|
728
|
-
|
|
729
|
-
setCursor(newText.length);
|
|
832
|
+
updateComposerBuffer((current) => replaceComposerRange(current, 0, current.text.length, newText));
|
|
730
833
|
setSelectedIndex(0);
|
|
731
834
|
}
|
|
732
835
|
}
|
|
@@ -746,55 +849,44 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
746
849
|
// While the agent runs, Tab queues the composer content for the next
|
|
747
850
|
// turn (Enter steers — handled by the app-level submit routing).
|
|
748
851
|
if (key.tab && !key.shift && onQueue && !showSuggestions) {
|
|
749
|
-
|
|
750
|
-
return;
|
|
751
|
-
submitInput(text, "queue");
|
|
852
|
+
submitInput(composerBufferRef.current, "queue");
|
|
752
853
|
return;
|
|
753
854
|
}
|
|
754
855
|
if (enterIntent === "submit") {
|
|
755
856
|
// A paste is still mid-flight — dropping this Enter avoids submitting
|
|
756
857
|
// an input state that doesn't yet include the paste.
|
|
757
|
-
|
|
758
|
-
return;
|
|
759
|
-
submitInput(text);
|
|
858
|
+
submitInput(composerBufferRef.current);
|
|
760
859
|
return;
|
|
761
860
|
}
|
|
762
861
|
const editAction = resolveComposerEditAction(input, key);
|
|
763
862
|
if (editAction) {
|
|
764
863
|
if (editAction === "word-left") {
|
|
765
|
-
|
|
864
|
+
updateComposerBuffer((current) => moveComposerCursor(current, previousWordBoundary(current.text, current.cursor), "left"));
|
|
766
865
|
}
|
|
767
866
|
else if (editAction === "word-right") {
|
|
768
|
-
|
|
867
|
+
updateComposerBuffer((current) => moveComposerCursor(current, nextWordBoundary(current.text, current.cursor), "right"));
|
|
769
868
|
}
|
|
770
869
|
else if (editAction === "line-start") {
|
|
771
|
-
|
|
870
|
+
updateComposerBuffer((current) => moveComposerCursor(current, lineStartBoundary(current.text, current.cursor), "left"));
|
|
772
871
|
}
|
|
773
872
|
else if (editAction === "line-end") {
|
|
774
|
-
|
|
873
|
+
updateComposerBuffer((current) => moveComposerCursor(current, lineEndBoundary(current.text, current.cursor), "right"));
|
|
775
874
|
}
|
|
776
875
|
else if (editAction === "delete-line-start") {
|
|
777
|
-
|
|
778
|
-
setText(next.text);
|
|
779
|
-
setCursor(next.cursor);
|
|
876
|
+
updateComposerBuffer((current) => replaceComposerRange(current, lineStartBoundary(current.text, current.cursor), current.cursor));
|
|
780
877
|
}
|
|
781
878
|
else {
|
|
782
|
-
|
|
783
|
-
setText(next.text);
|
|
784
|
-
setCursor(next.cursor);
|
|
879
|
+
updateComposerBuffer((current) => replaceComposerRange(current, current.cursor, lineEndBoundary(current.text, current.cursor)));
|
|
785
880
|
}
|
|
786
881
|
setSelectedIndex(0);
|
|
787
882
|
return;
|
|
788
883
|
}
|
|
789
884
|
if (key.backspace) {
|
|
790
|
-
if (cursor > 0) {
|
|
791
|
-
|
|
792
|
-
const after = text.slice(cursor);
|
|
793
|
-
setText(before + after);
|
|
794
|
-
setCursor(cursor - 1);
|
|
885
|
+
if (composerBufferRef.current.cursor > 0) {
|
|
886
|
+
updateComposerBuffer((current) => deleteComposerBackward(current));
|
|
795
887
|
setSelectedIndex(0);
|
|
796
888
|
}
|
|
797
|
-
else if (
|
|
889
|
+
else if (attachmentCountRef.current > 0) {
|
|
798
890
|
// Backspace at position 0 drops the most recent attachment so users
|
|
799
891
|
// can undo a misfired paste without submitting the message.
|
|
800
892
|
setAttachments((prev) => {
|
|
@@ -808,21 +900,17 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
808
900
|
return;
|
|
809
901
|
}
|
|
810
902
|
if (key.delete) {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
setText(next.text);
|
|
814
|
-
setCursor(next.cursor);
|
|
815
|
-
setSelectedIndex(0);
|
|
816
|
-
}
|
|
903
|
+
updateComposerBuffer((current) => deleteComposerForward(current));
|
|
904
|
+
setSelectedIndex(0);
|
|
817
905
|
return;
|
|
818
906
|
}
|
|
819
907
|
if (key.leftArrow) {
|
|
820
|
-
|
|
908
|
+
updateComposerBuffer((current) => moveComposerCursor(current, current.cursor - 1, "left"));
|
|
821
909
|
setSelectedIndex(0);
|
|
822
910
|
return;
|
|
823
911
|
}
|
|
824
912
|
if (key.rightArrow) {
|
|
825
|
-
|
|
913
|
+
updateComposerBuffer((current) => moveComposerCursor(current, current.cursor + 1, "right"));
|
|
826
914
|
setSelectedIndex(0);
|
|
827
915
|
return;
|
|
828
916
|
}
|
|
@@ -841,10 +929,7 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
841
929
|
const printable = input.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "");
|
|
842
930
|
if (!printable)
|
|
843
931
|
return;
|
|
844
|
-
|
|
845
|
-
const after = text.slice(cursor);
|
|
846
|
-
setText(before + printable + after);
|
|
847
|
-
setCursor(cursor + printable.length);
|
|
932
|
+
updateComposerBuffer((current) => insertComposerText(current, printable));
|
|
848
933
|
setSelectedIndex(0);
|
|
849
934
|
}
|
|
850
935
|
});
|
|
@@ -882,18 +967,16 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
882
967
|
if (lastDraftEpochRef.current === draftEpoch)
|
|
883
968
|
return;
|
|
884
969
|
lastDraftEpochRef.current = draftEpoch;
|
|
885
|
-
if (
|
|
970
|
+
if (draftText === undefined)
|
|
886
971
|
return;
|
|
887
|
-
|
|
888
|
-
setCursor(draftText.length);
|
|
972
|
+
updateComposerBuffer(() => createComposerBuffer(draftText));
|
|
889
973
|
setSelectedIndex(0);
|
|
890
974
|
setAttachments([]);
|
|
891
975
|
attachmentCountRef.current = 0;
|
|
892
976
|
setImageLabelStartOverride(null);
|
|
893
|
-
setPastedContentRefs([]);
|
|
894
|
-
nextPastedContentIndexRef.current = 1;
|
|
895
977
|
setHistoryIndex(null);
|
|
896
|
-
historyDraftRef.current =
|
|
978
|
+
historyDraftRef.current = null;
|
|
979
|
+
invalidateDraftAsyncWork();
|
|
897
980
|
onDraftApplied?.();
|
|
898
981
|
}, [draftEpoch, draftText, onDraftApplied]);
|
|
899
982
|
useEffect(() => {
|
|
@@ -948,30 +1031,40 @@ export function InputBox({ onSubmit, onQueue, onPasteNotice, disabled, onArrowDo
|
|
|
948
1031
|
// bottom edge (Down → next message, then back to the in-progress draft).
|
|
949
1032
|
const performVerticalArrowRef = useRef(() => { });
|
|
950
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;
|
|
951
1038
|
if (direction === "up") {
|
|
952
1039
|
if (cursorVisualRow > 0) {
|
|
953
|
-
|
|
1040
|
+
const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow - 1, cursorVisualCol));
|
|
1041
|
+
updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
|
|
954
1042
|
return;
|
|
955
1043
|
}
|
|
956
1044
|
}
|
|
957
1045
|
else {
|
|
958
1046
|
if (cursorVisualRow < visualLines.length - 1) {
|
|
959
|
-
|
|
1047
|
+
const requested = displayCursorToSourceCursor(visualToCursor(visualLines, cursorVisualRow + 1, cursorVisualCol));
|
|
1048
|
+
updateComposerBuffer((current) => moveComposerCursor(current, requested, "nearest"));
|
|
960
1049
|
return;
|
|
961
1050
|
}
|
|
962
1051
|
}
|
|
963
|
-
const result =
|
|
1052
|
+
const result = stepComposerHistory({
|
|
1053
|
+
buffer: composerBufferRef.current,
|
|
1054
|
+
attachments,
|
|
1055
|
+
imageLabelStartOverride,
|
|
1056
|
+
historyIndex,
|
|
1057
|
+
draftSnapshot: historyDraftRef.current,
|
|
1058
|
+
}, history, direction);
|
|
964
1059
|
if (result.changed) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
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;
|
|
972
1066
|
setSelectedIndex(0);
|
|
973
|
-
|
|
974
|
-
nextPastedContentIndexRef.current = 1;
|
|
1067
|
+
invalidateDraftAsyncWork();
|
|
975
1068
|
}
|
|
976
1069
|
else if (direction === "down") {
|
|
977
1070
|
// At the bottom edge with nothing newer in history: hand Down to the
|