@bubblebrain-ai/bubble 0.0.19 → 0.0.21
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 +10 -0
- package/dist/agent.js +310 -18
- package/dist/approval/controller.d.ts +6 -0
- package/dist/approval/controller.js +104 -11
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/debug-trace.js +4 -0
- package/dist/feishu/agent-host/run-driver.js +29 -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 +86 -13
- 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.d.ts +0 -1
- package/dist/network/chatgpt-transport.js +40 -121
- 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/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +164 -0
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/bash.js +4 -0
- package/dist/tools/edit-apply.js +63 -3
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +6 -5
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +2 -2
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- 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/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -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 +568 -223
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +82 -5
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +1 -0
- package/dist/tui/wordmark.js +56 -54
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +303 -248
- package/dist/tui-ink/display-history.d.ts +16 -1
- package/dist/tui-ink/display-history.js +50 -21
- package/dist/tui-ink/footer.d.ts +6 -12
- package/dist/tui-ink/footer.js +10 -29
- package/dist/tui-ink/image-paste.d.ts +59 -0
- package/dist/tui-ink/image-paste.js +277 -0
- package/dist/tui-ink/input-box.d.ts +26 -1
- package/dist/tui-ink/input-box.js +171 -41
- package/dist/tui-ink/message-list.d.ts +1 -1
- package/dist/tui-ink/message-list.js +46 -29
- package/dist/tui-ink/run.d.ts +7 -2
- package/dist/tui-ink/run.js +73 -23
- package/dist/tui-ink/terminal-mouse.d.ts +1 -0
- package/dist/tui-ink/terminal-mouse.js +4 -0
- package/dist/tui-ink/trace-groups.d.ts +16 -0
- package/dist/tui-ink/trace-groups.js +90 -6
- package/dist/tui-ink/transcript-viewport-math.d.ts +11 -0
- package/dist/tui-ink/transcript-viewport-math.js +17 -0
- package/dist/tui-ink/transcript-viewport.d.ts +24 -0
- package/dist/tui-ink/transcript-viewport.js +83 -0
- package/dist/tui-ink/welcome.d.ts +9 -7
- package/dist/tui-ink/welcome.js +7 -33
- 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
|
@@ -6,11 +6,11 @@ import qrTerminal from "qrcode-terminal";
|
|
|
6
6
|
import { existsSync, statSync } from "node:fs";
|
|
7
7
|
import { basename, isAbsolute, resolve as resolvePath } from "node:path";
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
|
-
import { AgentAbortError } from "../agent.js";
|
|
9
|
+
import { AgentAbortError, INTERRUPTED_ASSISTANT_CONTENT } 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";
|
|
@@ -40,14 +41,16 @@ import { submitFeedback, FeedbackSubmitError } from "../feedback/submit.js";
|
|
|
40
41
|
import { createFrames } from "./opencode-spinner.js";
|
|
41
42
|
import { copyTextToClipboard } from "./clipboard.js";
|
|
42
43
|
import { readGitSidebarState } from "./sidebar-state.js";
|
|
43
|
-
import { buildImageContentPartsFromLabels, extractImagePathTokens, imageAttachmentLabelPattern, resolveComposerImagePaths, resolveImageInput, } from "./image-paste.js";
|
|
44
|
+
import { buildImageContentPartsFromLabels, bareImageFilenameFromPaste, extractImagePathTokens, imageAttachmentLabelPattern, imageLabelForPath, ingestClipboardImage, ingestImagePath, isImagePathPaste, splitPastedPaths, 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";
|
|
47
49
|
import { appendHistoryEntry, loadHistorySync, pushHistoryEntry } from "./input-history.js";
|
|
48
|
-
import { buildTraceGroups, traceGroupLabel } from "./trace-groups.js";
|
|
50
|
+
import { buildTraceGroups, executeCommandBlock, shouldInlineExecuteCommand, traceGroupLabel, } from "./trace-groups.js";
|
|
49
51
|
import { sessionDisplayName } from "./session-display.js";
|
|
50
52
|
import { bubbleWordmarkForWidth, bubbleWordmarkLineText, } from "./wordmark.js";
|
|
53
|
+
import { resolveTranscriptScroll } from "./transcript-scroll.js";
|
|
51
54
|
import { bootstrapConfig } from "../feishu/config.js";
|
|
52
55
|
import { ScopeRegistry } from "../feishu/scope/scope-registry.js";
|
|
53
56
|
const treeSitterClient = getTreeSitterClient();
|
|
@@ -71,6 +74,8 @@ const DEFAULT_THEME = {
|
|
|
71
74
|
info: "#56b6c2",
|
|
72
75
|
text: "#eeeeee",
|
|
73
76
|
textMuted: "#808080",
|
|
77
|
+
selectionBg: "#3D5066",
|
|
78
|
+
selectionFg: "#eeeeee",
|
|
74
79
|
background: "#0a0a0a",
|
|
75
80
|
backgroundPanel: "#141414",
|
|
76
81
|
backgroundElement: "#1e1e1e",
|
|
@@ -113,6 +118,8 @@ const LIGHT_THEME = {
|
|
|
113
118
|
info: "#257E8A",
|
|
114
119
|
text: "#171717",
|
|
115
120
|
textMuted: "#6F7377",
|
|
121
|
+
selectionBg: "#B9D4F7",
|
|
122
|
+
selectionFg: "#171717",
|
|
116
123
|
background: "#FCFCFA",
|
|
117
124
|
backgroundPanel: "#F6F6F3",
|
|
118
125
|
backgroundElement: "#ECEDEA",
|
|
@@ -186,6 +193,9 @@ const PROMPT_SCANNER_INTERVAL_MS = 80;
|
|
|
186
193
|
const SESSION_SIDEBAR_WIDTH = 42;
|
|
187
194
|
const SESSION_SIDEBAR_AUTO_WIDTH = 120;
|
|
188
195
|
const PROVIDER_DIALOG_ROWS = 13;
|
|
196
|
+
const PROVIDER_DIALOG_MIN_WIDTH = 56;
|
|
197
|
+
const PROVIDER_DIALOG_MAX_WIDTH = 84;
|
|
198
|
+
const PROVIDER_DIALOG_ROW_RESERVED_WIDTH = 10;
|
|
189
199
|
const QUESTION_MAX_TABS = 4;
|
|
190
200
|
const QUESTION_MAX_OPTIONS = 10;
|
|
191
201
|
const QUESTION_MAX_CONFIRM_ROWS = 3;
|
|
@@ -221,6 +231,7 @@ const HOME_TIPS = [
|
|
|
221
231
|
"Use /compact to summarize long sessions near context limits",
|
|
222
232
|
"Shift+Enter or Ctrl+J inserts a newline in your prompt",
|
|
223
233
|
];
|
|
234
|
+
const SELECTABLE_TEXT_TAGS = new Set(["text", "textarea", "code", "markdown", "diff", "input"]);
|
|
224
235
|
function h(tag, props, ...children) {
|
|
225
236
|
const allProps = props ?? {};
|
|
226
237
|
const childList = children.length > 0 ? children : allProps.children !== undefined ? [allProps.children] : [];
|
|
@@ -232,6 +243,14 @@ function h(tag, props, ...children) {
|
|
|
232
243
|
}
|
|
233
244
|
const element = createElement(tag);
|
|
234
245
|
const { children: _children, ...rest } = allProps;
|
|
246
|
+
// Without explicit selection colors OpenTUI inverts fg/bg; with our
|
|
247
|
+
// transparent backgrounds that degrades to black-on-black on light themes.
|
|
248
|
+
if (SELECTABLE_TEXT_TAGS.has(tag)) {
|
|
249
|
+
if (rest.selectionBg === undefined)
|
|
250
|
+
rest.selectionBg = theme.selectionBg;
|
|
251
|
+
if (rest.selectionFg === undefined)
|
|
252
|
+
rest.selectionFg = theme.selectionFg;
|
|
253
|
+
}
|
|
235
254
|
spread(element, rest, false);
|
|
236
255
|
if (childList.length === 1)
|
|
237
256
|
insert(element, childList[0]);
|
|
@@ -239,6 +258,27 @@ function h(tag, props, ...children) {
|
|
|
239
258
|
insert(element, childList);
|
|
240
259
|
return element;
|
|
241
260
|
}
|
|
261
|
+
// OpenTUI hardcodes updateCursor=true for mouse-driven selection, so dragging
|
|
262
|
+
// a selection yanks the editor cursor to the drag focus. Keep plain clicks
|
|
263
|
+
// (empty selection) positioning the cursor and keyboard selection intact, but
|
|
264
|
+
// freeze the cursor while a real range is being dragged.
|
|
265
|
+
function preserveCursorOnMouseSelection(ref) {
|
|
266
|
+
const editor = ref?.editorView;
|
|
267
|
+
if (!editor || editor.__bubbleSelectionCursorPatch)
|
|
268
|
+
return;
|
|
269
|
+
editor.__bubbleSelectionCursorPatch = true;
|
|
270
|
+
for (const method of ["setLocalSelection", "updateLocalSelection"]) {
|
|
271
|
+
const original = editor[method]?.bind(editor);
|
|
272
|
+
if (!original)
|
|
273
|
+
continue;
|
|
274
|
+
editor[method] = (anchorX, anchorY, focusX, focusY, bg, fg, updateCursor, followCursor) => {
|
|
275
|
+
const keyboardDriven = ref?._keyboardSelectionActive === true;
|
|
276
|
+
const emptySelection = anchorX === focusX && anchorY === focusY;
|
|
277
|
+
const allowCursorMove = keyboardDriven || emptySelection;
|
|
278
|
+
return original(anchorX, anchorY, focusX, focusY, bg, fg, allowCursorMove ? updateCursor : false, allowCursorMove ? followCursor : false);
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
242
282
|
function isDestroyedRenderable(ref) {
|
|
243
283
|
return !ref || ref.isDestroyed === true;
|
|
244
284
|
}
|
|
@@ -438,6 +478,15 @@ function OpenTuiApp(props) {
|
|
|
438
478
|
let promptHistory = initialPromptHistory(displayMessages);
|
|
439
479
|
let nextImageAttachmentIndex = nextImageLabelIndex(displayMessages);
|
|
440
480
|
const pendingImageAttachments = new Map();
|
|
481
|
+
// Image-path pastes insert their [image#N] label immediately and ingest the
|
|
482
|
+
// file in the background; sends await these so a quick Enter can't outrun
|
|
483
|
+
// attachment registration.
|
|
484
|
+
const pendingImageIngestions = new Set();
|
|
485
|
+
// Long pastes are collapsed to "[Pasted text #N +M lines]" in the composer
|
|
486
|
+
// and expanded back to the full content when the message is submitted,
|
|
487
|
+
// mirroring how image attachments use "[Image #N]" labels.
|
|
488
|
+
const pendingPastedTexts = new Map();
|
|
489
|
+
let nextPastedTextIndex = 1;
|
|
441
490
|
let composerImageResolutionSeq = 0;
|
|
442
491
|
let applyingComposerImageReplacement = false;
|
|
443
492
|
let promptHistoryIndex;
|
|
@@ -467,7 +516,7 @@ function OpenTuiApp(props) {
|
|
|
467
516
|
let copyToastRoot;
|
|
468
517
|
let copyToastText;
|
|
469
518
|
const [sessionActive, setSessionActive] = createSignal(false);
|
|
470
|
-
const [sidebarMode, setSidebarModeState] = createSignal("
|
|
519
|
+
const [sidebarMode, setSidebarModeState] = createSignal("collapsed");
|
|
471
520
|
const [sidebarTick, setSidebarTick] = createSignal(0);
|
|
472
521
|
// Sidebar MCP section collapsed state. Persisted across sidebarTick bumps,
|
|
473
522
|
// only reset on actual mount. Collapse toggle exposed when > 2 servers.
|
|
@@ -502,11 +551,14 @@ function OpenTuiApp(props) {
|
|
|
502
551
|
let providerDialogModelItems;
|
|
503
552
|
let providerDialogModelRefreshId = 0;
|
|
504
553
|
let previousPickerForKey;
|
|
505
|
-
let homePromptRef;
|
|
506
554
|
let sessionPromptRef;
|
|
507
555
|
let scrollbox;
|
|
508
556
|
let transcriptScrollFollowing = true;
|
|
509
557
|
let transcriptScrollInitialized = false;
|
|
558
|
+
// Set by forceFollow renders (user sends, approvals). Survives intervening
|
|
559
|
+
// streaming redraws that recompute follow state from the (still-unscrolled)
|
|
560
|
+
// position before the deferred scroll runs; cleared on user mouse scroll.
|
|
561
|
+
let transcriptForceScrollPending = false;
|
|
510
562
|
let rootBox;
|
|
511
563
|
let sidebarShell;
|
|
512
564
|
let homeSurfaceShell;
|
|
@@ -518,7 +570,6 @@ function OpenTuiApp(props) {
|
|
|
518
570
|
defaultWritesExpanded: false,
|
|
519
571
|
};
|
|
520
572
|
let dock;
|
|
521
|
-
let homeComposerShell;
|
|
522
573
|
let sessionComposerShell;
|
|
523
574
|
const promptScannerSyncs = new Set();
|
|
524
575
|
let approvalRoot;
|
|
@@ -596,8 +647,6 @@ function OpenTuiApp(props) {
|
|
|
596
647
|
const providerDialogFooters = [];
|
|
597
648
|
const promptModeLabels = new Set();
|
|
598
649
|
const promptModelLabels = new Set();
|
|
599
|
-
let footerModeBadge;
|
|
600
|
-
let footerTraceBadge;
|
|
601
650
|
let sidebarTokenText;
|
|
602
651
|
let sidebarPercentText;
|
|
603
652
|
let sidebarGaugeText;
|
|
@@ -619,9 +668,7 @@ function OpenTuiApp(props) {
|
|
|
619
668
|
const sidebarFileAdditions = [];
|
|
620
669
|
const sidebarFileDeletions = [];
|
|
621
670
|
let sidebarFileSection;
|
|
622
|
-
const activePrompt = () =>
|
|
623
|
-
? homePromptRef ?? sessionPromptRef
|
|
624
|
-
: sessionPromptRef ?? homePromptRef;
|
|
671
|
+
const activePrompt = () => sessionPromptRef;
|
|
625
672
|
function setPromptText(value) {
|
|
626
673
|
promptText = value;
|
|
627
674
|
const prompt = activePrompt();
|
|
@@ -703,7 +750,6 @@ function OpenTuiApp(props) {
|
|
|
703
750
|
return true;
|
|
704
751
|
}
|
|
705
752
|
function blurInputsForModal() {
|
|
706
|
-
homePromptRef?.blur();
|
|
707
753
|
sessionPromptRef?.blur();
|
|
708
754
|
questionCustomInput?.blur();
|
|
709
755
|
providerDialogInput?.blur();
|
|
@@ -760,9 +806,7 @@ function OpenTuiApp(props) {
|
|
|
760
806
|
activePrompt()?.focus();
|
|
761
807
|
}, 0);
|
|
762
808
|
}
|
|
763
|
-
const activeComposerShell = () =>
|
|
764
|
-
? homeComposerShell ?? sessionComposerShell
|
|
765
|
-
: sessionComposerShell ?? homeComposerShell;
|
|
809
|
+
const activeComposerShell = () => sessionComposerShell;
|
|
766
810
|
onCleanup(() => {
|
|
767
811
|
uiDisposed = true;
|
|
768
812
|
if (copyToastClearTimer)
|
|
@@ -774,7 +818,6 @@ function OpenTuiApp(props) {
|
|
|
774
818
|
feishuSetupAbortController?.abort();
|
|
775
819
|
promptModeLabels.clear();
|
|
776
820
|
promptModelLabels.clear();
|
|
777
|
-
footerModeBadge = undefined;
|
|
778
821
|
});
|
|
779
822
|
function showCopyToast(toast, ttl = 2200) {
|
|
780
823
|
if (copyToastClearTimer)
|
|
@@ -894,6 +937,23 @@ function OpenTuiApp(props) {
|
|
|
894
937
|
return dimensions().width > SESSION_SIDEBAR_AUTO_WIDTH;
|
|
895
938
|
};
|
|
896
939
|
const contentWidth = () => Math.max(20, dimensions().width - (sidebarVisible() ? SESSION_SIDEBAR_WIDTH : 0) - 4);
|
|
940
|
+
const liveTerminalDimensions = () => {
|
|
941
|
+
const reactive = dimensions();
|
|
942
|
+
// Some terminal split-pane flows leave OpenTUI's resize signal stale. Node's
|
|
943
|
+
// TTY size is sampled on demand, so use it for modal geometry when present.
|
|
944
|
+
const stdoutWidth = process.stdout.columns;
|
|
945
|
+
const stdoutHeight = process.stdout.rows;
|
|
946
|
+
const width = Number.isFinite(stdoutWidth) && stdoutWidth && stdoutWidth > 0
|
|
947
|
+
? stdoutWidth
|
|
948
|
+
: reactive.width;
|
|
949
|
+
const height = Number.isFinite(stdoutHeight) && stdoutHeight && stdoutHeight > 0
|
|
950
|
+
? stdoutHeight
|
|
951
|
+
: reactive.height;
|
|
952
|
+
return {
|
|
953
|
+
width: Math.max(1, Math.floor(width)),
|
|
954
|
+
height: Math.max(1, Math.floor(height)),
|
|
955
|
+
};
|
|
956
|
+
};
|
|
897
957
|
const bumpSidebar = () => {
|
|
898
958
|
setSidebarTick((value) => value + 1);
|
|
899
959
|
syncSidebarContext();
|
|
@@ -1075,9 +1135,7 @@ function OpenTuiApp(props) {
|
|
|
1075
1135
|
}
|
|
1076
1136
|
const promptModeTitle = () => mode() === "plan" ? "Plan" : "Build";
|
|
1077
1137
|
const promptModeBadge = () => promptModeBadgeContent(mode());
|
|
1078
|
-
const footerModeText = () => footerPermissionModeText(mode());
|
|
1079
1138
|
const effectiveShowThinking = () => showThinking() || verboseTrace();
|
|
1080
|
-
const footerTraceText = () => footerTraceModeText(verboseTrace());
|
|
1081
1139
|
function syncModeChrome() {
|
|
1082
1140
|
if (uiDisposed)
|
|
1083
1141
|
return;
|
|
@@ -1085,23 +1143,12 @@ function OpenTuiApp(props) {
|
|
|
1085
1143
|
if (!safeSetText(label, promptModeBadge()))
|
|
1086
1144
|
promptModeLabels.delete(label);
|
|
1087
1145
|
}
|
|
1088
|
-
if (footerModeBadge) {
|
|
1089
|
-
footerModeBadge.fg = permissionModeColor(mode());
|
|
1090
|
-
if (!safeSetText(footerModeBadge, footerModeText()))
|
|
1091
|
-
footerModeBadge = undefined;
|
|
1092
|
-
}
|
|
1093
|
-
safeRequestRender(homeComposerShell);
|
|
1094
1146
|
safeRequestRender(sessionComposerShell);
|
|
1095
1147
|
safeRequestRender(rootBox);
|
|
1096
1148
|
}
|
|
1097
1149
|
function syncTraceChrome() {
|
|
1098
1150
|
if (uiDisposed)
|
|
1099
1151
|
return;
|
|
1100
|
-
if (footerTraceBadge) {
|
|
1101
|
-
footerTraceBadge.fg = verboseTrace() ? theme.warning : theme.textMuted;
|
|
1102
|
-
if (!safeSetText(footerTraceBadge, footerTraceText()))
|
|
1103
|
-
footerTraceBadge = undefined;
|
|
1104
|
-
}
|
|
1105
1152
|
safeRequestRender(rootBox);
|
|
1106
1153
|
}
|
|
1107
1154
|
const registerPromptModeLabel = (ref) => {
|
|
@@ -1119,7 +1166,6 @@ function OpenTuiApp(props) {
|
|
|
1119
1166
|
if (!safeSetText(label, promptModelTitle()))
|
|
1120
1167
|
promptModelLabels.delete(label);
|
|
1121
1168
|
}
|
|
1122
|
-
safeRequestRender(homeComposerShell);
|
|
1123
1169
|
safeRequestRender(sessionComposerShell);
|
|
1124
1170
|
safeRequestRender(rootBox);
|
|
1125
1171
|
};
|
|
@@ -1130,21 +1176,6 @@ function OpenTuiApp(props) {
|
|
|
1130
1176
|
if (!safeSetText(ref, promptModelTitle()))
|
|
1131
1177
|
promptModelLabels.delete(ref);
|
|
1132
1178
|
};
|
|
1133
|
-
const registerFooterModeBadge = (ref) => {
|
|
1134
|
-
if (uiDisposed)
|
|
1135
|
-
return;
|
|
1136
|
-
footerModeBadge = ref;
|
|
1137
|
-
if (!safeSetText(ref, footerModeText()))
|
|
1138
|
-
footerModeBadge = undefined;
|
|
1139
|
-
};
|
|
1140
|
-
const registerFooterTraceBadge = (ref) => {
|
|
1141
|
-
if (uiDisposed)
|
|
1142
|
-
return;
|
|
1143
|
-
footerTraceBadge = ref;
|
|
1144
|
-
ref.fg = verboseTrace() ? theme.warning : theme.textMuted;
|
|
1145
|
-
if (!safeSetText(ref, footerTraceText()))
|
|
1146
|
-
footerTraceBadge = undefined;
|
|
1147
|
-
};
|
|
1148
1179
|
const cycleMode = () => {
|
|
1149
1180
|
if (picker || pendingPlan() || isRunning())
|
|
1150
1181
|
return false;
|
|
@@ -1367,7 +1398,7 @@ function OpenTuiApp(props) {
|
|
|
1367
1398
|
redrawApprovalPanel();
|
|
1368
1399
|
if (approval || plan)
|
|
1369
1400
|
focusApprovalPanel();
|
|
1370
|
-
redrawTranscript();
|
|
1401
|
+
redrawTranscript(streamingDisplay, displayMessages, { forceFollow: !!approval });
|
|
1371
1402
|
};
|
|
1372
1403
|
function questionStateFromRequest(request) {
|
|
1373
1404
|
return {
|
|
@@ -1450,7 +1481,13 @@ function OpenTuiApp(props) {
|
|
|
1450
1481
|
setTimeout(() => {
|
|
1451
1482
|
if (!scrollbox)
|
|
1452
1483
|
return;
|
|
1453
|
-
|
|
1484
|
+
const action = resolveTranscriptScroll({
|
|
1485
|
+
forcePending: transcriptForceScrollPending,
|
|
1486
|
+
shouldFollow,
|
|
1487
|
+
following: transcriptScrollFollowing,
|
|
1488
|
+
});
|
|
1489
|
+
if (action === "scroll-bottom") {
|
|
1490
|
+
transcriptForceScrollPending = false;
|
|
1454
1491
|
scrollTranscriptToBottom();
|
|
1455
1492
|
}
|
|
1456
1493
|
else {
|
|
@@ -1459,6 +1496,7 @@ function OpenTuiApp(props) {
|
|
|
1459
1496
|
}, delay);
|
|
1460
1497
|
}
|
|
1461
1498
|
function handleTranscriptMouseScroll() {
|
|
1499
|
+
transcriptForceScrollPending = false;
|
|
1462
1500
|
setTimeout(updateTranscriptScrollFollowingFromPosition, 0);
|
|
1463
1501
|
}
|
|
1464
1502
|
function syncQuestionUI(focusCustom = false) {
|
|
@@ -2327,18 +2365,18 @@ function OpenTuiApp(props) {
|
|
|
2327
2365
|
function isHomeSurfaceActive(extra) {
|
|
2328
2366
|
return !hasTranscriptMessages(extra) && !pendingPlan() && !pendingQuestion() && !pendingFeedback() && !statsPanel && !pendingFeishuSetup();
|
|
2329
2367
|
}
|
|
2368
|
+
function isComposerHiddenByModal() {
|
|
2369
|
+
return !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
|
|
2370
|
+
}
|
|
2330
2371
|
function syncPromptSurfaces(focus = false) {
|
|
2331
2372
|
const homeActive = isHomeSurfaceActive(streamingDisplay);
|
|
2332
2373
|
const nextSessionActive = !homeActive;
|
|
2333
2374
|
const surfaceChanged = sessionActive() !== nextSessionActive;
|
|
2334
2375
|
setSessionActive(nextSessionActive);
|
|
2335
|
-
const modalComposerHidden = !!pendingQuestion() || !!pendingFeedback() || !!statsPanel || !!pendingFeishuSetup();
|
|
2336
2376
|
if (homeSurfaceShell)
|
|
2337
2377
|
homeSurfaceShell.visible = homeActive;
|
|
2338
|
-
if (homeComposerShell)
|
|
2339
|
-
homeComposerShell.visible = homeActive && !modalComposerHidden;
|
|
2340
2378
|
if (sessionComposerShell)
|
|
2341
|
-
sessionComposerShell.visible = !
|
|
2379
|
+
sessionComposerShell.visible = !isComposerHiddenByModal();
|
|
2342
2380
|
syncSidebarChrome();
|
|
2343
2381
|
if (focus || surfaceChanged)
|
|
2344
2382
|
setTimeout(() => activePrompt()?.focus(), 0);
|
|
@@ -2362,7 +2400,6 @@ function OpenTuiApp(props) {
|
|
|
2362
2400
|
}
|
|
2363
2401
|
}
|
|
2364
2402
|
try {
|
|
2365
|
-
homeComposerShell?.requestRender();
|
|
2366
2403
|
sessionComposerShell?.requestRender();
|
|
2367
2404
|
rootBox?.requestRender();
|
|
2368
2405
|
}
|
|
@@ -2409,25 +2446,33 @@ function OpenTuiApp(props) {
|
|
|
2409
2446
|
function queuedInputLabel(count = queuedInputCount()) {
|
|
2410
2447
|
return `${count} queued message${count === 1 ? "" : "s"}`;
|
|
2411
2448
|
}
|
|
2412
|
-
function redrawTranscriptWithQueuedDisplays() {
|
|
2413
|
-
redrawTranscript(streamingDisplay, displayMessages);
|
|
2449
|
+
function redrawTranscriptWithQueuedDisplays(options = {}) {
|
|
2450
|
+
redrawTranscript(streamingDisplay, displayMessages, options);
|
|
2414
2451
|
}
|
|
2415
|
-
function
|
|
2452
|
+
function addUserInputStatusDisplay(input, inputStatus) {
|
|
2416
2453
|
const displayId = `queued-${++nextQueuedDisplayId}`;
|
|
2417
2454
|
queuedDisplayMessages = [
|
|
2418
2455
|
...queuedDisplayMessages,
|
|
2419
|
-
{ role: "user", content: input, clientId: displayId,
|
|
2456
|
+
{ role: "user", content: input, clientId: displayId, inputStatus },
|
|
2420
2457
|
];
|
|
2421
|
-
|
|
2458
|
+
// Sending a message is explicit user intent to look at the newest turn:
|
|
2459
|
+
// snap to the bottom even if the transcript was scrolled up.
|
|
2460
|
+
redrawTranscriptWithQueuedDisplays({ forceFollow: true });
|
|
2422
2461
|
return displayId;
|
|
2423
2462
|
}
|
|
2424
|
-
function
|
|
2463
|
+
function addQueuedUserDisplay(input) {
|
|
2464
|
+
return addUserInputStatusDisplay(input, "queued");
|
|
2465
|
+
}
|
|
2466
|
+
function addPendingSteerUserDisplay(input) {
|
|
2467
|
+
return addUserInputStatusDisplay(input, "pending_steer");
|
|
2468
|
+
}
|
|
2469
|
+
function updateUserInputDisplayStatus(displayId, inputStatus) {
|
|
2425
2470
|
let changed = false;
|
|
2426
2471
|
const update = (message) => {
|
|
2427
2472
|
if (message.clientId !== displayId)
|
|
2428
2473
|
return message;
|
|
2429
2474
|
changed = true;
|
|
2430
|
-
return
|
|
2475
|
+
return setUserInputStatus(message, inputStatus);
|
|
2431
2476
|
};
|
|
2432
2477
|
displayMessages = displayMessages.map(update);
|
|
2433
2478
|
queuedDisplayMessages = queuedDisplayMessages.map(update);
|
|
@@ -2452,11 +2497,14 @@ function OpenTuiApp(props) {
|
|
|
2452
2497
|
return false;
|
|
2453
2498
|
const index = queuedDisplayMessages.findIndex((message) => message.clientId === displayId);
|
|
2454
2499
|
if (index === -1) {
|
|
2455
|
-
return
|
|
2500
|
+
return updateUserInputDisplayStatus(displayId);
|
|
2456
2501
|
}
|
|
2457
2502
|
const message = queuedDisplayMessages[index];
|
|
2458
2503
|
queuedDisplayMessages = queuedDisplayMessages.filter((_, itemIndex) => itemIndex !== index);
|
|
2459
|
-
displayMessages = [
|
|
2504
|
+
displayMessages = [
|
|
2505
|
+
...displayMessages,
|
|
2506
|
+
setUserInputStatus({ ...message, content: message.content || fallbackContent || " " }),
|
|
2507
|
+
];
|
|
2460
2508
|
redrawTranscriptWithQueuedDisplays();
|
|
2461
2509
|
return true;
|
|
2462
2510
|
}
|
|
@@ -2490,7 +2538,7 @@ function OpenTuiApp(props) {
|
|
|
2490
2538
|
}
|
|
2491
2539
|
function requeueRejectedSteer(input, displayId) {
|
|
2492
2540
|
const queuedDisplayId = displayId ?? addQueuedUserDisplay(input);
|
|
2493
|
-
|
|
2541
|
+
updateUserInputDisplayStatus(queuedDisplayId, "queued");
|
|
2494
2542
|
rejectedSteerInputs.push({ input, displayId: queuedDisplayId });
|
|
2495
2543
|
syncQueuedComposerInputCount();
|
|
2496
2544
|
if (!isRunning())
|
|
@@ -2583,9 +2631,12 @@ function OpenTuiApp(props) {
|
|
|
2583
2631
|
queueComposerInput(input, { showInTranscript: true });
|
|
2584
2632
|
return;
|
|
2585
2633
|
}
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2634
|
+
// Expand here because steer inputs bypass handleInput; keep the expanded
|
|
2635
|
+
// text in the record so a rejected steer requeues without stale markers.
|
|
2636
|
+
const expandedInput = expandComposerPastedTexts(input);
|
|
2637
|
+
const displayId = addPendingSteerUserDisplay(expandedInput);
|
|
2638
|
+
const pendingInput = run.inputController.enqueue(expandedInput);
|
|
2639
|
+
pendingSteerInputs.push({ id: pendingInput.id, input: expandedInput, displayId });
|
|
2589
2640
|
syncPendingSteerInputCount();
|
|
2590
2641
|
setNotice("Steer pending for next model call");
|
|
2591
2642
|
}
|
|
@@ -2883,12 +2934,17 @@ function OpenTuiApp(props) {
|
|
|
2883
2934
|
updateTranscriptHost(transcriptHost, transcriptState, messages, transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
|
|
2884
2935
|
syncPromptSurfaces();
|
|
2885
2936
|
}
|
|
2886
|
-
function redrawTranscript(extra, baseMessages = displayMessages) {
|
|
2937
|
+
function redrawTranscript(extra, baseMessages = displayMessages, options = {}) {
|
|
2887
2938
|
streamingDisplay = extra;
|
|
2888
|
-
renderTranscriptNow(streamingDisplay, baseMessages);
|
|
2939
|
+
renderTranscriptNow(streamingDisplay, baseMessages, options);
|
|
2889
2940
|
}
|
|
2890
|
-
function renderTranscriptNow(extra, baseMessages = displayMessages) {
|
|
2891
|
-
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
2941
|
+
function renderTranscriptNow(extra, baseMessages = displayMessages, options = {}) {
|
|
2942
|
+
const shouldFollow = options.forceFollow ? true : shouldFollowTranscriptBeforeUpdate();
|
|
2943
|
+
if (options.forceFollow) {
|
|
2944
|
+
transcriptScrollFollowing = true;
|
|
2945
|
+
transcriptScrollInitialized = true;
|
|
2946
|
+
transcriptForceScrollPending = true;
|
|
2947
|
+
}
|
|
2892
2948
|
const nextMessages = compactDisplayMessages([
|
|
2893
2949
|
...baseMessages,
|
|
2894
2950
|
...(extra ? [extra] : []),
|
|
@@ -2906,6 +2962,7 @@ function OpenTuiApp(props) {
|
|
|
2906
2962
|
syncSidebarChrome();
|
|
2907
2963
|
redrawQuestionPanel();
|
|
2908
2964
|
redrawStatsPanel();
|
|
2965
|
+
redrawProviderDialog();
|
|
2909
2966
|
redrawFeishuSetupPanel();
|
|
2910
2967
|
scrollbox?.requestRender();
|
|
2911
2968
|
scheduleTranscriptScrollAfterUpdate(shouldFollow);
|
|
@@ -2980,7 +3037,12 @@ function OpenTuiApp(props) {
|
|
|
2980
3037
|
step,
|
|
2981
3038
|
providerId,
|
|
2982
3039
|
query: "",
|
|
2983
|
-
index: step === "models"
|
|
3040
|
+
index: step === "models"
|
|
3041
|
+
? preferredPickerIndex("model", items)
|
|
3042
|
+
// "(current)" sits at the bottom of the rewind list and is the safe default.
|
|
3043
|
+
: step === "rewind"
|
|
3044
|
+
? Math.max(0, items.length - 1)
|
|
3045
|
+
: 0,
|
|
2984
3046
|
apiKey: "",
|
|
2985
3047
|
};
|
|
2986
3048
|
activePrompt()?.clear();
|
|
@@ -3011,6 +3073,10 @@ function OpenTuiApp(props) {
|
|
|
3011
3073
|
return providerId ? buildPickerItems("provider-auth", providerId) : [];
|
|
3012
3074
|
if (step === "skills")
|
|
3013
3075
|
return buildSkillItems();
|
|
3076
|
+
if (step === "rewind")
|
|
3077
|
+
return buildRewindPickerItems();
|
|
3078
|
+
if (step === "rewind-action")
|
|
3079
|
+
return buildRewindActionItems(providerId);
|
|
3014
3080
|
if (step === "models") {
|
|
3015
3081
|
if (providerDialogModelItems?.key === modelPickerCacheKey(providerId)) {
|
|
3016
3082
|
return providerDialogModelItems.items;
|
|
@@ -3131,11 +3197,13 @@ function OpenTuiApp(props) {
|
|
|
3131
3197
|
providerDialogRoot.requestRender();
|
|
3132
3198
|
return;
|
|
3133
3199
|
}
|
|
3134
|
-
const
|
|
3200
|
+
const terminal = liveTerminalDimensions();
|
|
3201
|
+
const width = providerDialogPanelWidth(terminal.width);
|
|
3135
3202
|
const height = PROVIDER_DIALOG_ROWS + 7;
|
|
3203
|
+
const columnWidths = providerDialogColumnWidths(state, width);
|
|
3136
3204
|
providerDialogRoot.visible = true;
|
|
3137
|
-
providerDialogRoot.width =
|
|
3138
|
-
providerDialogRoot.height =
|
|
3205
|
+
providerDialogRoot.width = terminal.width;
|
|
3206
|
+
providerDialogRoot.height = terminal.height;
|
|
3139
3207
|
providerDialogRoot.left = 0;
|
|
3140
3208
|
providerDialogRoot.top = 0;
|
|
3141
3209
|
providerDialogRoot.backgroundColor = modalBackdropColor();
|
|
@@ -3143,8 +3211,8 @@ function OpenTuiApp(props) {
|
|
|
3143
3211
|
providerDialogPanel.visible = true;
|
|
3144
3212
|
providerDialogPanel.width = width;
|
|
3145
3213
|
providerDialogPanel.height = height;
|
|
3146
|
-
providerDialogPanel.left = Math.max(0, Math.floor((
|
|
3147
|
-
providerDialogPanel.top = Math.max(0, Math.floor(
|
|
3214
|
+
providerDialogPanel.left = Math.max(0, Math.floor((terminal.width - width) / 2));
|
|
3215
|
+
providerDialogPanel.top = Math.max(0, Math.floor(terminal.height / 4));
|
|
3148
3216
|
providerDialogPanel.backgroundColor = theme.backgroundPanel;
|
|
3149
3217
|
providerDialogPanel.borderColor = theme.backgroundPanel;
|
|
3150
3218
|
providerDialogPanel.requestRender();
|
|
@@ -3218,20 +3286,20 @@ function OpenTuiApp(props) {
|
|
|
3218
3286
|
gutter.fg = active ? activeText : providerDialogGutterColor(row.item.gutter ?? (isCurrentModelItem(row.item) ? "●" : undefined));
|
|
3219
3287
|
}
|
|
3220
3288
|
if (label) {
|
|
3221
|
-
label.content = truncate(row.item.label,
|
|
3289
|
+
label.content = truncate(row.item.label, columnWidths.label);
|
|
3222
3290
|
label.fg = active ? activeText : isCurrentModelItem(row.item) ? theme.primary : theme.text;
|
|
3223
3291
|
}
|
|
3224
3292
|
if (detail) {
|
|
3225
3293
|
const detailText = state.query.trim() && state.step === "models"
|
|
3226
3294
|
? row.item.category ?? row.item.detail ?? ""
|
|
3227
3295
|
: row.item.detail ?? "";
|
|
3228
|
-
detail.width =
|
|
3229
|
-
detail.content = truncate(detailText,
|
|
3296
|
+
detail.width = columnWidths.detail;
|
|
3297
|
+
detail.content = truncate(detailText, columnWidths.detail);
|
|
3230
3298
|
detail.fg = active ? activeText : theme.textMuted;
|
|
3231
3299
|
}
|
|
3232
3300
|
if (footer) {
|
|
3233
|
-
footer.width =
|
|
3234
|
-
footer.content = row.item.footer ?? "";
|
|
3301
|
+
footer.width = columnWidths.footer;
|
|
3302
|
+
footer.content = truncate(row.item.footer ?? "", columnWidths.footer);
|
|
3235
3303
|
footer.fg = active ? activeText : theme.textMuted;
|
|
3236
3304
|
}
|
|
3237
3305
|
}
|
|
@@ -3247,6 +3315,10 @@ function OpenTuiApp(props) {
|
|
|
3247
3315
|
return "Connect a provider";
|
|
3248
3316
|
if (state.step === "skills")
|
|
3249
3317
|
return "Select skill";
|
|
3318
|
+
if (state.step === "rewind")
|
|
3319
|
+
return "Rewind — restore to the point before…";
|
|
3320
|
+
if (state.step === "rewind-action")
|
|
3321
|
+
return "Rewind — what to restore?";
|
|
3250
3322
|
const provider = providerDisplayName(state.providerId);
|
|
3251
3323
|
if (state.step === "auth")
|
|
3252
3324
|
return `${provider} auth method`;
|
|
@@ -3267,6 +3339,10 @@ function OpenTuiApp(props) {
|
|
|
3267
3339
|
}
|
|
3268
3340
|
if (state.step === "skills")
|
|
3269
3341
|
return `↑/↓ move · enter insert · esc close${count}`;
|
|
3342
|
+
if (state.step === "rewind")
|
|
3343
|
+
return `↑/↓ move · enter continue · esc cancel${count}`;
|
|
3344
|
+
if (state.step === "rewind-action")
|
|
3345
|
+
return "↑/↓ move · enter confirm · esc back";
|
|
3270
3346
|
const escLabel = state.step === "providers" ? "esc close" : "esc back";
|
|
3271
3347
|
return `↑/↓ move · enter select · ${escLabel}${count}`;
|
|
3272
3348
|
}
|
|
@@ -3279,14 +3355,25 @@ function OpenTuiApp(props) {
|
|
|
3279
3355
|
return theme.warning;
|
|
3280
3356
|
return theme.textMuted;
|
|
3281
3357
|
}
|
|
3282
|
-
function
|
|
3283
|
-
return
|
|
3284
|
-
}
|
|
3285
|
-
function
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3358
|
+
function providerDialogPanelWidth(terminalWidth) {
|
|
3359
|
+
return Math.max(PROVIDER_DIALOG_MIN_WIDTH, Math.min(PROVIDER_DIALOG_MAX_WIDTH, terminalWidth - 4));
|
|
3360
|
+
}
|
|
3361
|
+
function providerDialogColumnWidths(state, panelWidth) {
|
|
3362
|
+
const contentWidth = Math.max(24, panelWidth - PROVIDER_DIALOG_ROW_RESERVED_WIDTH);
|
|
3363
|
+
const footer = state.step === "skills" ? 10 : state.step === "providers" ? 9 : 8;
|
|
3364
|
+
const minLabel = state.step === "skills" ? 18 : 24;
|
|
3365
|
+
const desiredDetail = state.step === "skills"
|
|
3366
|
+
? 30
|
|
3367
|
+
: state.step === "providers"
|
|
3368
|
+
? 24
|
|
3369
|
+
: state.step === "rewind-action"
|
|
3370
|
+
? 40
|
|
3371
|
+
: state.step === "rewind"
|
|
3372
|
+
? 18
|
|
3373
|
+
: 16;
|
|
3374
|
+
const detail = Math.max(8, Math.min(desiredDetail, contentWidth - footer - minLabel));
|
|
3375
|
+
const label = Math.max(8, contentWidth - detail - footer);
|
|
3376
|
+
return { label, detail, footer };
|
|
3290
3377
|
}
|
|
3291
3378
|
function isCurrentModelItem(item) {
|
|
3292
3379
|
return item.value === props.agent.model || item.detail?.includes("current");
|
|
@@ -3351,8 +3438,8 @@ function OpenTuiApp(props) {
|
|
|
3351
3438
|
else if (state.step === "key") {
|
|
3352
3439
|
openProviderDialog(state.providerId && registry.supportsOAuth(state.providerId) ? "auth" : "providers", state.providerId);
|
|
3353
3440
|
}
|
|
3354
|
-
else if (state.step === "
|
|
3355
|
-
|
|
3441
|
+
else if (state.step === "rewind-action") {
|
|
3442
|
+
openProviderDialog("rewind");
|
|
3356
3443
|
}
|
|
3357
3444
|
else {
|
|
3358
3445
|
closeProviderDialog();
|
|
@@ -3483,6 +3570,20 @@ function OpenTuiApp(props) {
|
|
|
3483
3570
|
await executeSlash(item.command);
|
|
3484
3571
|
return;
|
|
3485
3572
|
}
|
|
3573
|
+
if (state.step === "rewind") {
|
|
3574
|
+
if (!item.value) {
|
|
3575
|
+
// "(current)" — keep everything as is.
|
|
3576
|
+
closeProviderDialog();
|
|
3577
|
+
return;
|
|
3578
|
+
}
|
|
3579
|
+
openProviderDialog("rewind-action", item.value);
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
if (state.step === "rewind-action") {
|
|
3583
|
+
closeProviderDialog();
|
|
3584
|
+
await executeSlash(item.command);
|
|
3585
|
+
return;
|
|
3586
|
+
}
|
|
3486
3587
|
if (state.step === "skills") {
|
|
3487
3588
|
closeProviderDialog();
|
|
3488
3589
|
insertSkillPrompt(item.value);
|
|
@@ -4597,6 +4698,129 @@ function OpenTuiApp(props) {
|
|
|
4597
4698
|
applyingComposerImageReplacement = false;
|
|
4598
4699
|
}
|
|
4599
4700
|
}
|
|
4701
|
+
// Replaces pasted-text markers with their full content. Runs after @mention
|
|
4702
|
+
// expansion so mention-like tokens inside pasted content stay literal.
|
|
4703
|
+
// References stay registered for the whole session so prompt-history recall
|
|
4704
|
+
// and requeued drafts containing a marker expand again on resend.
|
|
4705
|
+
function expandComposerPastedTexts(text) {
|
|
4706
|
+
if (pendingPastedTexts.size === 0 || !text.includes("[Pasted text #"))
|
|
4707
|
+
return text;
|
|
4708
|
+
const references = [...pendingPastedTexts.entries()].map(([marker, content]) => ({ marker, content }));
|
|
4709
|
+
return expandPastedContentMarkers(text, references);
|
|
4710
|
+
}
|
|
4711
|
+
// Inserts [image#N] labels at the cursor, padding with spaces when the
|
|
4712
|
+
// paste lands glued to surrounding text. Returns false when no prompt is
|
|
4713
|
+
// mounted (the caller should leave the paste alone).
|
|
4714
|
+
function insertComposerImageLabels(event, labels) {
|
|
4715
|
+
const prompt = activePrompt();
|
|
4716
|
+
if (!prompt)
|
|
4717
|
+
return false;
|
|
4718
|
+
event.preventDefault?.();
|
|
4719
|
+
const current = prompt.plainText ?? "";
|
|
4720
|
+
const offset = Math.min(Math.max(prompt.cursorOffset ?? current.length, 0), current.length);
|
|
4721
|
+
const needsLead = offset > 0 && !/\s/.test(current[offset - 1] ?? "");
|
|
4722
|
+
const needsTrail = offset < current.length && !/\s/.test(current[offset] ?? "");
|
|
4723
|
+
const joined = labels.map((label) => `[${label}]`).join(" ");
|
|
4724
|
+
prompt.insertText(`${needsLead ? " " : ""}${joined}${needsTrail ? " " : ""}`);
|
|
4725
|
+
onPromptContentChange(readPromptText());
|
|
4726
|
+
return true;
|
|
4727
|
+
}
|
|
4728
|
+
function handleComposerPaste(event) {
|
|
4729
|
+
const text = typeof event.text === "string" ? event.text : decodePastedBytes(event.bytes);
|
|
4730
|
+
if (isImagePathPaste(text)) {
|
|
4731
|
+
// Insert the final [image#N] label at paste time and ingest the file in
|
|
4732
|
+
// the background. Inserting the raw path and swapping it later flashes
|
|
4733
|
+
// the path and resets the cursor (setPromptText jumps it to the end).
|
|
4734
|
+
const entries = splitPastedPaths(text).map((rawPath) => ({
|
|
4735
|
+
rawPath,
|
|
4736
|
+
label: imageLabelForPath(rawPath, nextImageAttachmentIndex),
|
|
4737
|
+
}));
|
|
4738
|
+
if (!insertComposerImageLabels(event, entries.map((entry) => entry.label)))
|
|
4739
|
+
return;
|
|
4740
|
+
nextImageAttachmentIndex += entries.length;
|
|
4741
|
+
trackImagePathIngestion(entries);
|
|
4742
|
+
return;
|
|
4743
|
+
}
|
|
4744
|
+
// Copying an image file in Finder pastes only the file's NAME; Cmd+V of
|
|
4745
|
+
// raw image data pastes nothing at all. Both leave the real bits on the
|
|
4746
|
+
// system clipboard, so attach from there.
|
|
4747
|
+
const bareName = bareImageFilenameFromPaste(text);
|
|
4748
|
+
if (bareName || !text.trim()) {
|
|
4749
|
+
const label = bareName
|
|
4750
|
+
? imageLabelForPath(bareName, nextImageAttachmentIndex)
|
|
4751
|
+
: `image#${nextImageAttachmentIndex}.png`;
|
|
4752
|
+
if (!insertComposerImageLabels(event, [label]))
|
|
4753
|
+
return;
|
|
4754
|
+
nextImageAttachmentIndex += 1;
|
|
4755
|
+
trackClipboardImageIngestion(label, text);
|
|
4756
|
+
return;
|
|
4757
|
+
}
|
|
4758
|
+
if (!shouldCollapsePastedContent(text))
|
|
4759
|
+
return;
|
|
4760
|
+
event.preventDefault?.();
|
|
4761
|
+
const marker = createPastedContentMarker(text, nextPastedTextIndex);
|
|
4762
|
+
nextPastedTextIndex += 1;
|
|
4763
|
+
pendingPastedTexts.set(marker, text);
|
|
4764
|
+
const prompt = activePrompt();
|
|
4765
|
+
prompt?.insertText(marker);
|
|
4766
|
+
onPromptContentChange(readPromptText());
|
|
4767
|
+
}
|
|
4768
|
+
function trackImageIngestion(task) {
|
|
4769
|
+
pendingImageIngestions.add(task);
|
|
4770
|
+
void task.finally(() => pendingImageIngestions.delete(task));
|
|
4771
|
+
}
|
|
4772
|
+
function trackImagePathIngestion(entries) {
|
|
4773
|
+
trackImageIngestion((async () => {
|
|
4774
|
+
for (const { rawPath, label } of entries) {
|
|
4775
|
+
const result = await ingestImagePath(rawPath);
|
|
4776
|
+
if (result.attachment) {
|
|
4777
|
+
pendingImageAttachments.set(label, result.attachment);
|
|
4778
|
+
}
|
|
4779
|
+
else {
|
|
4780
|
+
addMessage("error", `Skipped image: ${rawPath}: ${result.error ?? "could not attach image"}`);
|
|
4781
|
+
replaceComposerImageLabel(label, "");
|
|
4782
|
+
}
|
|
4783
|
+
}
|
|
4784
|
+
})());
|
|
4785
|
+
}
|
|
4786
|
+
function trackClipboardImageIngestion(label, originalText) {
|
|
4787
|
+
trackImageIngestion((async () => {
|
|
4788
|
+
const result = await ingestClipboardImage();
|
|
4789
|
+
if (result.attachment) {
|
|
4790
|
+
pendingImageAttachments.set(label, result.attachment);
|
|
4791
|
+
return;
|
|
4792
|
+
}
|
|
4793
|
+
const restored = originalText.trim();
|
|
4794
|
+
// A filename-looking text paste with no image on the clipboard is just
|
|
4795
|
+
// text — restore it quietly. Only an empty paste (Cmd+V of image data)
|
|
4796
|
+
// warrants an error, since there is nothing to restore.
|
|
4797
|
+
if (!restored)
|
|
4798
|
+
addMessage("error", `Could not attach image from clipboard: ${result.error ?? "unknown error"}`);
|
|
4799
|
+
replaceComposerImageLabel(label, restored);
|
|
4800
|
+
})());
|
|
4801
|
+
}
|
|
4802
|
+
// Swaps a failed image label for its replacement (or drops it) without
|
|
4803
|
+
// moving the cursor relative to the surrounding text.
|
|
4804
|
+
function replaceComposerImageLabel(label, replacement) {
|
|
4805
|
+
const prompt = activePrompt();
|
|
4806
|
+
const current = prompt?.plainText ?? promptText;
|
|
4807
|
+
const token = `[${label}]`;
|
|
4808
|
+
const start = current.indexOf(token);
|
|
4809
|
+
if (start < 0)
|
|
4810
|
+
return;
|
|
4811
|
+
let end = start + token.length;
|
|
4812
|
+
if (!replacement && current[end] === " ")
|
|
4813
|
+
end += 1;
|
|
4814
|
+
const next = current.slice(0, start) + replacement + current.slice(end);
|
|
4815
|
+
if (prompt) {
|
|
4816
|
+
const cursor = Math.min(Math.max(prompt.cursorOffset ?? next.length, 0), current.length);
|
|
4817
|
+
prompt.setText(next);
|
|
4818
|
+
prompt.cursorOffset = cursor <= start
|
|
4819
|
+
? cursor
|
|
4820
|
+
: Math.min(next.length, Math.max(start + replacement.length, cursor - (end - start) + replacement.length));
|
|
4821
|
+
}
|
|
4822
|
+
onPromptContentChange(next);
|
|
4823
|
+
}
|
|
4600
4824
|
async function expandTextParts(parts) {
|
|
4601
4825
|
const expandedParts = [];
|
|
4602
4826
|
for (const part of parts) {
|
|
@@ -4607,14 +4831,18 @@ function OpenTuiApp(props) {
|
|
|
4607
4831
|
const expansion = await expandAtMentions(part.text, props.args.cwd);
|
|
4608
4832
|
if (expansion.missing.length)
|
|
4609
4833
|
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
4610
|
-
for (const skipped of expansion.skipped)
|
|
4611
|
-
|
|
4612
|
-
|
|
4834
|
+
for (const skipped of expansion.skipped) {
|
|
4835
|
+
if (skipped.reason !== "too large")
|
|
4836
|
+
addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
|
|
4837
|
+
}
|
|
4838
|
+
expandedParts.push({ type: "text", text: expandComposerPastedTexts(expansion.text) });
|
|
4613
4839
|
}
|
|
4614
4840
|
return expandedParts;
|
|
4615
4841
|
}
|
|
4616
4842
|
async function handleInput(input, options = {}) {
|
|
4617
4843
|
setNotice("");
|
|
4844
|
+
if (pendingImageIngestions.size > 0)
|
|
4845
|
+
await Promise.all([...pendingImageIngestions]);
|
|
4618
4846
|
const labeledInput = buildImageContentPartsFromLabels(input, pendingImageAttachments);
|
|
4619
4847
|
if (labeledInput.actualInput) {
|
|
4620
4848
|
await runAgentInput(await expandTextParts(labeledInput.actualInput), labeledInput.displayInput, options);
|
|
@@ -4635,7 +4863,7 @@ function OpenTuiApp(props) {
|
|
|
4635
4863
|
if (input.startsWith("/")) {
|
|
4636
4864
|
const skillInvocation = parseSkillInvocation(input, skills);
|
|
4637
4865
|
if (skillInvocation) {
|
|
4638
|
-
await runAgentInput(skillInvocation.actualPrompt, input, options);
|
|
4866
|
+
await runAgentInput(expandComposerPastedTexts(skillInvocation.actualPrompt), input, options);
|
|
4639
4867
|
return;
|
|
4640
4868
|
}
|
|
4641
4869
|
const handled = await executeSlash(input, options);
|
|
@@ -4645,9 +4873,11 @@ function OpenTuiApp(props) {
|
|
|
4645
4873
|
const expansion = await expandAtMentions(input, props.args.cwd);
|
|
4646
4874
|
if (expansion.missing.length)
|
|
4647
4875
|
addMessage("error", `Could not resolve @mention: ${expansion.missing.join(", ")}`);
|
|
4648
|
-
for (const skipped of expansion.skipped)
|
|
4649
|
-
|
|
4650
|
-
|
|
4876
|
+
for (const skipped of expansion.skipped) {
|
|
4877
|
+
if (skipped.reason !== "too large")
|
|
4878
|
+
addMessage("error", `Skipped @${skipped.path}: ${skipped.reason}`);
|
|
4879
|
+
}
|
|
4880
|
+
await runAgentInput(expandComposerPastedTexts(expansion.text), input, options);
|
|
4651
4881
|
}
|
|
4652
4882
|
async function executeSlash(input, options = {}) {
|
|
4653
4883
|
if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
|
|
@@ -4676,10 +4906,19 @@ function OpenTuiApp(props) {
|
|
|
4676
4906
|
openPicker: (kind, providerId) => {
|
|
4677
4907
|
void openPicker(kind, providerId);
|
|
4678
4908
|
},
|
|
4909
|
+
openRewindPicker: () => {
|
|
4910
|
+
openProviderDialog("rewind");
|
|
4911
|
+
},
|
|
4912
|
+
fillComposer: (text) => {
|
|
4913
|
+
resetPromptHistoryBrowse();
|
|
4914
|
+
setPromptText(text);
|
|
4915
|
+
redrawDock();
|
|
4916
|
+
},
|
|
4679
4917
|
registry,
|
|
4680
4918
|
skillRegistry: skills,
|
|
4681
4919
|
bashAllowlist: props.options.bashAllowlist,
|
|
4682
4920
|
settingsManager: props.options.settingsManager,
|
|
4921
|
+
hookController: props.options.hookController,
|
|
4683
4922
|
mcpManager: props.options.mcpManager,
|
|
4684
4923
|
lspService,
|
|
4685
4924
|
flushMemory: props.options.flushMemory,
|
|
@@ -4721,6 +4960,13 @@ function OpenTuiApp(props) {
|
|
|
4721
4960
|
redrawTranscript(undefined, displayMessages);
|
|
4722
4961
|
setTimeout(() => setNotice(""), 4000);
|
|
4723
4962
|
}
|
|
4963
|
+
else if (result.startsWith("⏪")) {
|
|
4964
|
+
// /rewind truncated agent.messages — rebuild the transcript from the
|
|
4965
|
+
// rewound state before appending the summary.
|
|
4966
|
+
displayMessages = reconstructDisplayMessages(props.agent.messages);
|
|
4967
|
+
streamingDisplay = undefined;
|
|
4968
|
+
addMessage("assistant", result);
|
|
4969
|
+
}
|
|
4724
4970
|
else {
|
|
4725
4971
|
addMessage("assistant", result);
|
|
4726
4972
|
}
|
|
@@ -4992,6 +5238,48 @@ function OpenTuiApp(props) {
|
|
|
4992
5238
|
command: `/logout ${provider.id}`,
|
|
4993
5239
|
}));
|
|
4994
5240
|
}
|
|
5241
|
+
function buildRewindPickerItems() {
|
|
5242
|
+
const session = props.options.sessionManager;
|
|
5243
|
+
if (!session)
|
|
5244
|
+
return [];
|
|
5245
|
+
const checkpoints = session.getCheckpoints();
|
|
5246
|
+
const items = session.listUserTurns().map((turn, index) => {
|
|
5247
|
+
const files = checkpoints.filesTouchedAt(turn.id).length;
|
|
5248
|
+
return {
|
|
5249
|
+
label: turn.preview,
|
|
5250
|
+
detail: files > 0 ? `${files} file${files === 1 ? "" : "s"} changed` : "No code changes",
|
|
5251
|
+
value: String(index + 1),
|
|
5252
|
+
command: `/rewind ${index + 1}`,
|
|
5253
|
+
};
|
|
5254
|
+
});
|
|
5255
|
+
// Selecting "(current)" keeps everything as is — mirrors Claude Code.
|
|
5256
|
+
items.push({ label: "(current)", value: "", command: "" });
|
|
5257
|
+
return items;
|
|
5258
|
+
}
|
|
5259
|
+
function buildRewindActionItems(turnNumber) {
|
|
5260
|
+
if (!turnNumber)
|
|
5261
|
+
return [];
|
|
5262
|
+
return [
|
|
5263
|
+
{
|
|
5264
|
+
label: "Restore conversation and code",
|
|
5265
|
+
detail: "Rewind the chat and undo tracked file edits",
|
|
5266
|
+
value: turnNumber,
|
|
5267
|
+
command: `/rewind ${turnNumber}`,
|
|
5268
|
+
},
|
|
5269
|
+
{
|
|
5270
|
+
label: "Restore conversation only",
|
|
5271
|
+
detail: "Keep file changes on disk",
|
|
5272
|
+
value: turnNumber,
|
|
5273
|
+
command: `/rewind ${turnNumber} --chat`,
|
|
5274
|
+
},
|
|
5275
|
+
{
|
|
5276
|
+
label: "Restore code only",
|
|
5277
|
+
detail: "Undo tracked file edits, keep the conversation",
|
|
5278
|
+
value: turnNumber,
|
|
5279
|
+
command: `/rewind ${turnNumber} --code`,
|
|
5280
|
+
},
|
|
5281
|
+
];
|
|
5282
|
+
}
|
|
4995
5283
|
function buildSkillItems() {
|
|
4996
5284
|
return skills.summaries().map((skill) => {
|
|
4997
5285
|
const tags = skill.tags && skill.tags.length > 0 ? ` · ${skill.tags.join(", ")}` : "";
|
|
@@ -5106,14 +5394,19 @@ function OpenTuiApp(props) {
|
|
|
5106
5394
|
return;
|
|
5107
5395
|
}
|
|
5108
5396
|
rememberPromptHistory(displayInput);
|
|
5109
|
-
|
|
5397
|
+
// History keeps the short marker (it expands again on resend); the
|
|
5398
|
+
// transcript shows the full pasted content once the message is sent.
|
|
5399
|
+
const displayContent = expandComposerPastedTexts(displayInput);
|
|
5400
|
+
const reusedQueuedDisplay = promoteQueuedUserDisplay(options.displayId, displayContent);
|
|
5110
5401
|
const nextMessages = reusedQueuedDisplay
|
|
5111
5402
|
? displayMessages
|
|
5112
|
-
: [...displayMessages, { role: "user", content:
|
|
5403
|
+
: [...displayMessages, { role: "user", content: displayContent }];
|
|
5113
5404
|
if (!reusedQueuedDisplay)
|
|
5114
5405
|
displayMessages = nextMessages;
|
|
5115
5406
|
streamingDisplay = undefined;
|
|
5116
|
-
|
|
5407
|
+
// The user just sent this message — re-engage bottom-follow so the new
|
|
5408
|
+
// turn is visible even if they had scrolled up to read earlier history.
|
|
5409
|
+
redrawTranscript(undefined, nextMessages, { forceFollow: true });
|
|
5117
5410
|
const taskStartedAt = Date.now();
|
|
5118
5411
|
const run = beginAgentRun();
|
|
5119
5412
|
traceEvent("tui_agent_run_begin", {
|
|
@@ -5127,6 +5420,8 @@ function OpenTuiApp(props) {
|
|
|
5127
5420
|
}, { surface: "tui" });
|
|
5128
5421
|
let assistantContent = "";
|
|
5129
5422
|
let assistantReasoning = "";
|
|
5423
|
+
let textDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5424
|
+
let reasoningDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5130
5425
|
const toolCalls = [];
|
|
5131
5426
|
const assistantParts = [];
|
|
5132
5427
|
let turnStartedAt;
|
|
@@ -5141,7 +5436,7 @@ function OpenTuiApp(props) {
|
|
|
5141
5436
|
const buildStreamingDisplay = (status) => {
|
|
5142
5437
|
const currentParts = snapshotDisplayParts(assistantParts);
|
|
5143
5438
|
const partContent = assistantContent || contentFromParts(currentParts);
|
|
5144
|
-
return {
|
|
5439
|
+
return sanitizeDisplayMessage({
|
|
5145
5440
|
role: "assistant",
|
|
5146
5441
|
content: partContent,
|
|
5147
5442
|
reasoning: assistantReasoning || undefined,
|
|
@@ -5150,7 +5445,7 @@ function OpenTuiApp(props) {
|
|
|
5150
5445
|
status,
|
|
5151
5446
|
streaming: true,
|
|
5152
5447
|
turnStartedAt,
|
|
5153
|
-
};
|
|
5448
|
+
});
|
|
5154
5449
|
};
|
|
5155
5450
|
const flushStreamingRedraw = () => {
|
|
5156
5451
|
if (pendingStreamingRedrawTimer === undefined)
|
|
@@ -5180,6 +5475,8 @@ function OpenTuiApp(props) {
|
|
|
5180
5475
|
if (event.type === "turn_start") {
|
|
5181
5476
|
assistantContent = "";
|
|
5182
5477
|
assistantReasoning = "";
|
|
5478
|
+
textDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5479
|
+
reasoningDisplaySanitizer = createStreamingInternalReminderSanitizer();
|
|
5183
5480
|
toolCalls.length = 0;
|
|
5184
5481
|
assistantParts.length = 0;
|
|
5185
5482
|
turnStartedAt = Date.now();
|
|
@@ -5192,22 +5489,42 @@ function OpenTuiApp(props) {
|
|
|
5192
5489
|
});
|
|
5193
5490
|
}
|
|
5194
5491
|
else if (event.type === "text_delta") {
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5492
|
+
const content = textDisplaySanitizer.push(event.content);
|
|
5493
|
+
if (content) {
|
|
5494
|
+
assistantContent += content;
|
|
5495
|
+
appendTextPart(assistantParts, content);
|
|
5496
|
+
scheduleStreamingRedraw();
|
|
5497
|
+
}
|
|
5198
5498
|
}
|
|
5199
5499
|
else if (event.type === "reasoning_delta") {
|
|
5500
|
+
const content = reasoningDisplaySanitizer.push(event.content);
|
|
5501
|
+
if (!content)
|
|
5502
|
+
continue;
|
|
5200
5503
|
debugReasoningStream({
|
|
5201
5504
|
stage: "ui_append",
|
|
5202
5505
|
providerId: props.agent.providerId,
|
|
5203
5506
|
modelId: props.agent.apiModel,
|
|
5204
5507
|
beforeLength: assistantReasoning.length,
|
|
5205
|
-
delta: summarizeDebugText(
|
|
5206
|
-
afterLength: assistantReasoning.length +
|
|
5508
|
+
delta: summarizeDebugText(content),
|
|
5509
|
+
afterLength: assistantReasoning.length + content.length,
|
|
5207
5510
|
});
|
|
5208
|
-
assistantReasoning +=
|
|
5511
|
+
assistantReasoning += content;
|
|
5209
5512
|
scheduleStreamingRedraw();
|
|
5210
5513
|
}
|
|
5514
|
+
else if (event.type === "hook_start") {
|
|
5515
|
+
setNotice(`Hook ${event.eventName}: ${event.hookId}`);
|
|
5516
|
+
}
|
|
5517
|
+
else if (event.type === "hook_end") {
|
|
5518
|
+
if (event.decision === "deny") {
|
|
5519
|
+
setNotice(event.reason ?? `Hook ${event.hookId} denied ${event.eventName}`);
|
|
5520
|
+
}
|
|
5521
|
+
}
|
|
5522
|
+
else if (event.type === "hook_error") {
|
|
5523
|
+
setNotice(`Hook ${event.hookId} error: ${event.error}`);
|
|
5524
|
+
}
|
|
5525
|
+
else if (event.type === "provider_retry") {
|
|
5526
|
+
setNotice(`Connection interrupted — retrying (${event.attempt}/${event.maxAttempts})…`);
|
|
5527
|
+
}
|
|
5211
5528
|
else if (event.type === "tool_call_start") {
|
|
5212
5529
|
// Insert a streaming placeholder so the user sees feedback the moment
|
|
5213
5530
|
// the model commits to a tool call, instead of waiting for the args
|
|
@@ -5336,6 +5653,15 @@ function OpenTuiApp(props) {
|
|
|
5336
5653
|
clearTimeout(pendingStreamingRedrawTimer);
|
|
5337
5654
|
pendingStreamingRedrawTimer = undefined;
|
|
5338
5655
|
}
|
|
5656
|
+
const flushedText = textDisplaySanitizer.flush();
|
|
5657
|
+
if (flushedText) {
|
|
5658
|
+
assistantContent += flushedText;
|
|
5659
|
+
appendTextPart(assistantParts, flushedText);
|
|
5660
|
+
}
|
|
5661
|
+
const flushedReasoning = reasoningDisplaySanitizer.flush();
|
|
5662
|
+
if (flushedReasoning) {
|
|
5663
|
+
assistantReasoning += flushedReasoning;
|
|
5664
|
+
}
|
|
5339
5665
|
if (event.usage) {
|
|
5340
5666
|
setSidebarUsage((current) => ({
|
|
5341
5667
|
contextTokens: event.usage.promptTokens || current.contextTokens,
|
|
@@ -5351,20 +5677,21 @@ function OpenTuiApp(props) {
|
|
|
5351
5677
|
}
|
|
5352
5678
|
bumpSidebar();
|
|
5353
5679
|
const currentParts = snapshotDisplayParts(assistantParts);
|
|
5354
|
-
const finalContent = assistantContent || contentFromParts(currentParts);
|
|
5680
|
+
const finalContent = sanitizeInternalReminderBlocks(assistantContent || contentFromParts(currentParts));
|
|
5681
|
+
const finalReasoning = sanitizeInternalReasoningText(assistantReasoning);
|
|
5355
5682
|
const finalToolCalls = toolCalls.length > 0
|
|
5356
5683
|
? [...toolCalls]
|
|
5357
5684
|
: toolCallsFromParts(currentParts);
|
|
5358
|
-
const assistantMessage = {
|
|
5685
|
+
const assistantMessage = sanitizeDisplayMessage({
|
|
5359
5686
|
role: "assistant",
|
|
5360
5687
|
content: finalContent,
|
|
5361
|
-
reasoning:
|
|
5688
|
+
reasoning: finalReasoning || undefined,
|
|
5362
5689
|
toolCalls: finalToolCalls.length ? finalToolCalls : undefined,
|
|
5363
5690
|
parts: currentParts.length ? currentParts : undefined,
|
|
5364
5691
|
turnStartedAt,
|
|
5365
5692
|
turnCompletedAt: Date.now(),
|
|
5366
5693
|
turnUsage: event.usage,
|
|
5367
|
-
};
|
|
5694
|
+
});
|
|
5368
5695
|
const nextMessages = hasRenderableMessage(assistantMessage)
|
|
5369
5696
|
? [...displayMessages, assistantMessage]
|
|
5370
5697
|
: displayMessages;
|
|
@@ -5464,16 +5791,16 @@ function OpenTuiApp(props) {
|
|
|
5464
5791
|
return h("box", {
|
|
5465
5792
|
ref: (ref) => {
|
|
5466
5793
|
sessionComposerShell = ref;
|
|
5467
|
-
ref.visible = !
|
|
5794
|
+
ref.visible = !isComposerHiddenByModal();
|
|
5468
5795
|
},
|
|
5469
5796
|
width: "100%",
|
|
5470
5797
|
paddingLeft: 2,
|
|
5471
5798
|
paddingRight: 2,
|
|
5472
5799
|
flexShrink: 0,
|
|
5473
|
-
visible: !
|
|
5800
|
+
visible: !isComposerHiddenByModal(),
|
|
5474
5801
|
}, renderPrompt({
|
|
5475
5802
|
ref: (ref) => { sessionPromptRef = ref; },
|
|
5476
|
-
focused: !
|
|
5803
|
+
focused: !isComposerHiddenByModal(),
|
|
5477
5804
|
onSubmit: submitPrompt,
|
|
5478
5805
|
isFallbackNewlineKey: isTrackedShiftReturn,
|
|
5479
5806
|
onFallbackNewline: () => canInsertPromptNewline() && (activePrompt()?.newLine() ?? false),
|
|
@@ -5488,6 +5815,7 @@ function OpenTuiApp(props) {
|
|
|
5488
5815
|
model: promptModelTitle,
|
|
5489
5816
|
interruptHint: promptStatusText,
|
|
5490
5817
|
tabHint: () => isRunning() ? "queue" : "mode",
|
|
5818
|
+
onPaste: handleComposerPaste,
|
|
5491
5819
|
placeholder: () => {
|
|
5492
5820
|
const approvalState = pendingApproval();
|
|
5493
5821
|
if (approvalState)
|
|
@@ -5508,7 +5836,6 @@ function OpenTuiApp(props) {
|
|
|
5508
5836
|
}));
|
|
5509
5837
|
}
|
|
5510
5838
|
function renderHomeSurface() {
|
|
5511
|
-
const homeHeight = Math.max(16, dimensions().height - 4);
|
|
5512
5839
|
const logoLines = bubbleWordmarkForWidth(dimensions().width);
|
|
5513
5840
|
return h("box", {
|
|
5514
5841
|
ref: (ref) => {
|
|
@@ -5516,7 +5843,8 @@ function OpenTuiApp(props) {
|
|
|
5516
5843
|
ref.visible = isHomeSurfaceActive(streamingDisplay);
|
|
5517
5844
|
},
|
|
5518
5845
|
visible: isHomeSurfaceActive(streamingDisplay),
|
|
5519
|
-
height:
|
|
5846
|
+
height: "100%",
|
|
5847
|
+
minHeight: 0,
|
|
5520
5848
|
flexDirection: "column",
|
|
5521
5849
|
alignItems: "center",
|
|
5522
5850
|
justifyContent: "center",
|
|
@@ -5528,57 +5856,6 @@ function OpenTuiApp(props) {
|
|
|
5528
5856
|
...(props.options.updateNotice
|
|
5529
5857
|
? [h("box", { flexShrink: 0, flexDirection: "column", alignItems: "center" }, h("text", { fg: theme.accent, content: props.options.updateNotice }))]
|
|
5530
5858
|
: []),
|
|
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
5859
|
]);
|
|
5583
5860
|
}
|
|
5584
5861
|
function renderQuestionPanelHost() {
|
|
@@ -5656,7 +5933,10 @@ function OpenTuiApp(props) {
|
|
|
5656
5933
|
visible: false,
|
|
5657
5934
|
flexShrink: 0,
|
|
5658
5935
|
}, h("textarea", {
|
|
5659
|
-
ref: (ref) => {
|
|
5936
|
+
ref: (ref) => {
|
|
5937
|
+
preserveCursorOnMouseSelection(ref);
|
|
5938
|
+
questionCustomInput = ref;
|
|
5939
|
+
},
|
|
5660
5940
|
placeholder: "Type your own answer",
|
|
5661
5941
|
placeholderColor: theme.textMuted,
|
|
5662
5942
|
textColor: theme.text,
|
|
@@ -5745,7 +6025,10 @@ function OpenTuiApp(props) {
|
|
|
5745
6025
|
wrapMode: "word",
|
|
5746
6026
|
content: "Creates a public GitHub issue at DylanDDeng/bubble. Review before sending.",
|
|
5747
6027
|
}), h("textarea", {
|
|
5748
|
-
ref: (ref) => {
|
|
6028
|
+
ref: (ref) => {
|
|
6029
|
+
preserveCursorOnMouseSelection(ref);
|
|
6030
|
+
feedbackInput = ref;
|
|
6031
|
+
},
|
|
5749
6032
|
placeholder: "Describe what happened",
|
|
5750
6033
|
placeholderColor: theme.textMuted,
|
|
5751
6034
|
textColor: theme.text,
|
|
@@ -6657,11 +6940,9 @@ function OpenTuiApp(props) {
|
|
|
6657
6940
|
visible: !!approval,
|
|
6658
6941
|
focusable: true,
|
|
6659
6942
|
onKeyDown: handleApprovalKey,
|
|
6660
|
-
|
|
6661
|
-
|
|
6662
|
-
|
|
6663
|
-
bottom: 4,
|
|
6664
|
-
zIndex: 200,
|
|
6943
|
+
width: "100%",
|
|
6944
|
+
flexShrink: 0,
|
|
6945
|
+
marginTop: 1,
|
|
6665
6946
|
backgroundColor: theme.backgroundPanel,
|
|
6666
6947
|
border: ["left"],
|
|
6667
6948
|
borderColor: theme.warning,
|
|
@@ -6825,12 +7106,8 @@ function OpenTuiApp(props) {
|
|
|
6825
7106
|
]),
|
|
6826
7107
|
renderFooter({
|
|
6827
7108
|
cwd: props.args.cwd,
|
|
6828
|
-
mode,
|
|
6829
7109
|
running: isRunning,
|
|
6830
7110
|
registerScanner: registerPromptScanner,
|
|
6831
|
-
registerModeBadge: registerFooterModeBadge,
|
|
6832
|
-
traceVerbose: verboseTrace,
|
|
6833
|
-
registerTraceBadge: registerFooterTraceBadge,
|
|
6834
7111
|
}),
|
|
6835
7112
|
renderProviderDialog(),
|
|
6836
7113
|
renderStatsPanel(),
|
|
@@ -6841,7 +7118,10 @@ function OpenTuiApp(props) {
|
|
|
6841
7118
|
function renderPrompt(input) {
|
|
6842
7119
|
const transparentBackground = "#00000000";
|
|
6843
7120
|
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:
|
|
7121
|
+
ref: (ref) => {
|
|
7122
|
+
preserveCursorOnMouseSelection(ref);
|
|
7123
|
+
input.ref(ref);
|
|
7124
|
+
},
|
|
6845
7125
|
focused: input.focused,
|
|
6846
7126
|
placeholder: input.placeholder(),
|
|
6847
7127
|
placeholderColor: theme.textMuted,
|
|
@@ -6849,8 +7129,10 @@ function renderPrompt(input) {
|
|
|
6849
7129
|
focusedTextColor: theme.text,
|
|
6850
7130
|
backgroundColor: transparentBackground,
|
|
6851
7131
|
focusedBackgroundColor: transparentBackground,
|
|
7132
|
+
cursorColor: theme.primary,
|
|
6852
7133
|
minHeight: 1,
|
|
6853
7134
|
maxHeight: 6,
|
|
7135
|
+
...(input.onPaste ? { onPaste: input.onPaste } : {}),
|
|
6854
7136
|
onContentChange: () => input.onContentChange(input.getText()),
|
|
6855
7137
|
keyBindings: PROMPT_TEXTAREA_KEYBINDINGS,
|
|
6856
7138
|
onKeyDown: (event) => {
|
|
@@ -6965,8 +7247,9 @@ function renderUserMessage(message, index) {
|
|
|
6965
7247
|
const userChildren = [
|
|
6966
7248
|
h("text", { fg: theme.messageUserText, wrapMode: "word" }, message.content || " "),
|
|
6967
7249
|
];
|
|
6968
|
-
|
|
6969
|
-
|
|
7250
|
+
const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
|
|
7251
|
+
if (inputBadge) {
|
|
7252
|
+
userChildren.push(h("box", { paddingTop: 1 }, h("text", { fg: theme.textMuted }, h("span", { bg: theme.primary, fg: theme.background, bold: true }, ` ${inputBadge} `))));
|
|
6970
7253
|
}
|
|
6971
7254
|
return h("box", {
|
|
6972
7255
|
border: ["left"],
|
|
@@ -6977,17 +7260,20 @@ function renderUserMessage(message, index) {
|
|
|
6977
7260
|
}, h("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: theme.backgroundPanel, flexShrink: 0, flexDirection: "column" }, ...userChildren));
|
|
6978
7261
|
}
|
|
6979
7262
|
function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false, width = 80) {
|
|
7263
|
+
message = sanitizeDisplayMessage(message);
|
|
6980
7264
|
const visibleReasoning = showThinking
|
|
6981
|
-
?
|
|
7265
|
+
? sanitizeInternalReasoningText(message.reasoning ?? "").trim()
|
|
6982
7266
|
: "";
|
|
6983
|
-
const
|
|
7267
|
+
const sanitizedContent = sanitizeInternalReminderBlocks(message.content);
|
|
7268
|
+
const modelSwitch = parseModelSwitchMessage(sanitizedContent);
|
|
6984
7269
|
if (modelSwitch && !visibleReasoning && !(message.toolCalls?.length)) {
|
|
6985
7270
|
return renderModelSwitchMessage(modelSwitch);
|
|
6986
7271
|
}
|
|
6987
7272
|
const children = [];
|
|
6988
7273
|
const parts = message.parts ?? [];
|
|
6989
7274
|
const hasParts = parts.length > 0;
|
|
6990
|
-
|
|
7275
|
+
const trimmedContent = sanitizedContent.trim();
|
|
7276
|
+
if (message.status && !visibleReasoning && !trimmedContent && !(message.toolCalls?.length) && !hasParts) {
|
|
6991
7277
|
children.push(h("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0 }, h("text", { fg: theme.messageThinkingText }, assistantStatusLabel(message))));
|
|
6992
7278
|
}
|
|
6993
7279
|
if (visibleReasoning) {
|
|
@@ -7003,7 +7289,6 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
|
|
|
7003
7289
|
fg: theme.messageThinkingContentText,
|
|
7004
7290
|
})));
|
|
7005
7291
|
}
|
|
7006
|
-
const trimmedContent = message.content.trim();
|
|
7007
7292
|
if (hasParts) {
|
|
7008
7293
|
renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace, width, message.streaming === true);
|
|
7009
7294
|
}
|
|
@@ -7041,7 +7326,7 @@ function renderAssistantMessage(message, syntaxStyle, subtleSyntaxStyle, showThi
|
|
|
7041
7326
|
function renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace, width, streaming) {
|
|
7042
7327
|
for (const part of parts) {
|
|
7043
7328
|
if (part.type === "text") {
|
|
7044
|
-
const content = part.content.trim();
|
|
7329
|
+
const content = sanitizeInternalReminderBlocks(part.content).trim();
|
|
7045
7330
|
if (!content)
|
|
7046
7331
|
continue;
|
|
7047
7332
|
children.push(h("box", {
|
|
@@ -7067,7 +7352,7 @@ function renderAssistantMessageParts(children, parts, syntaxStyle, verboseTrace,
|
|
|
7067
7352
|
}
|
|
7068
7353
|
function lastPartHasText(parts) {
|
|
7069
7354
|
const last = parts[parts.length - 1];
|
|
7070
|
-
return last?.type === "text" && !!last.content.trim();
|
|
7355
|
+
return last?.type === "text" && !!sanitizeInternalReminderBlocks(last.content).trim();
|
|
7071
7356
|
}
|
|
7072
7357
|
function parseModelSwitchMessage(content) {
|
|
7073
7358
|
const match = content.trim().match(/^Model switched to (.+)\.$/);
|
|
@@ -7121,7 +7406,7 @@ function renderMarkdownContent(content, syntaxStyle, options) {
|
|
|
7121
7406
|
function updateTranscriptHost(host, state, messages, options, syntaxStyle, subtleSyntaxStyle) {
|
|
7122
7407
|
const showThinking = options?.showThinking ?? true;
|
|
7123
7408
|
const verboseTrace = options?.verboseTrace ?? false;
|
|
7124
|
-
const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
|
|
7409
|
+
const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
|
|
7125
7410
|
const ctx = host.ctx;
|
|
7126
7411
|
const nextEntries = [];
|
|
7127
7412
|
if (!visibleMessages.length && !options?.plan) {
|
|
@@ -7200,6 +7485,7 @@ function transcriptMessageKey(message, index) {
|
|
|
7200
7485
|
return `${index}:${message.role}`;
|
|
7201
7486
|
}
|
|
7202
7487
|
function transcriptMessageSignature(message, compactionExpanded = false) {
|
|
7488
|
+
message = sanitizeDisplayMessage(message);
|
|
7203
7489
|
if (message.role !== "assistant")
|
|
7204
7490
|
return message.role;
|
|
7205
7491
|
if (message.syntheticKind === "ui_compact_card") {
|
|
@@ -7217,12 +7503,13 @@ function transcriptMessageSignature(message, compactionExpanded = false) {
|
|
|
7217
7503
|
}
|
|
7218
7504
|
function updateMessageEntry(entry, message, showThinking = true, compactionExpanded = false, assistantOptions) {
|
|
7219
7505
|
if (message.role === "user") {
|
|
7506
|
+
const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
|
|
7220
7507
|
if (entry.refs.userText)
|
|
7221
7508
|
entry.refs.userText.content = message.content || " ";
|
|
7222
7509
|
if (entry.refs.userQueuedBox)
|
|
7223
|
-
entry.refs.userQueuedBox.visible =
|
|
7510
|
+
entry.refs.userQueuedBox.visible = !!inputBadge;
|
|
7224
7511
|
if (entry.refs.userQueuedText)
|
|
7225
|
-
entry.refs.userQueuedText.content =
|
|
7512
|
+
entry.refs.userQueuedText.content = inputBadge ? ` ${inputBadge} ` : "";
|
|
7226
7513
|
return;
|
|
7227
7514
|
}
|
|
7228
7515
|
if (message.role === "error") {
|
|
@@ -7249,6 +7536,7 @@ function updateMessageEntry(entry, message, showThinking = true, compactionExpan
|
|
|
7249
7536
|
}
|
|
7250
7537
|
}
|
|
7251
7538
|
function updateAssistantEntry(entry, message, showThinking, options) {
|
|
7539
|
+
message = sanitizeDisplayMessage(message);
|
|
7252
7540
|
const content = message.content.trim();
|
|
7253
7541
|
const visibleReasoning = showThinking ? message.reasoning?.trim() ?? "" : "";
|
|
7254
7542
|
const tools = message.toolCalls ?? [];
|
|
@@ -7345,7 +7633,7 @@ function updateAssistantPartEntries(entry, parts, options, streaming) {
|
|
|
7345
7633
|
const key = `part:${index}:${part.type}`;
|
|
7346
7634
|
const previous = previousEntries.get(key);
|
|
7347
7635
|
if (part.type === "text") {
|
|
7348
|
-
const content = part.content.trim();
|
|
7636
|
+
const content = sanitizeInternalReminderBlocks(part.content).trim();
|
|
7349
7637
|
let ref;
|
|
7350
7638
|
if (previous?.kind === "text") {
|
|
7351
7639
|
ref = previous;
|
|
@@ -7467,6 +7755,25 @@ function createTraceGroupRenderable(ctx, group, syntaxStyle, width = 80) {
|
|
|
7467
7755
|
const children = [
|
|
7468
7756
|
createText(ctx, traceGroupHeaderStyledText(group, width), { wrapMode: "none" }),
|
|
7469
7757
|
];
|
|
7758
|
+
const commandBlock = executeCommandBlockFor(group, width);
|
|
7759
|
+
if (commandBlock) {
|
|
7760
|
+
children.push(createBox(ctx, {
|
|
7761
|
+
paddingLeft: 2,
|
|
7762
|
+
flexDirection: "column",
|
|
7763
|
+
flexShrink: 0,
|
|
7764
|
+
}, [
|
|
7765
|
+
...commandBlock.lines.map((line, index) => createText(ctx, `${index === 0 ? "$ " : " "}${line}`, {
|
|
7766
|
+
fg: theme.toolText,
|
|
7767
|
+
wrapMode: "word",
|
|
7768
|
+
})),
|
|
7769
|
+
commandBlock.omitted > 0
|
|
7770
|
+
? createText(ctx, `... +${commandBlock.omitted} lines, Ctrl+O to view`, {
|
|
7771
|
+
fg: theme.textMuted,
|
|
7772
|
+
wrapMode: "word",
|
|
7773
|
+
})
|
|
7774
|
+
: null,
|
|
7775
|
+
].filter((node) => !!node)));
|
|
7776
|
+
}
|
|
7470
7777
|
if (detailLines.length > 0) {
|
|
7471
7778
|
children.push(createBox(ctx, {
|
|
7472
7779
|
paddingLeft: 2,
|
|
@@ -7506,6 +7813,20 @@ function shouldRenderTraceGroupAsRawTool(tool) {
|
|
|
7506
7813
|
function traceGroupDetailLines(group) {
|
|
7507
7814
|
return group.previewLines.length > 0 ? group.previewLines : group.items;
|
|
7508
7815
|
}
|
|
7816
|
+
const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
|
|
7817
|
+
function executeInlineBudget(group, width) {
|
|
7818
|
+
return Math.max(14, width - group.title.length - 20);
|
|
7819
|
+
}
|
|
7820
|
+
// Returns the wrapped command block for execute groups, or null when the
|
|
7821
|
+
// command is short enough to live inline in the header (nothing clipped).
|
|
7822
|
+
function executeCommandBlockFor(group, width) {
|
|
7823
|
+
if (group.kind !== "execute")
|
|
7824
|
+
return null;
|
|
7825
|
+
if (shouldInlineExecuteCommand(group, executeInlineBudget(group, width)))
|
|
7826
|
+
return null;
|
|
7827
|
+
const block = executeCommandBlock(group, EXECUTE_COMMAND_BLOCK_MAX_LINES);
|
|
7828
|
+
return block.lines.length > 0 ? block : null;
|
|
7829
|
+
}
|
|
7509
7830
|
function traceGroupStatus(group) {
|
|
7510
7831
|
if (group.hasError) {
|
|
7511
7832
|
const count = group.errorCount || 1;
|
|
@@ -7532,8 +7853,15 @@ function traceGroupHeaderStyledText(group, width = 80) {
|
|
|
7532
7853
|
const chunks = [
|
|
7533
7854
|
fg(titleColor)(bold(group.title)),
|
|
7534
7855
|
];
|
|
7535
|
-
if (group.
|
|
7536
|
-
chunks.push(fg(theme.toolText)(` ${truncate(group.
|
|
7856
|
+
if (group.kind === "execute" && group.description) {
|
|
7857
|
+
chunks.push(fg(theme.toolText)(` ${truncate(group.description, commandWidth)}`));
|
|
7858
|
+
}
|
|
7859
|
+
else if (group.command) {
|
|
7860
|
+
// Execute commands only render inline when they fit whole; longer ones
|
|
7861
|
+
// move to the wrapped command block below instead of being clipped here.
|
|
7862
|
+
if (group.kind !== "execute" || shouldInlineExecuteCommand(group, commandWidth)) {
|
|
7863
|
+
chunks.push(fg(theme.toolText)(` ${truncate(group.command, commandWidth)}`));
|
|
7864
|
+
}
|
|
7537
7865
|
}
|
|
7538
7866
|
else if (group.count !== undefined && group.noun) {
|
|
7539
7867
|
chunks.push(fg(theme.textMuted)(` ${group.count} ${group.noun}`));
|
|
@@ -7544,6 +7872,8 @@ function traceGroupHeaderStyledText(group, width = 80) {
|
|
|
7544
7872
|
return new StyledText(chunks);
|
|
7545
7873
|
}
|
|
7546
7874
|
function traceGroupCompactLabel(group) {
|
|
7875
|
+
if (group.description)
|
|
7876
|
+
return `${group.title} ${group.description}`;
|
|
7547
7877
|
if (group.command)
|
|
7548
7878
|
return `${group.title} ${group.command}`;
|
|
7549
7879
|
if (group.count !== undefined && group.noun)
|
|
@@ -7573,6 +7903,8 @@ function traceGroupRenderableSignature(group) {
|
|
|
7573
7903
|
group.count ?? "",
|
|
7574
7904
|
group.noun ?? "",
|
|
7575
7905
|
group.command ?? "",
|
|
7906
|
+
group.description ?? "",
|
|
7907
|
+
hashString(stableStringify(group.commandLines ?? [])),
|
|
7576
7908
|
group.omitted,
|
|
7577
7909
|
hashString(stableStringify(group.items)),
|
|
7578
7910
|
hashString(stableStringify(group.previewLines)),
|
|
@@ -7896,14 +8228,15 @@ function createUserEntry(ctx, message, index, key, signature) {
|
|
|
7896
8228
|
wrapMode: "word",
|
|
7897
8229
|
});
|
|
7898
8230
|
refs.userText = text;
|
|
7899
|
-
const
|
|
8231
|
+
const inputBadge = userInputStatusBadgeLabel(message.inputStatus);
|
|
8232
|
+
const queuedText = createText(ctx, inputBadge ? ` ${inputBadge} ` : "", {
|
|
7900
8233
|
fg: theme.background,
|
|
7901
8234
|
bg: theme.primary,
|
|
7902
8235
|
});
|
|
7903
8236
|
refs.userQueuedText = queuedText;
|
|
7904
8237
|
const queuedBox = createBox(ctx, {
|
|
7905
8238
|
paddingTop: 1,
|
|
7906
|
-
visible:
|
|
8239
|
+
visible: !!inputBadge,
|
|
7907
8240
|
}, [queuedText]);
|
|
7908
8241
|
refs.userQueuedBox = queuedBox;
|
|
7909
8242
|
const node = createBox(ctx, {
|
|
@@ -7944,6 +8277,7 @@ function createErrorEntry(ctx, message, key, signature) {
|
|
|
7944
8277
|
return { key, signature, node, refs };
|
|
7945
8278
|
}
|
|
7946
8279
|
function createAssistantEntry(ctx, message, syntaxStyle, subtleSyntaxStyle, key, signature, showThinking = true, width = 80, verboseTrace = false, expandedWrites = new Set(), onToggleWrite) {
|
|
8280
|
+
message = sanitizeDisplayMessage(message);
|
|
7947
8281
|
const modelSwitch = parseModelSwitchMessage(message.content);
|
|
7948
8282
|
if (modelSwitch && !message.reasoning?.trim() && !(message.toolCalls?.length)) {
|
|
7949
8283
|
return createModelSwitchEntry(ctx, modelSwitch, key, signature);
|
|
@@ -8109,7 +8443,7 @@ function createCompactionCardEntry(ctx, message, key, signature, expanded, onTog
|
|
|
8109
8443
|
statsParts.push(`${meta.turns} turn${meta.turns === 1 ? "" : "s"}`);
|
|
8110
8444
|
if (meta?.messages)
|
|
8111
8445
|
statsParts.push(`${meta.messages} message${meta.messages === 1 ? "" : "s"}`);
|
|
8112
|
-
const statsLine = statsParts.length > 0 ? statsParts.join(" · ") : "
|
|
8446
|
+
const statsLine = statsParts.length > 0 ? `${statsParts.join(" · ")} collapsed` : "Collapsed";
|
|
8113
8447
|
const children = [];
|
|
8114
8448
|
const headerRow = createBox(ctx, {
|
|
8115
8449
|
flexDirection: "row",
|
|
@@ -8118,8 +8452,8 @@ function createCompactionCardEntry(ctx, message, key, signature, expanded, onTog
|
|
|
8118
8452
|
alignItems: "center",
|
|
8119
8453
|
}, [
|
|
8120
8454
|
createText(ctx, new StyledText([
|
|
8121
|
-
fg(theme.info)(bold("◈
|
|
8122
|
-
]), { width:
|
|
8455
|
+
fg(theme.info)(bold("◈ Earlier Conversation")),
|
|
8456
|
+
]), { width: 23 }),
|
|
8123
8457
|
createText(ctx, new StyledText([
|
|
8124
8458
|
fg(theme.textMuted)(`─ ${statsLine}`),
|
|
8125
8459
|
])),
|
|
@@ -8327,10 +8661,15 @@ function renderTraceGroup(group, syntaxStyle, width = 80) {
|
|
|
8327
8661
|
const status = traceGroupStatus(group);
|
|
8328
8662
|
const detailColor = traceGroupDetailColor(group);
|
|
8329
8663
|
const detailWidth = Math.max(20, width - 10);
|
|
8664
|
+
const commandBlock = executeCommandBlockFor(group, width);
|
|
8330
8665
|
return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", {
|
|
8331
8666
|
content: traceGroupHeaderStyledText(group, width),
|
|
8332
8667
|
wrapMode: "none",
|
|
8333
|
-
}),
|
|
8668
|
+
}), commandBlock
|
|
8669
|
+
? h("box", { paddingLeft: 2, flexDirection: "column", flexShrink: 0 }, ...commandBlock.lines.map((line, index) => h("text", { fg: theme.toolText, wrapMode: "word" }, `${index === 0 ? "$ " : " "}${line}`)), commandBlock.omitted > 0
|
|
8670
|
+
? h("text", { fg: theme.textMuted, wrapMode: "word" }, `... +${commandBlock.omitted} lines, Ctrl+O to view`)
|
|
8671
|
+
: null)
|
|
8672
|
+
: null, detailLines.length > 0
|
|
8334
8673
|
? h("box", { paddingLeft: 2, flexDirection: "column", flexShrink: 0 }, detailLines.map((line, index) => h("text", {
|
|
8335
8674
|
fg: detailColor,
|
|
8336
8675
|
wrapMode: "word",
|
|
@@ -8419,13 +8758,7 @@ function renderFooter(input) {
|
|
|
8419
8758
|
idleContent: `${shortCwd(input.cwd)} idle`,
|
|
8420
8759
|
idleFg: theme.textMuted,
|
|
8421
8760
|
runningFg: theme.primary,
|
|
8422
|
-
}), h("
|
|
8423
|
-
fg: permissionModeColor(input.mode()),
|
|
8424
|
-
ref: input.registerModeBadge,
|
|
8425
|
-
}, footerPermissionModeText(input.mode())), h("text", {
|
|
8426
|
-
fg: input.traceVerbose?.() ? theme.warning : theme.textMuted,
|
|
8427
|
-
ref: input.registerTraceBadge,
|
|
8428
|
-
}, footerTraceModeText(input.traceVerbose?.() === true)), h("box", { flexGrow: 1 }));
|
|
8761
|
+
}), h("box", { flexGrow: 1 }));
|
|
8429
8762
|
}
|
|
8430
8763
|
function pickerTitle(kind, providerId) {
|
|
8431
8764
|
switch (kind) {
|
|
@@ -8695,9 +9028,22 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
8695
9028
|
: "pending",
|
|
8696
9029
|
});
|
|
8697
9030
|
}
|
|
9031
|
+
// The aborted-assistant interruption note is model-facing bookkeeping —
|
|
9032
|
+
// strip it so it never renders as something the assistant "said".
|
|
9033
|
+
const interrupted = message.error?.aborted === true;
|
|
9034
|
+
let content = message.content;
|
|
9035
|
+
if (interrupted) {
|
|
9036
|
+
content = content === INTERRUPTED_ASSISTANT_CONTENT
|
|
9037
|
+
? ""
|
|
9038
|
+
: content.endsWith(`\n\n${INTERRUPTED_ASSISTANT_CONTENT}`)
|
|
9039
|
+
? content.slice(0, -`\n\n${INTERRUPTED_ASSISTANT_CONTENT}`.length)
|
|
9040
|
+
: content;
|
|
9041
|
+
if (!content && !message.reasoning && toolCalls.length === 0)
|
|
9042
|
+
continue;
|
|
9043
|
+
}
|
|
8698
9044
|
result.push({
|
|
8699
9045
|
role: "assistant",
|
|
8700
|
-
content
|
|
9046
|
+
content,
|
|
8701
9047
|
reasoning: message.reasoning || undefined,
|
|
8702
9048
|
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
8703
9049
|
});
|
|
@@ -8716,7 +9062,7 @@ function renderTranscript(messages, options, syntaxStyle, subtleSyntaxStyle) {
|
|
|
8716
9062
|
return items;
|
|
8717
9063
|
}
|
|
8718
9064
|
function renderSessionMessages(messages, syntaxStyle, subtleSyntaxStyle, showThinking = true, verboseTrace = false) {
|
|
8719
|
-
const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
|
|
9065
|
+
const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
|
|
8720
9066
|
if (!visibleMessages.length)
|
|
8721
9067
|
return null;
|
|
8722
9068
|
return visibleMessages.map((message, index) => renderMessage(message, index, syntaxStyle, subtleSyntaxStyle, showThinking, verboseTrace));
|
|
@@ -8724,7 +9070,7 @@ function renderSessionMessages(messages, syntaxStyle, subtleSyntaxStyle, showThi
|
|
|
8724
9070
|
function formatTranscript(messages, options) {
|
|
8725
9071
|
const showThinking = options?.showThinking ?? true;
|
|
8726
9072
|
const verboseTrace = options?.verboseTrace ?? false;
|
|
8727
|
-
const visibleMessages = messages.filter((message) => hasRenderableMessage(message, showThinking));
|
|
9073
|
+
const visibleMessages = sanitizeDisplayMessages(messages).filter((message) => hasRenderableMessage(message, showThinking));
|
|
8728
9074
|
const chunks = [];
|
|
8729
9075
|
const append = (content, color = theme.text) => {
|
|
8730
9076
|
if (content)
|
|
@@ -8873,6 +9219,15 @@ function appendTraceGroupTranscript(chunks, group) {
|
|
|
8873
9219
|
appendLine("");
|
|
8874
9220
|
if (group.pending)
|
|
8875
9221
|
return;
|
|
9222
|
+
// Verbose mode shows the full command with its original line structure
|
|
9223
|
+
// whenever the header line alone doesn't already carry it verbatim.
|
|
9224
|
+
const commandLines = group.commandLines ?? [];
|
|
9225
|
+
if (group.kind === "execute" && (group.description || commandLines.length > 1)) {
|
|
9226
|
+
for (const [index, line] of commandLines.entries()) {
|
|
9227
|
+
append(" ", theme.borderSubtle);
|
|
9228
|
+
appendLine(`${index === 0 ? "$ " : " "}${line}`, theme.toolText);
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
8876
9231
|
const detailLines = traceGroupDetailLines(group);
|
|
8877
9232
|
const detailColor = traceGroupDetailColor(group);
|
|
8878
9233
|
for (const [index, line] of detailLines.entries()) {
|
|
@@ -8901,6 +9256,7 @@ function renderHomeState(input) {
|
|
|
8901
9256
|
}, 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
9257
|
}
|
|
8903
9258
|
function hasRenderableMessage(message, showThinking = true) {
|
|
9259
|
+
message = sanitizeDisplayMessage(message);
|
|
8904
9260
|
if (message.role === "error")
|
|
8905
9261
|
return !!message.content.trim();
|
|
8906
9262
|
if (message.role === "user")
|
|
@@ -9057,17 +9413,6 @@ function permissionModeBadgeLabel(mode) {
|
|
|
9057
9413
|
case "bypassPermissions": return "Bypass";
|
|
9058
9414
|
}
|
|
9059
9415
|
}
|
|
9060
|
-
function footerPermissionModeText(mode) {
|
|
9061
|
-
const info = PERMISSION_MODE_INFO[mode];
|
|
9062
|
-
if (mode === "default")
|
|
9063
|
-
return " mode: build · shift+tab plan";
|
|
9064
|
-
if (mode === "plan")
|
|
9065
|
-
return " mode: plan · shift+tab bypass";
|
|
9066
|
-
return ` mode: ${info.shortTitle} · shift+tab build`;
|
|
9067
|
-
}
|
|
9068
|
-
function footerTraceModeText(verbose) {
|
|
9069
|
-
return verbose ? " trace: verbose · ctrl+o compact" : " trace: compact · ctrl+o verbose";
|
|
9070
|
-
}
|
|
9071
9416
|
function permissionModeColor(mode) {
|
|
9072
9417
|
const info = PERMISSION_MODE_INFO[mode];
|
|
9073
9418
|
switch (info.color) {
|