@bubblebrain-ai/bubble 0.0.18 → 0.0.20
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/dist/agent/internal-reminder-sanitizer.d.ts +1 -0
- package/dist/agent/internal-reminder-sanitizer.js +46 -0
- package/dist/agent.d.ts +9 -0
- package/dist/agent.js +305 -17
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +28 -0
- package/dist/hooks/config.d.ts +9 -0
- package/dist/hooks/config.js +278 -0
- package/dist/hooks/controller.d.ts +24 -0
- package/dist/hooks/controller.js +254 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/log.d.ts +14 -0
- package/dist/hooks/log.js +54 -0
- package/dist/hooks/runner.d.ts +5 -0
- package/dist/hooks/runner.js +225 -0
- package/dist/hooks/trust.d.ts +37 -0
- package/dist/hooks/trust.js +143 -0
- package/dist/hooks/types.d.ts +173 -0
- package/dist/hooks/types.js +46 -0
- package/dist/main.js +32 -0
- package/dist/memory/prompts.js +3 -1
- package/dist/model-catalog.js +2 -0
- package/dist/model-pricing.js +8 -0
- package/dist/network/chatgpt-transport.js +34 -9
- package/dist/network/provider-transport.d.ts +32 -0
- package/dist/network/provider-transport.js +265 -0
- package/dist/network/retry.d.ts +29 -0
- package/dist/network/retry.js +88 -0
- package/dist/network/system-proxy.d.ts +18 -0
- package/dist/network/system-proxy.js +175 -0
- package/dist/provider-anthropic.d.ts +1 -0
- package/dist/provider-anthropic.js +127 -52
- package/dist/provider-openai-codex.js +19 -29
- package/dist/session-log.js +3 -3
- package/dist/slash-commands/commands.js +84 -0
- package/dist/slash-commands/types.d.ts +2 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.js +4 -4
- package/dist/tui/display-history.d.ts +4 -3
- package/dist/tui/display-history.js +34 -57
- package/dist/tui/display-sanitizer.d.ts +3 -0
- package/dist/tui/display-sanitizer.js +38 -0
- package/dist/tui/paste-placeholder.d.ts +1 -0
- package/dist/tui/paste-placeholder.js +7 -0
- package/dist/tui/run.d.ts +2 -0
- package/dist/tui/run.js +260 -155
- package/dist/tui/trace-groups.js +40 -4
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.js +2 -1
- package/dist/tui-ink/trace-groups.js +40 -4
- package/dist/tui-opentui/app.js +2 -1
- package/dist/tui-opentui/trace-groups.js +40 -4
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
package/dist/tui/run.js
CHANGED
|
@@ -10,7 +10,7 @@ import { AgentAbortError } from "../agent.js";
|
|
|
10
10
|
import { AgentRunInputQueue } from "../agent/input-controller.js";
|
|
11
11
|
import { debugReasoningStream, summarizeDebugText } from "../reasoning-debug.js";
|
|
12
12
|
import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
13
|
-
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
13
|
+
import { createStreamingInternalReminderSanitizer, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "../agent/internal-reminder-sanitizer.js";
|
|
14
14
|
import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceValue, traceEvent, } from "../debug-trace.js";
|
|
15
15
|
import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
16
16
|
import { calculateUsageCost } from "../model-pricing.js";
|
|
@@ -22,7 +22,8 @@ import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
|
22
22
|
import { sourceRank } from "../slash-commands/unified.js";
|
|
23
23
|
import { sidebarMcpRowsFromStates, renderMcpRowMarker } from "./sidebar-mcp.js";
|
|
24
24
|
import { expandAtMentions, filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
25
|
-
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, snapshotDisplayParts, toolCallsFromParts, } from "./display-history.js";
|
|
25
|
+
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, setUserInputStatus, snapshotDisplayParts, toolCallsFromParts, userInputStatusBadgeLabel, } from "./display-history.js";
|
|
26
|
+
import { sanitizeDisplayMessage, sanitizeDisplayMessages } from "./display-sanitizer.js";
|
|
26
27
|
import { createMarkdownSyntaxStyle, createSubtleMarkdownSyntaxStyle } from "./markdown-theme.js";
|
|
27
28
|
import { markdownInlineSegments } from "./markdown-inline.js";
|
|
28
29
|
import { hashString } from "./render-signature.js";
|
|
@@ -41,6 +42,7 @@ import { createFrames } from "./opencode-spinner.js";
|
|
|
41
42
|
import { copyTextToClipboard } from "./clipboard.js";
|
|
42
43
|
import { readGitSidebarState } from "./sidebar-state.js";
|
|
43
44
|
import { buildImageContentPartsFromLabels, extractImagePathTokens, imageAttachmentLabelPattern, resolveComposerImagePaths, resolveImageInput, } from "./image-paste.js";
|
|
45
|
+
import { createPastedContentMarker, decodePastedBytes, expandPastedContentMarkers, shouldCollapsePastedContent, } from "./paste-placeholder.js";
|
|
44
46
|
import { isModeCycleKeyEvent, isModeCycleSequence, isModifiedEnterSequence, PROMPT_TEXTAREA_KEYBINDINGS, } from "./prompt-keybindings.js";
|
|
45
47
|
import { keyNameFromEvent, keyNameFromSequence } from "./global-key-router.js";
|
|
46
48
|
import { EscapeConfirmationGate } from "./escape-confirmation.js";
|
|
@@ -71,6 +73,8 @@ const DEFAULT_THEME = {
|
|
|
71
73
|
info: "#56b6c2",
|
|
72
74
|
text: "#eeeeee",
|
|
73
75
|
textMuted: "#808080",
|
|
76
|
+
selectionBg: "#3D5066",
|
|
77
|
+
selectionFg: "#eeeeee",
|
|
74
78
|
background: "#0a0a0a",
|
|
75
79
|
backgroundPanel: "#141414",
|
|
76
80
|
backgroundElement: "#1e1e1e",
|
|
@@ -113,6 +117,8 @@ const LIGHT_THEME = {
|
|
|
113
117
|
info: "#257E8A",
|
|
114
118
|
text: "#171717",
|
|
115
119
|
textMuted: "#6F7377",
|
|
120
|
+
selectionBg: "#B9D4F7",
|
|
121
|
+
selectionFg: "#171717",
|
|
116
122
|
background: "#FCFCFA",
|
|
117
123
|
backgroundPanel: "#F6F6F3",
|
|
118
124
|
backgroundElement: "#ECEDEA",
|
|
@@ -186,6 +192,9 @@ const PROMPT_SCANNER_INTERVAL_MS = 80;
|
|
|
186
192
|
const SESSION_SIDEBAR_WIDTH = 42;
|
|
187
193
|
const SESSION_SIDEBAR_AUTO_WIDTH = 120;
|
|
188
194
|
const PROVIDER_DIALOG_ROWS = 13;
|
|
195
|
+
const PROVIDER_DIALOG_MIN_WIDTH = 56;
|
|
196
|
+
const PROVIDER_DIALOG_MAX_WIDTH = 84;
|
|
197
|
+
const PROVIDER_DIALOG_ROW_RESERVED_WIDTH = 10;
|
|
189
198
|
const QUESTION_MAX_TABS = 4;
|
|
190
199
|
const QUESTION_MAX_OPTIONS = 10;
|
|
191
200
|
const QUESTION_MAX_CONFIRM_ROWS = 3;
|
|
@@ -221,6 +230,7 @@ const HOME_TIPS = [
|
|
|
221
230
|
"Use /compact to summarize long sessions near context limits",
|
|
222
231
|
"Shift+Enter or Ctrl+J inserts a newline in your prompt",
|
|
223
232
|
];
|
|
233
|
+
const SELECTABLE_TEXT_TAGS = new Set(["text", "textarea", "code", "markdown", "diff", "input"]);
|
|
224
234
|
function h(tag, props, ...children) {
|
|
225
235
|
const allProps = props ?? {};
|
|
226
236
|
const childList = children.length > 0 ? children : allProps.children !== undefined ? [allProps.children] : [];
|
|
@@ -232,6 +242,14 @@ function h(tag, props, ...children) {
|
|
|
232
242
|
}
|
|
233
243
|
const element = createElement(tag);
|
|
234
244
|
const { children: _children, ...rest } = allProps;
|
|
245
|
+
// Without explicit selection colors OpenTUI inverts fg/bg; with our
|
|
246
|
+
// transparent backgrounds that degrades to black-on-black on light themes.
|
|
247
|
+
if (SELECTABLE_TEXT_TAGS.has(tag)) {
|
|
248
|
+
if (rest.selectionBg === undefined)
|
|
249
|
+
rest.selectionBg = theme.selectionBg;
|
|
250
|
+
if (rest.selectionFg === undefined)
|
|
251
|
+
rest.selectionFg = theme.selectionFg;
|
|
252
|
+
}
|
|
235
253
|
spread(element, rest, false);
|
|
236
254
|
if (childList.length === 1)
|
|
237
255
|
insert(element, childList[0]);
|
|
@@ -239,6 +257,27 @@ function h(tag, props, ...children) {
|
|
|
239
257
|
insert(element, childList);
|
|
240
258
|
return element;
|
|
241
259
|
}
|
|
260
|
+
// OpenTUI hardcodes updateCursor=true for mouse-driven selection, so dragging
|
|
261
|
+
// a selection yanks the editor cursor to the drag focus. Keep plain clicks
|
|
262
|
+
// (empty selection) positioning the cursor and keyboard selection intact, but
|
|
263
|
+
// freeze the cursor while a real range is being dragged.
|
|
264
|
+
function preserveCursorOnMouseSelection(ref) {
|
|
265
|
+
const editor = ref?.editorView;
|
|
266
|
+
if (!editor || editor.__bubbleSelectionCursorPatch)
|
|
267
|
+
return;
|
|
268
|
+
editor.__bubbleSelectionCursorPatch = true;
|
|
269
|
+
for (const method of ["setLocalSelection", "updateLocalSelection"]) {
|
|
270
|
+
const original = editor[method]?.bind(editor);
|
|
271
|
+
if (!original)
|
|
272
|
+
continue;
|
|
273
|
+
editor[method] = (anchorX, anchorY, focusX, focusY, bg, fg, updateCursor, followCursor) => {
|
|
274
|
+
const keyboardDriven = ref?._keyboardSelectionActive === true;
|
|
275
|
+
const emptySelection = anchorX === focusX && anchorY === focusY;
|
|
276
|
+
const allowCursorMove = keyboardDriven || emptySelection;
|
|
277
|
+
return original(anchorX, anchorY, focusX, focusY, bg, fg, allowCursorMove ? updateCursor : false, allowCursorMove ? followCursor : false);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
242
281
|
function isDestroyedRenderable(ref) {
|
|
243
282
|
return !ref || ref.isDestroyed === true;
|
|
244
283
|
}
|
|
@@ -438,6 +477,11 @@ function OpenTuiApp(props) {
|
|
|
438
477
|
let promptHistory = initialPromptHistory(displayMessages);
|
|
439
478
|
let nextImageAttachmentIndex = nextImageLabelIndex(displayMessages);
|
|
440
479
|
const pendingImageAttachments = new Map();
|
|
480
|
+
// Long pastes are collapsed to "[Pasted text #N +M lines]" in the composer
|
|
481
|
+
// and expanded back to the full content when the message is submitted,
|
|
482
|
+
// mirroring how image attachments use "[Image #N]" labels.
|
|
483
|
+
const pendingPastedTexts = new Map();
|
|
484
|
+
let nextPastedTextIndex = 1;
|
|
441
485
|
let composerImageResolutionSeq = 0;
|
|
442
486
|
let applyingComposerImageReplacement = false;
|
|
443
487
|
let promptHistoryIndex;
|
|
@@ -467,7 +511,7 @@ function OpenTuiApp(props) {
|
|
|
467
511
|
let copyToastRoot;
|
|
468
512
|
let copyToastText;
|
|
469
513
|
const [sessionActive, setSessionActive] = createSignal(false);
|
|
470
|
-
const [sidebarMode, setSidebarModeState] = createSignal("
|
|
514
|
+
const [sidebarMode, setSidebarModeState] = createSignal("collapsed");
|
|
471
515
|
const [sidebarTick, setSidebarTick] = createSignal(0);
|
|
472
516
|
// Sidebar MCP section collapsed state. Persisted across sidebarTick bumps,
|
|
473
517
|
// only reset on actual mount. Collapse toggle exposed when > 2 servers.
|
|
@@ -502,7 +546,6 @@ function OpenTuiApp(props) {
|
|
|
502
546
|
let providerDialogModelItems;
|
|
503
547
|
let providerDialogModelRefreshId = 0;
|
|
504
548
|
let previousPickerForKey;
|
|
505
|
-
let homePromptRef;
|
|
506
549
|
let sessionPromptRef;
|
|
507
550
|
let scrollbox;
|
|
508
551
|
let transcriptScrollFollowing = true;
|
|
@@ -518,7 +561,6 @@ function OpenTuiApp(props) {
|
|
|
518
561
|
defaultWritesExpanded: false,
|
|
519
562
|
};
|
|
520
563
|
let dock;
|
|
521
|
-
let homeComposerShell;
|
|
522
564
|
let sessionComposerShell;
|
|
523
565
|
const promptScannerSyncs = new Set();
|
|
524
566
|
let approvalRoot;
|
|
@@ -619,9 +661,7 @@ function OpenTuiApp(props) {
|
|
|
619
661
|
const sidebarFileAdditions = [];
|
|
620
662
|
const sidebarFileDeletions = [];
|
|
621
663
|
let sidebarFileSection;
|
|
622
|
-
const activePrompt = () =>
|
|
623
|
-
? homePromptRef ?? sessionPromptRef
|
|
624
|
-
: sessionPromptRef ?? homePromptRef;
|
|
664
|
+
const activePrompt = () => sessionPromptRef;
|
|
625
665
|
function setPromptText(value) {
|
|
626
666
|
promptText = value;
|
|
627
667
|
const prompt = activePrompt();
|
|
@@ -703,7 +743,6 @@ function OpenTuiApp(props) {
|
|
|
703
743
|
return true;
|
|
704
744
|
}
|
|
705
745
|
function blurInputsForModal() {
|
|
706
|
-
homePromptRef?.blur();
|
|
707
746
|
sessionPromptRef?.blur();
|
|
708
747
|
questionCustomInput?.blur();
|
|
709
748
|
providerDialogInput?.blur();
|
|
@@ -760,9 +799,7 @@ function OpenTuiApp(props) {
|
|
|
760
799
|
activePrompt()?.focus();
|
|
761
800
|
}, 0);
|
|
762
801
|
}
|
|
763
|
-
const activeComposerShell = () =>
|
|
764
|
-
? homeComposerShell ?? sessionComposerShell
|
|
765
|
-
: sessionComposerShell ?? homeComposerShell;
|
|
802
|
+
const activeComposerShell = () => sessionComposerShell;
|
|
766
803
|
onCleanup(() => {
|
|
767
804
|
uiDisposed = true;
|
|
768
805
|
if (copyToastClearTimer)
|
|
@@ -894,6 +931,23 @@ function OpenTuiApp(props) {
|
|
|
894
931
|
return dimensions().width > SESSION_SIDEBAR_AUTO_WIDTH;
|
|
895
932
|
};
|
|
896
933
|
const contentWidth = () => Math.max(20, dimensions().width - (sidebarVisible() ? SESSION_SIDEBAR_WIDTH : 0) - 4);
|
|
934
|
+
const liveTerminalDimensions = () => {
|
|
935
|
+
const reactive = dimensions();
|
|
936
|
+
// Some terminal split-pane flows leave OpenTUI's resize signal stale. Node's
|
|
937
|
+
// TTY size is sampled on demand, so use it for modal geometry when present.
|
|
938
|
+
const stdoutWidth = process.stdout.columns;
|
|
939
|
+
const stdoutHeight = process.stdout.rows;
|
|
940
|
+
const width = Number.isFinite(stdoutWidth) && stdoutWidth && stdoutWidth > 0
|
|
941
|
+
? stdoutWidth
|
|
942
|
+
: reactive.width;
|
|
943
|
+
const height = Number.isFinite(stdoutHeight) && stdoutHeight && stdoutHeight > 0
|
|
944
|
+
? stdoutHeight
|
|
945
|
+
: reactive.height;
|
|
946
|
+
return {
|
|
947
|
+
width: Math.max(1, Math.floor(width)),
|
|
948
|
+
height: Math.max(1, Math.floor(height)),
|
|
949
|
+
};
|
|
950
|
+
};
|
|
897
951
|
const bumpSidebar = () => {
|
|
898
952
|
setSidebarTick((value) => value + 1);
|
|
899
953
|
syncSidebarContext();
|
|
@@ -1090,7 +1144,6 @@ function OpenTuiApp(props) {
|
|
|
1090
1144
|
if (!safeSetText(footerModeBadge, footerModeText()))
|
|
1091
1145
|
footerModeBadge = undefined;
|
|
1092
1146
|
}
|
|
1093
|
-
safeRequestRender(homeComposerShell);
|
|
1094
1147
|
safeRequestRender(sessionComposerShell);
|
|
1095
1148
|
safeRequestRender(rootBox);
|
|
1096
1149
|
}
|
|
@@ -1119,7 +1172,6 @@ function OpenTuiApp(props) {
|
|
|
1119
1172
|
if (!safeSetText(label, promptModelTitle()))
|
|
1120
1173
|
promptModelLabels.delete(label);
|
|
1121
1174
|
}
|
|
1122
|
-
safeRequestRender(homeComposerShell);
|
|
1123
1175
|
safeRequestRender(sessionComposerShell);
|
|
1124
1176
|
safeRequestRender(rootBox);
|
|
1125
1177
|
};
|
|
@@ -1367,7 +1419,7 @@ function OpenTuiApp(props) {
|
|
|
1367
1419
|
redrawApprovalPanel();
|
|
1368
1420
|
if (approval || plan)
|
|
1369
1421
|
focusApprovalPanel();
|
|
1370
|
-
redrawTranscript();
|
|
1422
|
+
redrawTranscript(streamingDisplay, displayMessages, { forceFollow: !!approval });
|
|
1371
1423
|
};
|
|
1372
1424
|
function questionStateFromRequest(request) {
|
|
1373
1425
|
return {
|
|
@@ -2327,18 +2379,18 @@ function OpenTuiApp(props) {
|
|
|
2327
2379
|
function isHomeSurfaceActive(extra) {
|
|
2328
2380
|
return !hasTranscriptMessages(extra) && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
|
|
2329
2381
|
}
|
|
2382
|
+
function isComposerHiddenByModal() {
|
|
2383
|
+
return !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
|
|
2384
|
+
}
|
|
2330
2385
|
function syncPromptSurfaces(focus = false) {
|
|
2331
2386
|
const homeActive = isHomeSurfaceActive(streamingDisplay);
|
|
2332
2387
|
const nextSessionActive = !homeActive;
|
|
2333
2388
|
const surfaceChanged = sessionActive() !== nextSessionActive;
|
|
2334
2389
|
setSessionActive(nextSessionActive);
|
|
2335
|
-
const modalComposerHidden = !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
|
|
2336
2390
|
if (homeSurfaceShell)
|
|
2337
2391
|
homeSurfaceShell.visible = homeActive;
|
|
2338
|
-
if (homeComposerShell)
|
|
2339
|
-
homeComposerShell.visible = homeActive && !modalComposerHidden;
|
|
2340
2392
|
if (sessionComposerShell)
|
|
2341
|
-
sessionComposerShell.visible = !
|
|
2393
|
+
sessionComposerShell.visible = !isComposerHiddenByModal();
|
|
2342
2394
|
syncSidebarChrome();
|
|
2343
2395
|
if (focus || surfaceChanged)
|
|
2344
2396
|
setTimeout(() => activePrompt()?.focus(), 0);
|
|
@@ -2362,7 +2414,6 @@ function OpenTuiApp(props) {
|
|
|
2362
2414
|
}
|
|
2363
2415
|
}
|
|
2364
2416
|
try {
|
|
2365
|
-
homeComposerShell?.requestRender();
|
|
2366
2417
|
sessionComposerShell?.requestRender();
|
|
2367
2418
|
rootBox?.requestRender();
|
|
2368
2419
|
}
|
|
@@ -2412,22 +2463,28 @@ function OpenTuiApp(props) {
|
|
|
2412
2463
|
function redrawTranscriptWithQueuedDisplays() {
|
|
2413
2464
|
redrawTranscript(streamingDisplay, displayMessages);
|
|
2414
2465
|
}
|
|
2415
|
-
function
|
|
2466
|
+
function addUserInputStatusDisplay(input, inputStatus) {
|
|
2416
2467
|
const displayId = `queued-${++nextQueuedDisplayId}`;
|
|
2417
2468
|
queuedDisplayMessages = [
|
|
2418
2469
|
...queuedDisplayMessages,
|
|
2419
|
-
{ role: "user", content: input, clientId: displayId,
|
|
2470
|
+
{ role: "user", content: input, clientId: displayId, inputStatus },
|
|
2420
2471
|
];
|
|
2421
2472
|
redrawTranscriptWithQueuedDisplays();
|
|
2422
2473
|
return displayId;
|
|
2423
2474
|
}
|
|
2424
|
-
function
|
|
2475
|
+
function addQueuedUserDisplay(input) {
|
|
2476
|
+
return addUserInputStatusDisplay(input, "queued");
|
|
2477
|
+
}
|
|
2478
|
+
function addPendingSteerUserDisplay(input) {
|
|
2479
|
+
return addUserInputStatusDisplay(input, "pending_steer");
|
|
2480
|
+
}
|
|
2481
|
+
function updateUserInputDisplayStatus(displayId, inputStatus) {
|
|
2425
2482
|
let changed = false;
|
|
2426
2483
|
const update = (message) => {
|
|
2427
2484
|
if (message.clientId !== displayId)
|
|
2428
2485
|
return message;
|
|
2429
2486
|
changed = true;
|
|
2430
|
-
return
|
|
2487
|
+
return setUserInputStatus(message, inputStatus);
|
|
2431
2488
|
};
|
|
2432
2489
|
displayMessages = displayMessages.map(update);
|
|
2433
2490
|
queuedDisplayMessages = queuedDisplayMessages.map(update);
|
|
@@ -2452,11 +2509,14 @@ function OpenTuiApp(props) {
|
|
|
2452
2509
|
return false;
|
|
2453
2510
|
const index = queuedDisplayMessages.findIndex((message) => message.clientId === displayId);
|
|
2454
2511
|
if (index === -1) {
|
|
2455
|
-
return
|
|
2512
|
+
return updateUserInputDisplayStatus(displayId);
|
|
2456
2513
|
}
|
|
2457
2514
|
const message = queuedDisplayMessages[index];
|
|
2458
2515
|
queuedDisplayMessages = queuedDisplayMessages.filter((_, itemIndex) => itemIndex !== index);
|
|
2459
|
-
displayMessages = [
|
|
2516
|
+
displayMessages = [
|
|
2517
|
+
...displayMessages,
|
|
2518
|
+
setUserInputStatus({ ...message, content: message.content || fallbackContent || " " }),
|
|
2519
|
+
];
|
|
2460
2520
|
redrawTranscriptWithQueuedDisplays();
|
|
2461
2521
|
return true;
|
|
2462
2522
|
}
|
|
@@ -2490,7 +2550,7 @@ function OpenTuiApp(props) {
|
|
|
2490
2550
|
}
|
|
2491
2551
|
function requeueRejectedSteer(input, displayId) {
|
|
2492
2552
|
const queuedDisplayId = displayId ?? addQueuedUserDisplay(input);
|
|
2493
|
-
|
|
2553
|
+
updateUserInputDisplayStatus(queuedDisplayId, "queued");
|
|
2494
2554
|
rejectedSteerInputs.push({ input, displayId: queuedDisplayId });
|
|
2495
2555
|
syncQueuedComposerInputCount();
|
|
2496
2556
|
if (!isRunning())
|
|
@@ -2583,9 +2643,12 @@ function OpenTuiApp(props) {
|
|
|
2583
2643
|
queueComposerInput(input, { showInTranscript: true });
|
|
2584
2644
|
return;
|
|
2585
2645
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2646
|
+
// Expand here because steer inputs bypass handleInput; keep the expanded
|
|
2647
|
+
// text in the record so a rejected steer requeues without stale markers.
|
|
2648
|
+
const expandedInput = expandComposerPastedTexts(input);
|
|
2649
|
+
const displayId = addPendingSteerUserDisplay(expandedInput);
|
|
2650
|
+
const pendingInput = run.inputController.enqueue(expandedInput);
|
|
2651
|
+
pendingSteerInputs.push({ id: pendingInput.id, input: expandedInput, displayId });
|
|
2589
2652
|
syncPendingSteerInputCount();
|
|
2590
2653
|
setNotice("Steer pending for next model call");
|
|
2591
2654
|
}
|
|
@@ -2883,12 +2946,16 @@ function OpenTuiApp(props) {
|
|
|
2883
2946
|
updateTranscriptHost(transcriptHost, transcriptState, messages, transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
|
|
2884
2947
|
syncPromptSurfaces();
|
|
2885
2948
|
}
|
|
2886
|
-
function redrawTranscript(extra, baseMessages = displayMessages) {
|
|
2949
|
+
function redrawTranscript(extra, baseMessages = displayMessages, options = {}) {
|
|
2887
2950
|
streamingDisplay = extra;
|
|
2888
|
-
renderTranscriptNow(streamingDisplay, baseMessages);
|
|
2951
|
+
renderTranscriptNow(streamingDisplay, baseMessages, options);
|
|
2889
2952
|
}
|
|
2890
|
-
function renderTranscriptNow(extra, baseMessages = displayMessages) {
|
|
2891
|
-
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
2953
|
+
function renderTranscriptNow(extra, baseMessages = displayMessages, options = {}) {
|
|
2954
|
+
const shouldFollow = options.forceFollow ? true : shouldFollowTranscriptBeforeUpdate();
|
|
2955
|
+
if (options.forceFollow) {
|
|
2956
|
+
transcriptScrollFollowing = true;
|
|
2957
|
+
transcriptScrollInitialized = true;
|
|
2958
|
+
}
|
|
2892
2959
|
const nextMessages = compactDisplayMessages([
|
|
2893
2960
|
...baseMessages,
|
|
2894
2961
|
...(extra ? [extra] : []),
|
|
@@ -2906,6 +2973,7 @@ function OpenTuiApp(props) {
|
|
|
2906
2973
|
syncSidebarChrome();
|
|
2907
2974
|
redrawQuestionPanel();
|
|
2908
2975
|
redrawStatsPanel();
|
|
2976
|
+
redrawProviderDialog();
|
|
2909
2977
|
redrawFeishuSetupPanel();
|
|
2910
2978
|
scrollbox?.requestRender();
|
|
2911
2979
|
scheduleTranscriptScrollAfterUpdate(shouldFollow);
|
|
@@ -3131,11 +3199,13 @@ function OpenTuiApp(props) {
|
|
|
3131
3199
|
providerDialogRoot.requestRender();
|
|
3132
3200
|
return;
|
|
3133
3201
|
}
|
|
3134
|
-
const
|
|
3202
|
+
const terminal = liveTerminalDimensions();
|
|
3203
|
+
const width = providerDialogPanelWidth(terminal.width);
|
|
3135
3204
|
const height = PROVIDER_DIALOG_ROWS + 7;
|
|
3205
|
+
const columnWidths = providerDialogColumnWidths(state, width);
|
|
3136
3206
|
providerDialogRoot.visible = true;
|
|
3137
|
-
providerDialogRoot.width =
|
|
3138
|
-
providerDialogRoot.height =
|
|
3207
|
+
providerDialogRoot.width = terminal.width;
|
|
3208
|
+
providerDialogRoot.height = terminal.height;
|
|
3139
3209
|
providerDialogRoot.left = 0;
|
|
3140
3210
|
providerDialogRoot.top = 0;
|
|
3141
3211
|
providerDialogRoot.backgroundColor = modalBackdropColor();
|
|
@@ -3143,8 +3213,8 @@ function OpenTuiApp(props) {
|
|
|
3143
3213
|
providerDialogPanel.visible = true;
|
|
3144
3214
|
providerDialogPanel.width = width;
|
|
3145
3215
|
providerDialogPanel.height = height;
|
|
3146
|
-
providerDialogPanel.left = Math.max(0, Math.floor((
|
|
3147
|
-
providerDialogPanel.top = Math.max(0, Math.floor(
|
|
3216
|
+
providerDialogPanel.left = Math.max(0, Math.floor((terminal.width - width) / 2));
|
|
3217
|
+
providerDialogPanel.top = Math.max(0, Math.floor(terminal.height / 4));
|
|
3148
3218
|
providerDialogPanel.backgroundColor = theme.backgroundPanel;
|
|
3149
3219
|
providerDialogPanel.borderColor = theme.backgroundPanel;
|
|
3150
3220
|
providerDialogPanel.requestRender();
|
|
@@ -3218,20 +3288,20 @@ function OpenTuiApp(props) {
|
|
|
3218
3288
|
gutter.fg = active ? activeText : providerDialogGutterColor(row.item.gutter ?? (isCurrentModelItem(row.item) ? "●" : undefined));
|
|
3219
3289
|
}
|
|
3220
3290
|
if (label) {
|
|
3221
|
-
label.content = truncate(row.item.label,
|
|
3291
|
+
label.content = truncate(row.item.label, columnWidths.label);
|
|
3222
3292
|
label.fg = active ? activeText : isCurrentModelItem(row.item) ? theme.primary : theme.text;
|
|
3223
3293
|
}
|
|
3224
3294
|
if (detail) {
|
|
3225
3295
|
const detailText = state.query.trim() && state.step === "models"
|
|
3226
3296
|
? row.item.category ?? row.item.detail ?? ""
|
|
3227
3297
|
: row.item.detail ?? "";
|
|
3228
|
-
detail.width =
|
|
3229
|
-
detail.content = truncate(detailText,
|
|
3298
|
+
detail.width = columnWidths.detail;
|
|
3299
|
+
detail.content = truncate(detailText, columnWidths.detail);
|
|
3230
3300
|
detail.fg = active ? activeText : theme.textMuted;
|
|
3231
3301
|
}
|
|
3232
3302
|
if (footer) {
|
|
3233
|
-
footer.width =
|
|
3234
|
-
footer.content = row.item.footer ?? "";
|
|
3303
|
+
footer.width = columnWidths.footer;
|
|
3304
|
+
footer.content = truncate(row.item.footer ?? "", columnWidths.footer);
|
|
3235
3305
|
footer.fg = active ? activeText : theme.textMuted;
|
|
3236
3306
|
}
|
|
3237
3307
|
}
|
|
@@ -3279,14 +3349,17 @@ function OpenTuiApp(props) {
|
|
|
3279
3349
|
return theme.warning;
|
|
3280
3350
|
return theme.textMuted;
|
|
3281
3351
|
}
|
|
3282
|
-
function
|
|
3283
|
-
return
|
|
3284
|
-
}
|
|
3285
|
-
function providerDialogDetailWidth(state) {
|
|
3286
|
-
return state.step === "skills" ? 26 : 16;
|
|
3352
|
+
function providerDialogPanelWidth(terminalWidth) {
|
|
3353
|
+
return Math.max(PROVIDER_DIALOG_MIN_WIDTH, Math.min(PROVIDER_DIALOG_MAX_WIDTH, terminalWidth - 4));
|
|
3287
3354
|
}
|
|
3288
|
-
function
|
|
3289
|
-
|
|
3355
|
+
function providerDialogColumnWidths(state, panelWidth) {
|
|
3356
|
+
const contentWidth = Math.max(24, panelWidth - PROVIDER_DIALOG_ROW_RESERVED_WIDTH);
|
|
3357
|
+
const footer = state.step === "skills" ? 10 : state.step === "providers" ? 9 : 8;
|
|
3358
|
+
const minLabel = state.step === "skills" ? 18 : 24;
|
|
3359
|
+
const desiredDetail = state.step === "skills" ? 30 : state.step === "providers" ? 24 : 16;
|
|
3360
|
+
const detail = Math.max(8, Math.min(desiredDetail, contentWidth - footer - minLabel));
|
|
3361
|
+
const label = Math.max(8, contentWidth - detail - footer);
|
|
3362
|
+
return { label, detail, footer };
|
|
3290
3363
|
}
|
|
3291
3364
|
function isCurrentModelItem(item) {
|
|
3292
3365
|
return item.value === props.agent.model || item.detail?.includes("current");
|
|
@@ -4597,6 +4670,28 @@ function OpenTuiApp(props) {
|
|
|
4597
4670
|
applyingComposerImageReplacement = false;
|
|
4598
4671
|
}
|
|
4599
4672
|
}
|
|
4673
|
+
// Replaces pasted-text markers with their full content. Runs after @mention
|
|
4674
|
+
// expansion so mention-like tokens inside pasted content stay literal.
|
|
4675
|
+
// References stay registered for the whole session so prompt-history recall
|
|
4676
|
+
// and requeued drafts containing a marker expand again on resend.
|
|
4677
|
+
function expandComposerPastedTexts(text) {
|
|
4678
|
+
if (pendingPastedTexts.size === 0 || !text.includes("[Pasted text #"))
|
|
4679
|
+
return text;
|
|
4680
|
+
const references = [...pendingPastedTexts.entries()].map(([marker, content]) => ({ marker, content }));
|
|
4681
|
+
return expandPastedContentMarkers(text, references);
|
|
4682
|
+
}
|
|
4683
|
+
function handleComposerPaste(event) {
|
|
4684
|
+
const text = typeof event.text === "string" ? event.text : decodePastedBytes(event.bytes);
|
|
4685
|
+
if (!text || !shouldCollapsePastedContent(text))
|
|
4686
|
+
return;
|
|
4687
|
+
event.preventDefault?.();
|
|
4688
|
+
const marker = createPastedContentMarker(text, nextPastedTextIndex);
|
|
4689
|
+
nextPastedTextIndex += 1;
|
|
4690
|
+
pendingPastedTexts.set(marker, text);
|
|
4691
|
+
const prompt = activePrompt();
|
|
4692
|
+
prompt?.insertText(marker);
|
|
4693
|
+
onPromptContentChange(readPromptText());
|
|
4694
|
+
}
|
|
4600
4695
|
async function expandTextParts(parts) {
|
|
4601
4696
|
const expandedParts = [];
|
|
4602
4697
|
for (const part of parts) {
|
|
@@ -4607,9 +4702,11 @@ function OpenTuiApp(props) {
|
|
|
4607
4702
|
const expansion = await expandAtMentions(part.text, props.args.cwd);
|
|
4608
4703
|
if (expansion.missing.length)
|
|
4609
4704
|
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
4610
|
-
for (const skipped of expansion.skipped)
|
|
4611
|
-
|
|
4612
|
-
|
|
4705
|
+
for (const skipped of expansion.skipped) {
|
|
4706
|
+
if (skipped.reason !== "too large")
|
|
4707
|
+
addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
|
|
4708
|
+
}
|
|
4709
|
+
expandedParts.push({ type: "text", text: expandComposerPastedTexts(expansion.text) });
|
|
4613
4710
|
}
|
|
4614
4711
|
return expandedParts;
|
|
4615
4712
|
}
|
|
@@ -4635,7 +4732,7 @@ function OpenTuiApp(props) {
|
|
|
4635
4732
|
if (input.startsWith("/")) {
|
|
4636
4733
|
const skillInvocation = parseSkillInvocation(input, skills);
|
|
4637
4734
|
if (skillInvocation) {
|
|
4638
|
-
await runAgentInput(skillInvocation.actualPrompt, input, options);
|
|
4735
|
+
await runAgentInput(expandComposerPastedTexts(skillInvocation.actualPrompt), input, options);
|
|
4639
4736
|
return;
|
|
4640
4737
|
}
|
|
4641
4738
|
const handled = await executeSlash(input, options);
|
|
@@ -4645,9 +4742,11 @@ function OpenTuiApp(props) {
|
|
|
4645
4742
|
const expansion = await expandAtMentions(input, props.args.cwd);
|
|
4646
4743
|
if (expansion.missing.length)
|
|
4647
4744
|
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
4648
|
-
for (const skipped of expansion.skipped)
|
|
4649
|
-
|
|
4650
|
-
|
|
4745
|
+
for (const skipped of expansion.skipped) {
|
|
4746
|
+
if (skipped.reason !== "too large")
|
|
4747
|
+
addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
|
|
4748
|
+
}
|
|
4749
|
+
await runAgentInput(expandComposerPastedTexts(expansion.text), input, options);
|
|
4651
4750
|
}
|
|
4652
4751
|
async function executeSlash(input, options = {}) {
|
|
4653
4752
|
if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
|
|
@@ -4680,6 +4779,7 @@ function OpenTuiApp(props) {
|
|
|
4680
4779
|
skillRegistry: skills,
|
|
4681
4780
|
bashAllowlist: props.options.bashAllowlist,
|
|
4682
4781
|
settingsManager: props.options.settingsManager,
|
|
4782
|
+
hookController: props.options.hookController,
|
|
4683
4783
|
mcpManager: props.options.mcpManager,
|
|
4684
4784
|
lspService,
|
|
4685
4785
|
flushMemory: props.options.flushMemory,
|
|
@@ -5106,10 +5206,13 @@ function OpenTuiApp(props) {
|
|
|
5106
5206
|
return;
|
|
5107
5207
|
}
|
|
5108
5208
|
rememberPromptHistory(displayInput);
|
|
5109
|
-
|
|
5209
|
+
// History keeps the short marker (it expands again on resend); the
|
|
5210
|
+
// transcript shows the full pasted content once the message is sent.
|
|
5211
|
+
const displayContent = expandComposerPastedTexts(displayInput);
|
|
5212
|
+
const reusedQueuedDisplay = promoteQueuedUserDisplay(options.displayId, displayContent);
|
|
5110
5213
|
const nextMessages = reusedQueuedDisplay
|
|
5111
5214
|
? displayMessages
|
|
5112
|
-
: [...displayMessages, { role: "user", content:
|
|
5215
|
+
: [...displayMessages, { role: "user", content: displayContent }];
|
|
5113
5216
|
if (!reusedQueuedDisplay)
|
|
5114
5217
|
displayMessages = nextMessages;
|
|
5115
5218
|
streamingDisplay = undefined;
|
|
@@ -5127,6 +5230,8 @@ function OpenTuiApp(props) {
|
|
|
5127
5230
|
}, { surface: "tui" });
|
|
5128
5231
|
let assistantContent = "";
|
|
5129
5232
|
let assistantReasoning = "";
|
|
5233
|
+
let textDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5234
|
+
let reasoningDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5130
5235
|
const toolCalls = [];
|
|
5131
5236
|
const assistantParts = [];
|
|
5132
5237
|
let turnStartedAt;
|
|
@@ -5141,7 +5246,7 @@ function OpenTuiApp(props) {
|
|
|
5141
5246
|
const buildStreamingDisplay = (status) => {
|
|
5142
5247
|
const currentParts = snapshotDisplayParts(assistantParts);
|
|
5143
5248
|
const partContent = assistantContent || contentFromParts(currentParts);
|
|
5144
|
-
return {
|
|
5249
|
+
return sanitizeDisplayMessage({
|
|
5145
5250
|
role: "assistant",
|
|
5146
5251
|
content: partContent,
|
|
5147
5252
|
reasoning: assistantReasoning || undefined,
|
|
@@ -5150,7 +5255,7 @@ function OpenTuiApp(props) {
|
|
|
5150
5255
|
status,
|
|
5151
5256
|
streaming: true,
|
|
5152
5257
|
turnStartedAt,
|
|
5153
|
-
};
|
|
5258
|
+
});
|
|
5154
5259
|
};
|
|
5155
5260
|
const flushStreamingRedraw = () => {
|
|
5156
5261
|
if (pendingStreamingRedrawTimer === undefined)
|
|
@@ -5180,6 +5285,8 @@ function OpenTuiApp(props) {
|
|
|
5180
5285
|
if (event.type === "turn_start") {
|
|
5181
5286
|
assistantContent = "";
|
|
5182
5287
|
assistantReasoning = "";
|
|
5288
|
+
textDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5289
|
+
reasoningDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5183
5290
|
toolCalls.length = 0;
|
|
5184
5291
|
assistantParts.length = 0;
|
|
5185
5292
|
turnStartedAt = Date.now();
|
|
@@ -5192,22 +5299,42 @@ function OpenTuiApp(props) {
|
|
|
5192
5299
|
});
|
|
5193
5300
|
}
|
|
5194
5301
|
else if (event.type === "text_delta") {
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5302
|
+
const content = textDisplaySanitizer.push(event.content);
|
|
5303
|
+
if (content) {
|
|
5304
|
+
assistantContent += content;
|
|
5305
|
+
appendTextPart(assistantParts, content);
|
|
5306
|
+
scheduleStreamingRedraw();
|
|
5307
|
+
}
|
|
5198
5308
|
}
|
|
5199
5309
|
else if (event.type === "reasoning_delta") {
|
|
5310
|
+
const content = reasoningDisplaySanitizer.push(event.content);
|
|
5311
|
+
if (!content)
|
|
5312
|
+
continue;
|
|
5200
5313
|
debugReasoningStream({
|
|
5201
5314
|
stage: "ui_append",
|
|
5202
5315
|
providerId: props.agent.providerId,
|
|
5203
5316
|
modelId: props.agent.apiModel,
|
|
5204
5317
|
beforeLength: assistantReasoning.length,
|
|
5205
|
-
delta: summarizeDebugText(
|
|
5206
|
-
afterLength: assistantReasoning.length +
|
|
5318
|
+
delta: summarizeDebugText(content),
|
|
5319
|
+
afterLength: assistantReasoning.length + content.length,
|
|
5207
5320
|
});
|
|
5208
|
-
assistantReasoning +=
|
|
5321
|
+
assistantReasoning += content;
|
|
5209
5322
|
scheduleStreamingRedraw();
|
|
5210
5323
|
}
|
|
5324
|
+
else if (event.type === "hook_start") {
|
|
5325
|
+
setNotice(`Hook ${event.eventName}: ${event.hookId}`);
|
|
5326
|
+
}
|
|
5327
|
+
else if (event.type === "hook_end") {
|
|
5328
|
+
if (event.decision === "deny") {
|
|
5329
|
+
setNotice(event.reason ?? `Hook ${event.hookId} denied ${event.eventName}`);
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
else if (event.type === "hook_error") {
|
|
5333
|
+
setNotice(`Hook ${event.hookId} error: ${event.error}`);
|
|
5334
|
+
}
|
|
5335
|
+
else if (event.type === "provider_retry") {
|
|
5336
|
+
setNotice(`Connection interrupted — retrying (${event.attempt}/${event.maxAttempts})…`);
|
|
5337
|
+
}
|
|
5211
5338
|
else if (event.type === "tool_call_start") {
|
|
5212
5339
|
// Insert a streaming placeholder so the user sees feedback the moment
|
|
5213
5340
|
// the model commits to a tool call, instead of waiting for the args
|
|
@@ -5336,6 +5463,15 @@ function OpenTuiApp(props) {
|
|
|
5336
5463
|
clearTimeout(pendingStreamingRedrawTimer);
|
|
5337
5464
|
pendingStreamingRedrawTimer = undefined;
|
|
5338
5465
|
}
|
|
5466
|
+
const flushedText = textDisplaySanitizer.flush();
|
|
5467
|
+
if (flushedText) {
|
|
5468
|
+
assistantContent += flushedText;
|
|
5469
|
+
appendTextPart(assistantParts, flushedText);
|
|
5470
|
+
}
|
|
5471
|
+
const flushedReasoning = reasoningDisplaySanitizer.flush();
|
|
5472
|
+
if (flushedReasoning) {
|
|
5473
|
+
assistantReasoning += flushedReasoning;
|
|
5474
|
+
}
|
|
5339
5475
|
if (event.usage) {
|
|
5340
5476
|
setSidebarUsage((current) => ({
|
|
5341
5477
|
contextTokens: event.usage.promptTokens || current.contextTokens,
|
|
@@ -5351,20 +5487,21 @@ function OpenTuiApp(props) {
|
|
|
5351
5487
|
}
|
|
5352
5488
|
bumpSidebar();
|
|
5353
5489
|
const currentParts = snapshotDisplayParts(assistantParts);
|
|
5354
|
-
const finalContent = assistantContent || contentFromParts(currentParts);
|
|
5490
|
+
const finalContent = sanitizeInternalReminderBlocks(assistantContent || contentFromParts(currentParts));
|
|
5491
|
+
const finalReasoning = sanitizeInternalReasoningText(assistantReasoning);
|
|
5355
5492
|
const finalToolCalls = toolCalls.length > 0
|
|
5356
5493
|
? [...toolCalls]
|
|
5357
5494
|
: toolCallsFromParts(currentParts);
|
|
5358
|
-
const assistantMessage = {
|
|
5495
|
+
const assistantMessage = sanitizeDisplayMessage({
|
|
5359
5496
|
role: "assistant",
|
|
5360
5497
|
content: finalContent,
|
|
5361
|
-
reasoning:
|
|
5498
|
+
reasoning: finalReasoning || undefined,
|
|
5362
5499
|
toolCalls: finalToolCalls.length ? finalToolCalls : undefined,
|
|
5363
5500
|
parts: currentParts.length ? currentParts : undefined,
|
|
5364
5501
|
turnStartedAt,
|
|
5365
5502
|
turnCompletedAt: Date.now(),
|
|
5366
5503
|
turnUsage: event.usage,
|
|
5367
|
-
};
|
|
5504
|
+
});
|
|
5368
5505
|
const nextMessages = hasRenderableMessage(assistantMessage)
|
|
5369
5506
|
? [...displayMessages, assistantMessage]
|
|
5370
5507
|
: displayMessages;
|
|
@@ -5464,16 +5601,16 @@ function OpenTuiApp(props) {
|
|
|
5464
5601
|
return h("box", {
|
|
5465
5602
|
ref: (ref) => {
|
|
5466
5603
|
sessionComposerShell = ref;
|
|
5467
|
-
ref.visible = !
|
|
5604
|
+
ref.visible = !isComposerHiddenByModal();
|
|
5468
5605
|
},
|
|
5469
5606
|
width: "100%",
|
|
5470
5607
|
paddingLeft: 2,
|
|
5471
5608
|
paddingRight: 2,
|
|
5472
5609
|
flexShrink: 0,
|
|
5473
|
-
visible: !
|
|
5610
|
+
visible: !isComposerHiddenByModal(),
|
|
5474
5611
|
}, renderPrompt({
|
|
5475
5612
|
ref: (ref) => { sessionPromptRef = ref; },
|
|
5476
|
-
focused: !
|
|
5613
|
+
focused: !isComposerHiddenByModal(),
|
|
5477
5614
|
onSubmit: submitPrompt,
|
|
5478
5615
|
isFallbackNewlineKey: isTrackedShiftReturn,
|
|
5479
5616
|
onFallbackNewline: () => canInsertPromptNewline() && (activePrompt()?.newLine() ?? false),
|
|
@@ -5488,6 +5625,7 @@ function OpenTuiApp(props) {
|
|
|
5488
5625
|
model: promptModelTitle,
|
|
5489
5626
|
interruptHint: promptStatusText,
|
|
5490
5627
|
tabHint: () => isRunning() ? "queue" : "mode",
|
|
5628
|
+
onPaste: handleComposerPaste,
|
|
5491
5629
|
placeholder: () => {
|
|
5492
5630
|
const approvalState = pendingApproval();
|
|
5493
5631
|
if (approvalState)
|
|
@@ -5508,7 +5646,6 @@ function OpenTuiApp(props) {
|
|
|
5508
5646
|
}));
|
|
5509
5647
|
}
|
|
5510
5648
|
function renderHomeSurface() {
|
|
5511
|
-
const homeHeight = Math.max(16, dimensions().height - 4);
|
|
5512
5649
|
const logoLines = bubbleWordmarkForWidth(dimensions().width);
|
|
5513
5650
|
return h("box", {
|
|
5514
5651
|
ref: (ref) => {
|
|
@@ -5516,7 +5653,8 @@ function OpenTuiApp(props) {
|
|
|
5516
5653
|
ref.visible = isHomeSurfaceActive(streamingDisplay);
|
|
5517
5654
|
},
|
|
5518
5655
|
visible: isHomeSurfaceActive(streamingDisplay),
|
|
5519
|
-
height:
|
|
5656
|
+
height: "100%",
|
|
5657
|
+
minHeight: 0,
|
|
5520
5658
|
flexDirection: "column",
|
|
5521
5659
|
alignItems: "center",
|
|
5522
5660
|
justifyContent: "center",
|
|
@@ -5528,57 +5666,6 @@ function OpenTuiApp(props) {
|
|
|
5528
5666
|
...(props.options.updateNotice
|
|
5529
5667
|
? [h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, h("text", { fg: theme.accent, content: props.options.updateNotice }))]
|
|
5530
5668
|
: []),
|
|
5531
|
-
h("box", { height: 1, minHeight: 0, flexShrink: 1 }),
|
|
5532
|
-
h("box", {
|
|
5533
|
-
ref: (ref) => {
|
|
5534
|
-
homeComposerShell = ref;
|
|
5535
|
-
ref.visible = isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
|
|
5536
|
-
},
|
|
5537
|
-
width: "100%",
|
|
5538
|
-
maxWidth: 75,
|
|
5539
|
-
zIndex: 1000,
|
|
5540
|
-
paddingTop: 1,
|
|
5541
|
-
flexShrink: 0,
|
|
5542
|
-
visible: isHomeSurfaceActive(streamingDisplay) && !pendingQuestion() && !statsPanel && !pendingFeishuSetup(),
|
|
5543
|
-
}, renderPrompt({
|
|
5544
|
-
ref: (ref) => {
|
|
5545
|
-
homePromptRef = ref;
|
|
5546
|
-
if (isHomeSurfaceActive(streamingDisplay))
|
|
5547
|
-
setTimeout(() => ref.focus(), 0);
|
|
5548
|
-
},
|
|
5549
|
-
focused: isHomeSurfaceActive(streamingDisplay),
|
|
5550
|
-
onSubmit: submitPrompt,
|
|
5551
|
-
isFallbackNewlineKey: isTrackedShiftReturn,
|
|
5552
|
-
onFallbackNewline: () => canInsertPromptNewline() && (activePrompt()?.newLine() ?? false),
|
|
5553
|
-
onContentChange: onPromptContentChange,
|
|
5554
|
-
onKeyDown: handlePickerKey,
|
|
5555
|
-
onUiKeyDown: promptUiKeyDown,
|
|
5556
|
-
getText: readPromptText,
|
|
5557
|
-
disabled: () => !!pendingFeedback() || !!statsPanel,
|
|
5558
|
-
mode,
|
|
5559
|
-
registerModeLabel: registerPromptModeLabel,
|
|
5560
|
-
registerModelLabel: registerPromptModelLabel,
|
|
5561
|
-
model: promptModelTitle,
|
|
5562
|
-
interruptHint: promptStatusText,
|
|
5563
|
-
tabHint: () => isRunning() ? "queue" : "mode",
|
|
5564
|
-
placeholder: () => {
|
|
5565
|
-
const approvalState = pendingApproval();
|
|
5566
|
-
if (approvalState)
|
|
5567
|
-
return "Press Enter to approve or Esc to reject";
|
|
5568
|
-
if (pendingQuestion())
|
|
5569
|
-
return "Answer the question below";
|
|
5570
|
-
if (pendingFeedback())
|
|
5571
|
-
return "Describe feedback below";
|
|
5572
|
-
if (statsPanel)
|
|
5573
|
-
return "Stats panel is open";
|
|
5574
|
-
const plan = pendingPlan();
|
|
5575
|
-
if (plan)
|
|
5576
|
-
return "Press Enter to approve plan or Esc to reject";
|
|
5577
|
-
if (isRunning())
|
|
5578
|
-
return "Steer current run...";
|
|
5579
|
-
return `Ask anything... "${homePrompt}"`;
|
|
5580
|
-
},
|
|
5581
|
-
})),
|
|
5582
5669
|
]);
|
|
5583
5670
|
}
|
|
5584
5671
|
function renderQuestionPanelHost() {
|
|
@@ -5656,7 +5743,10 @@ function OpenTuiApp(props) {
|
|
|
5656
5743
|
visible: false,
|
|
5657
5744
|
flexShrink: 0,
|
|
5658
5745
|
}, h("textarea", {
|
|
5659
|
-
ref: (ref) => {
|
|
5746
|
+
ref: (ref) => {
|
|
5747
|
+
preserveCursorOnMouseSelection(ref);
|
|
5748
|
+
questionCustomInput = ref;
|
|
5749
|
+
},
|
|
5660
5750
|
placeholder: "Type your own answer",
|
|
5661
5751
|
placeholderColor: theme.textMuted,
|
|
5662
5752
|
textColor: theme.text,
|
|
@@ -5745,7 +5835,10 @@ function OpenTuiApp(props) {
|
|
|
5745
5835
|
wrapMode: "word",
|
|
5746
5836
|
content: "Creates a public GitHub issue at DylanDDeng/bubble. Review before sending.",
|
|
5747
5837
|
}), h("textarea", {
|
|
5748
|
-
ref: (ref) => {
|
|
5838
|
+
ref: (ref) => {
|
|
5839
|
+
preserveCursorOnMouseSelection(ref);
|
|
5840
|
+
feedbackInput = ref;
|
|
5841
|
+
},
|
|
5749
5842
|
placeholder: "Describe what happened",
|
|
5750
5843
|
placeholderColor: theme.textMuted,
|
|
5751
5844
|
textColor: theme.text,
|
|
@@ -6657,11 +6750,9 @@ function OpenTuiApp(props) {
|
|
|
6657
6750
|
visible: !!approval,
|
|
6658
6751
|
focusable: true,
|
|
6659
6752
|
onKeyDown: handleApprovalKey,
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
bottom: 4,
|
|
6664
|
-
zIndex: 200,
|
|
6753
|
+
width: "100%",
|
|
6754
|
+
flexShrink: 0,
|
|
6755
|
+
marginTop: 1,
|
|
6665
6756
|
backgroundColor: theme.backgroundPanel,
|
|
6666
6757
|
border: ["left"],
|
|
6667
6758
|
borderColor: theme.warning,
|
|
@@ -6841,7 +6932,10 @@ function OpenTuiApp(props) {
|
|
|
6841
6932
|
function renderPrompt(input) {
|
|
6842
6933
|
const transparentBackground = "#00000000";
|
|
6843
6934
|
return h("box", { flexDirection: "column", flexShrink: 0, marginTop: 1 }, h("box", { width: "100%", border: true, borderColor: theme.border, backgroundColor: transparentBackground }, h("box", { flexDirection: "column", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, backgroundColor: transparentBackground }, h("textarea", {
|
|
6844
|
-
ref:
|
|
6935
|
+
ref: (ref) => {
|
|
6936
|
+
preserveCursorOnMouseSelection(ref);
|
|
6937
|
+
input.ref(ref);
|
|
6938
|
+
},
|
|
6845
6939
|
focused: input.focused,
|
|
6846
6940
|
placeholder: input.placeholder(),
|
|
6847
6941
|
placeholderColor: theme.textMuted,
|
|
@@ -6849,8 +6943,10 @@ function renderPrompt(input) {
|
|
|
6849
6943
|
focusedTextColor: theme.text,
|
|
6850
6944
|
backgroundColor: transparentBackground,
|
|
6851
6945
|
focusedBackgroundColor: transparentBackground,
|
|
6946
|
+
cursorColor: theme.primary,
|
|
6852
6947
|
minHeight: 1,
|
|
6853
6948
|
maxHeight: 6,
|
|
6949
|
+
...(input.onPaste ? { onPaste: input.onPaste } : {}),
|
|
6854
6950
|
onContentChange: () => input.onContentChange(input.getText()),
|
|
6855
6951
|
keyBindings: PROMPT_TEXTAREA_KEYBINDINGS,
|
|
6856
6952
|
onKeyDown: (event) => {
|
|
@@ -6965,8 +7061,9 @@ function renderUserMessage(message, index) {
|
|
|
6965
7061
|
const userChildren = [
|
|
6966
7062
|
h("text", { fg: theme.messageUserText, wrapMode: "word" }, message.content || " "),
|
|
6967
7063
|
];
|
|
6968
|
-
|
|
6969
|
-
|
|
7064
|
+
const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
|
|
7065
|
+
if (inputBadge) {
|
|
7066
|
+
userChildren.push(h("box", { paddingTop: 1 }, h("text", { fg: theme.textMuted }, h("span", { bg: theme.primary, fg: theme.background, bold: true }, ` ${inputBadge} `))));
|
|
6970
7067
|
}
|
|
6971
7068
|
return h("box", {
|
|
6972
7069
|
border: ["left"],
|
|
@@ -6977,17 +7074,20 @@ function renderUserMessage(message, index) {
|
|
|
6977
7074
|
}, h("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: theme.backgroundPanel, flexShrink: 0, flexDirection: "column" }, ...userChildren));
|
|
6978
7075
|
}
|
|
6979
7076
|
function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false, width = 80) {
|
|
7077
|
+
message = sanitizeDisplayMessage(message);
|
|
6980
7078
|
const visibleReasoning = showThinking
|
|
6981
|
-
?
|
|
7079
|
+
? sanitizeInternalReasoningText(message.reasoning ?? "").trim()
|
|
6982
7080
|
: "";
|
|
6983
|
-
const
|
|
7081
|
+
const sanitizedContent = sanitizeInternalReminderBlocks(message.content);
|
|
7082
|
+
const modelSwitch = parseModelSwitchMessage(sanitizedContent);
|
|
6984
7083
|
if (modelSwitch && !visibleReasoning && !(message.toolCalls?.length)) {
|
|
6985
7084
|
return renderModelSwitchMessage(modelSwitch);
|
|
6986
7085
|
}
|
|
6987
7086
|
const children = [];
|
|
6988
7087
|
const parts = message.parts ?? [];
|
|
6989
7088
|
const hasParts = parts.length > 0;
|
|
6990
|
-
|
|
7089
|
+
const trimmedContent = sanitizedContent.trim();
|
|
7090
|
+
if (message.status && !visibleReasoning && !trimmedContent && !(message.toolCalls?.length) && !hasParts) {
|
|
6991
7091
|
children.push(h("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0 }, h("text", { fg: theme.messageThinkingText }, assistantStatusLabel(message))));
|
|
6992
7092
|
}
|
|
6993
7093
|
if (visibleReasoning) {
|
|
@@ -7003,7 +7103,6 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
|
|
|
7003
7103
|
fg: theme.messageThinkingContentText,
|
|
7004
7104
|
})));
|
|
7005
7105
|
}
|
|
7006
|
-
const trimmedContent = message.content.trim();
|
|
7007
7106
|
if (hasParts) {
|
|
7008
7107
|
renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace, width, message.streaming === true);
|
|
7009
7108
|
}
|
|
@@ -7041,7 +7140,7 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
|
|
|
7041
7140
|
function renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace, width, streaming) {
|
|
7042
7141
|
for (const part of parts) {
|
|
7043
7142
|
if (part.type === "text") {
|
|
7044
|
-
const content = part.content.trim();
|
|
7143
|
+
const content = sanitizeInternalReminderBlocks(part.content).trim();
|
|
7045
7144
|
if (!content)
|
|
7046
7145
|
continue;
|
|
7047
7146
|
children.push(h("box", {
|
|
@@ -7067,7 +7166,7 @@ function renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace,
|
|
|
7067
7166
|
}
|
|
7068
7167
|
function lastPartHasText(parts) {
|
|
7069
7168
|
const last = parts[parts.length - 1];
|
|
7070
|
-
return last?.type === "text" && !!last.content.trim();
|
|
7169
|
+
return last?.type === "text" && !!sanitizeInternalReminderBlocks(last.content).trim();
|
|
7071
7170
|
}
|
|
7072
7171
|
function parseModelSwitchMessage(content) {
|
|
7073
7172
|
const match = content.trim().match(/^Model switched to (.+)\.$/);
|
|
@@ -7121,7 +7220,7 @@ function renderMarkdownContent(content, syntaxStyle, options) {
|
|
|
7121
7220
|
function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtleSyntaxStyle) {
|
|
7122
7221
|
const showThinking = options?.showThinking ?? true;
|
|
7123
7222
|
const verboseTrace = options?.verboseTrace ?? false;
|
|
7124
|
-
const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
|
|
7223
|
+
const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
|
|
7125
7224
|
const ctx = host.ctx;
|
|
7126
7225
|
const nextEntries = [];
|
|
7127
7226
|
if (!visibleMessages.length && !options?.plan) {
|
|
@@ -7200,6 +7299,7 @@ function transcriptMessageKey(message, index) {
|
|
|
7200
7299
|
return `${index}:${message.role}`;
|
|
7201
7300
|
}
|
|
7202
7301
|
function transcriptMessageSignature(message, compactionExpanded = false) {
|
|
7302
|
+
message = sanitizeDisplayMessage(message);
|
|
7203
7303
|
if (message.role !== "assistant")
|
|
7204
7304
|
return message.role;
|
|
7205
7305
|
if (message.syntheticKind === "ui_compact_card") {
|
|
@@ -7217,12 +7317,13 @@ function transcriptMessageSignature(message, compactionExpanded = false) {
|
|
|
7217
7317
|
}
|
|
7218
7318
|
function updateMessageEntry(entry, message, showThinking = true, compactionExpanded = false, assistantOptions) {
|
|
7219
7319
|
if (message.role === "user") {
|
|
7320
|
+
const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
|
|
7220
7321
|
if (entry.refs.userText)
|
|
7221
7322
|
entry.refs.userText.content = message.content || " ";
|
|
7222
7323
|
if (entry.refs.userQueuedBox)
|
|
7223
|
-
entry.refs.userQueuedBox.visible =
|
|
7324
|
+
entry.refs.userQueuedBox.visible = !!inputBadge;
|
|
7224
7325
|
if (entry.refs.userQueuedText)
|
|
7225
|
-
entry.refs.userQueuedText.content =
|
|
7326
|
+
entry.refs.userQueuedText.content = inputBadge ? ` ${inputBadge} ` : "";
|
|
7226
7327
|
return;
|
|
7227
7328
|
}
|
|
7228
7329
|
if (message.role === "error") {
|
|
@@ -7249,6 +7350,7 @@ function updateMessageEntry(entry, message, showThinking = true, compactionExpan
|
|
|
7249
7350
|
}
|
|
7250
7351
|
}
|
|
7251
7352
|
function updateAssistantEntry(entry, message, showThinking, options) {
|
|
7353
|
+
message = sanitizeDisplayMessage(message);
|
|
7252
7354
|
const content = message.content.trim();
|
|
7253
7355
|
const visibleReasoning = showThinking ? message.reasoning?.trim() ?? "" : "";
|
|
7254
7356
|
const tools = message.toolCalls ?? [];
|
|
@@ -7345,7 +7447,7 @@ function updateAssistantPartEntries(entry, parts, options, streaming) {
|
|
|
7345
7447
|
const key = `part:${index}:${part.type}`;
|
|
7346
7448
|
const previous = previousEntries.get(key);
|
|
7347
7449
|
if (part.type === "text") {
|
|
7348
|
-
const content = part.content.trim();
|
|
7450
|
+
const content = sanitizeInternalReminderBlocks(part.content).trim();
|
|
7349
7451
|
let ref;
|
|
7350
7452
|
if (previous?.kind === "text") {
|
|
7351
7453
|
ref = previous;
|
|
@@ -7896,14 +7998,15 @@ function createUserEntry(ctx, message, index, key, signature) {
|
|
|
7896
7998
|
wrapMode: "word",
|
|
7897
7999
|
});
|
|
7898
8000
|
refs.userText = text;
|
|
7899
|
-
const
|
|
8001
|
+
const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
|
|
8002
|
+
const queuedText = createText(ctx, inputBadge ? ` ${inputBadge} ` : "", {
|
|
7900
8003
|
fg: theme.background,
|
|
7901
8004
|
bg: theme.primary,
|
|
7902
8005
|
});
|
|
7903
8006
|
refs.userQueuedText = queuedText;
|
|
7904
8007
|
const queuedBox = createBox(ctx, {
|
|
7905
8008
|
paddingTop: 1,
|
|
7906
|
-
visible:
|
|
8009
|
+
visible: !!inputBadge,
|
|
7907
8010
|
}, [queuedText]);
|
|
7908
8011
|
refs.userQueuedBox = queuedBox;
|
|
7909
8012
|
const node = createBox(ctx, {
|
|
@@ -7944,6 +8047,7 @@ function createErrorEntry(ctx, message, key, signature) {
|
|
|
7944
8047
|
return { key, signature, node, refs };
|
|
7945
8048
|
}
|
|
7946
8049
|
function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, verboseTrace = false, expandedWrites = new Set(), onToggleWrite) {
|
|
8050
|
+
message = sanitizeDisplayMessage(message);
|
|
7947
8051
|
const modelSwitch = parseModelSwitchMessage(message.content);
|
|
7948
8052
|
if (modelSwitch && !message.reasoning?.trim() && !(message.toolCalls?.length)) {
|
|
7949
8053
|
return createModelSwitchEntry(ctx, modelSwitch, key, signature);
|
|
@@ -8109,7 +8213,7 @@ function createCompactionCardEntry(ctx, message, key, signature, expanded, onTog
|
|
|
8109
8213
|
statsParts.push(`${meta.turns} turn${meta.turns === 1 ? "" : "s"}`);
|
|
8110
8214
|
if (meta?.messages)
|
|
8111
8215
|
statsParts.push(`${meta.messages} message${meta.messages === 1 ? "" : "s"}`);
|
|
8112
|
-
const statsLine = statsParts.length > 0 ? statsParts.join(" · ") : "
|
|
8216
|
+
const statsLine = statsParts.length > 0 ? `${statsParts.join(" · ")} collapsed` : "Collapsed";
|
|
8113
8217
|
const children = [];
|
|
8114
8218
|
const headerRow = createBox(ctx, {
|
|
8115
8219
|
flexDirection: "row",
|
|
@@ -8118,8 +8222,8 @@ function createCompactionCardEntry(ctx, message, key, signature, expanded, onTog
|
|
|
8118
8222
|
alignItems: "center",
|
|
8119
8223
|
}, [
|
|
8120
8224
|
createText(ctx, new StyledText([
|
|
8121
|
-
fg(theme.info)(bold("◈
|
|
8122
|
-
]), { width:
|
|
8225
|
+
fg(theme.info)(bold("◈ Earlier Conversation")),
|
|
8226
|
+
]), { width: 23 }),
|
|
8123
8227
|
createText(ctx, new StyledText([
|
|
8124
8228
|
fg(theme.textMuted)(`─ ${statsLine}`),
|
|
8125
8229
|
])),
|
|
@@ -8716,7 +8820,7 @@ function renderTranscript(messages, options, syntaxStyle, subtleSyntaxStyle) {
|
|
|
8716
8820
|
return items;
|
|
8717
8821
|
}
|
|
8718
8822
|
function renderSessionMessages(messages, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false) {
|
|
8719
|
-
const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
|
|
8823
|
+
const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
|
|
8720
8824
|
if (!visibleMessages.length)
|
|
8721
8825
|
return null;
|
|
8722
8826
|
return visibleMessages.map((message, index) => renderMessage(message, index, syntaxStyle, subtleSyntaxStyle, showThinking, verboseTrace));
|
|
@@ -8724,7 +8828,7 @@ function renderSessionMessages(messages, syntaxStyle, subtleSyntaxStyle, showThi
|
|
|
8724
8828
|
function formatTranscript(messages, options) {
|
|
8725
8829
|
const showThinking = options?.showThinking ?? true;
|
|
8726
8830
|
const verboseTrace = options?.verboseTrace ?? false;
|
|
8727
|
-
const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
|
|
8831
|
+
const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
|
|
8728
8832
|
const chunks = [];
|
|
8729
8833
|
const append = (content, color = theme.text) => {
|
|
8730
8834
|
if (content)
|
|
@@ -8901,6 +9005,7 @@ function renderHomeState(input) {
|
|
|
8901
9005
|
}, h("box", { flexDirection: "column", flexShrink: 0, width: "100%" }, h("text", { fg: theme.text }, ""), h("text", { fg: theme.text }, ""), ...logoLines.map((line) => renderHomeLogoLine(line, width)), h("text", { fg: theme.text }, ""), h("text", { fg: theme.warning }, centerLine(`● Tip ${input.tip}`, width)), cwd ? h("text", { fg: theme.textMuted }, centerLine(` ${cwd}`, width)) : null));
|
|
8902
9006
|
}
|
|
8903
9007
|
function hasRenderableMessage(message, showThinking = true) {
|
|
9008
|
+
message = sanitizeDisplayMessage(message);
|
|
8904
9009
|
if (message.role === "error")
|
|
8905
9010
|
return !!message.content.trim();
|
|
8906
9011
|
if (message.role === "user")
|