@bubblebrain-ai/bubble 0.0.20 → 0.0.22
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/abort-errors.d.ts +14 -0
- package/dist/agent/abort-errors.js +21 -0
- package/dist/agent/budget-ledger.d.ts +41 -0
- package/dist/agent/budget-ledger.js +64 -0
- package/dist/agent/child-runner.d.ts +55 -0
- package/dist/agent/child-runner.js +312 -0
- package/dist/agent/profiles.d.ts +8 -0
- package/dist/agent/profiles.js +27 -5
- package/dist/agent/result-integrator.d.ts +22 -0
- package/dist/agent/result-integrator.js +50 -0
- package/dist/agent/subagent-control.d.ts +31 -0
- package/dist/agent/subagent-control.js +27 -0
- package/dist/agent/subagent-lifecycle-reminder.js +11 -2
- package/dist/agent/subagent-scheduler.d.ts +95 -0
- package/dist/agent/subagent-scheduler.js +256 -0
- package/dist/agent/subagent-store.d.ts +41 -0
- package/dist/agent/subagent-store.js +149 -0
- package/dist/agent/subagent-summary.d.ts +30 -0
- package/dist/agent/subagent-summary.js +74 -0
- package/dist/agent/worktree.d.ts +29 -0
- package/dist/agent/worktree.js +73 -0
- package/dist/agent.d.ts +64 -5
- package/dist/agent.js +365 -288
- package/dist/approval/controller.js +9 -1
- package/dist/approval/tool-helper.js +2 -0
- package/dist/approval/types.d.ts +17 -1
- package/dist/checkpoints.d.ts +57 -0
- package/dist/checkpoints.js +0 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +17 -0
- package/dist/feishu/agent-host/approval-card.js +9 -0
- package/dist/feishu/agent-host/run-driver.js +2 -0
- package/dist/main.js +88 -13
- package/dist/network/errors.d.ts +28 -0
- package/dist/network/errors.js +24 -0
- package/dist/orchestrator/default-hooks.js +5 -1
- package/dist/prompt/compose.js +3 -0
- package/dist/prompt/delegation.d.ts +14 -0
- package/dist/prompt/delegation.js +64 -0
- package/dist/prompt/task-reminders.d.ts +5 -1
- package/dist/prompt/task-reminders.js +10 -2
- package/dist/provider-anthropic.js +23 -0
- package/dist/provider.js +23 -3
- package/dist/session.d.ts +31 -0
- package/dist/session.js +69 -0
- package/dist/slash-commands/commands.js +109 -2
- package/dist/slash-commands/types.d.ts +6 -0
- package/dist/tools/agent-lifecycle.d.ts +29 -3
- package/dist/tools/agent-lifecycle.js +394 -40
- package/dist/tools/bash.js +4 -0
- package/dist/tools/child-tools.d.ts +31 -0
- package/dist/tools/child-tools.js +106 -0
- package/dist/tools/edit.d.ts +2 -1
- package/dist/tools/edit.js +2 -1
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/index.js +3 -3
- package/dist/tools/write.d.ts +2 -1
- package/dist/tools/write.js +2 -1
- package/dist/tui/image-paste.d.ts +18 -0
- package/dist/tui/image-paste.js +60 -0
- package/dist/tui/run.d.ts +11 -1
- package/dist/tui/run.js +399 -71
- package/dist/tui/session-picker-data.d.ts +18 -0
- package/dist/tui/session-picker-data.js +21 -0
- package/dist/tui/trace-groups.d.ts +16 -0
- package/dist/tui/trace-groups.js +42 -1
- package/dist/tui/transcript-scroll.d.ts +25 -0
- package/dist/tui/transcript-scroll.js +20 -0
- package/dist/tui/wordmark.d.ts +2 -0
- package/dist/tui/wordmark.js +31 -4
- package/dist/tui-ink/app.d.ts +4 -1
- package/dist/tui-ink/app.js +301 -247
- package/dist/tui-ink/approval/approval-dialog.js +10 -0
- 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 +50 -2
- 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/approval/approval-dialog.js +10 -0
- package/dist/types.d.ts +17 -0
- package/package.json +1 -1
package/dist/tui/run.js
CHANGED
|
@@ -6,12 +6,14 @@ 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
13
|
import { createStreamingInternalReminderSanitizer, sanitizeInternalReasoningText, sanitizeInternalReminderBlocks, } from "../agent/internal-reminder-sanitizer.js";
|
|
14
14
|
import { summarizeAgentEventForTrace, summarizeTraceError, summarizeTraceValue, traceEvent, } from "../debug-trace.js";
|
|
15
|
+
import { SessionManager } from "../session.js";
|
|
16
|
+
import { buildSessionPickerEntries, preferredSessionPickerIndex } from "./session-picker-data.js";
|
|
15
17
|
import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
16
18
|
import { calculateUsageCost } from "../model-pricing.js";
|
|
17
19
|
import { getAvailableThinkingLevels } from "../provider-transform.js";
|
|
@@ -41,15 +43,16 @@ import { submitFeedback, FeedbackSubmitError } from "../feedback/submit.js";
|
|
|
41
43
|
import { createFrames } from "./opencode-spinner.js";
|
|
42
44
|
import { copyTextToClipboard } from "./clipboard.js";
|
|
43
45
|
import { readGitSidebarState } from "./sidebar-state.js";
|
|
44
|
-
import { buildImageContentPartsFromLabels, extractImagePathTokens, imageAttachmentLabelPattern, resolveComposerImagePaths, resolveImageInput, } from "./image-paste.js";
|
|
46
|
+
import { buildImageContentPartsFromLabels, bareImageFilenameFromPaste, extractImagePathTokens, imageAttachmentLabelPattern, imageLabelForPath, ingestClipboardImage, ingestImagePath, isImagePathPaste, splitPastedPaths, resolveComposerImagePaths, resolveImageInput, } from "./image-paste.js";
|
|
45
47
|
import { createPastedContentMarker, decodePastedBytes, expandPastedContentMarkers, shouldCollapsePastedContent, } from "./paste-placeholder.js";
|
|
46
48
|
import { isModeCycleKeyEvent, isModeCycleSequence, isModifiedEnterSequence, PROMPT_TEXTAREA_KEYBINDINGS, } from "./prompt-keybindings.js";
|
|
47
49
|
import { keyNameFromEvent, keyNameFromSequence } from "./global-key-router.js";
|
|
48
50
|
import { EscapeConfirmationGate } from "./escape-confirmation.js";
|
|
49
51
|
import { appendHistoryEntry, loadHistorySync, pushHistoryEntry } from "./input-history.js";
|
|
50
|
-
import { buildTraceGroups, traceGroupLabel } from "./trace-groups.js";
|
|
52
|
+
import { buildTraceGroups, executeCommandBlock, shouldInlineExecuteCommand, traceGroupLabel, } from "./trace-groups.js";
|
|
51
53
|
import { sessionDisplayName } from "./session-display.js";
|
|
52
54
|
import { bubbleWordmarkForWidth, bubbleWordmarkLineText, } from "./wordmark.js";
|
|
55
|
+
import { resolveTranscriptScroll } from "./transcript-scroll.js";
|
|
53
56
|
import { bootstrapConfig } from "../feishu/config.js";
|
|
54
57
|
import { ScopeRegistry } from "../feishu/scope/scope-registry.js";
|
|
55
58
|
const treeSitterClient = getTreeSitterClient();
|
|
@@ -477,6 +480,10 @@ function OpenTuiApp(props) {
|
|
|
477
480
|
let promptHistory = initialPromptHistory(displayMessages);
|
|
478
481
|
let nextImageAttachmentIndex = nextImageLabelIndex(displayMessages);
|
|
479
482
|
const pendingImageAttachments = new Map();
|
|
483
|
+
// Image-path pastes insert their [image#N] label immediately and ingest the
|
|
484
|
+
// file in the background; sends await these so a quick Enter can't outrun
|
|
485
|
+
// attachment registration.
|
|
486
|
+
const pendingImageIngestions = new Set();
|
|
480
487
|
// Long pastes are collapsed to "[Pasted text #N +M lines]" in the composer
|
|
481
488
|
// and expanded back to the full content when the message is submitted,
|
|
482
489
|
// mirroring how image attachments use "[Image #N]" labels.
|
|
@@ -550,6 +557,10 @@ function OpenTuiApp(props) {
|
|
|
550
557
|
let scrollbox;
|
|
551
558
|
let transcriptScrollFollowing = true;
|
|
552
559
|
let transcriptScrollInitialized = false;
|
|
560
|
+
// Set by forceFollow renders (user sends, approvals). Survives intervening
|
|
561
|
+
// streaming redraws that recompute follow state from the (still-unscrolled)
|
|
562
|
+
// position before the deferred scroll runs; cleared on user mouse scroll.
|
|
563
|
+
let transcriptForceScrollPending = false;
|
|
553
564
|
let rootBox;
|
|
554
565
|
let sidebarShell;
|
|
555
566
|
let homeSurfaceShell;
|
|
@@ -638,8 +649,6 @@ function OpenTuiApp(props) {
|
|
|
638
649
|
const providerDialogFooters = [];
|
|
639
650
|
const promptModeLabels = new Set();
|
|
640
651
|
const promptModelLabels = new Set();
|
|
641
|
-
let footerModeBadge;
|
|
642
|
-
let footerTraceBadge;
|
|
643
652
|
let sidebarTokenText;
|
|
644
653
|
let sidebarPercentText;
|
|
645
654
|
let sidebarGaugeText;
|
|
@@ -811,7 +820,6 @@ function OpenTuiApp(props) {
|
|
|
811
820
|
feishuSetupAbortController?.abort();
|
|
812
821
|
promptModeLabels.clear();
|
|
813
822
|
promptModelLabels.clear();
|
|
814
|
-
footerModeBadge = undefined;
|
|
815
823
|
});
|
|
816
824
|
function showCopyToast(toast, ttl = 2200) {
|
|
817
825
|
if (copyToastClearTimer)
|
|
@@ -1129,9 +1137,7 @@ function OpenTuiApp(props) {
|
|
|
1129
1137
|
}
|
|
1130
1138
|
const promptModeTitle = () => mode() === "plan" ? "Plan" : "Build";
|
|
1131
1139
|
const promptModeBadge = () => promptModeBadgeContent(mode());
|
|
1132
|
-
const footerModeText = () => footerPermissionModeText(mode());
|
|
1133
1140
|
const effectiveShowThinking = () => showThinking() || verboseTrace();
|
|
1134
|
-
const footerTraceText = () => footerTraceModeText(verboseTrace());
|
|
1135
1141
|
function syncModeChrome() {
|
|
1136
1142
|
if (uiDisposed)
|
|
1137
1143
|
return;
|
|
@@ -1139,22 +1145,12 @@ function OpenTuiApp(props) {
|
|
|
1139
1145
|
if (!safeSetText(label, promptModeBadge()))
|
|
1140
1146
|
promptModeLabels.delete(label);
|
|
1141
1147
|
}
|
|
1142
|
-
if (footerModeBadge) {
|
|
1143
|
-
footerModeBadge.fg = permissionModeColor(mode());
|
|
1144
|
-
if (!safeSetText(footerModeBadge, footerModeText()))
|
|
1145
|
-
footerModeBadge = undefined;
|
|
1146
|
-
}
|
|
1147
1148
|
safeRequestRender(sessionComposerShell);
|
|
1148
1149
|
safeRequestRender(rootBox);
|
|
1149
1150
|
}
|
|
1150
1151
|
function syncTraceChrome() {
|
|
1151
1152
|
if (uiDisposed)
|
|
1152
1153
|
return;
|
|
1153
|
-
if (footerTraceBadge) {
|
|
1154
|
-
footerTraceBadge.fg = verboseTrace() ? theme.warning : theme.textMuted;
|
|
1155
|
-
if (!safeSetText(footerTraceBadge, footerTraceText()))
|
|
1156
|
-
footerTraceBadge = undefined;
|
|
1157
|
-
}
|
|
1158
1154
|
safeRequestRender(rootBox);
|
|
1159
1155
|
}
|
|
1160
1156
|
const registerPromptModeLabel = (ref) => {
|
|
@@ -1182,23 +1178,14 @@ function OpenTuiApp(props) {
|
|
|
1182
1178
|
if (!safeSetText(ref, promptModelTitle()))
|
|
1183
1179
|
promptModelLabels.delete(ref);
|
|
1184
1180
|
};
|
|
1185
|
-
const registerFooterModeBadge = (ref) => {
|
|
1186
|
-
if (uiDisposed)
|
|
1187
|
-
return;
|
|
1188
|
-
footerModeBadge = ref;
|
|
1189
|
-
if (!safeSetText(ref, footerModeText()))
|
|
1190
|
-
footerModeBadge = undefined;
|
|
1191
|
-
};
|
|
1192
|
-
const registerFooterTraceBadge = (ref) => {
|
|
1193
|
-
if (uiDisposed)
|
|
1194
|
-
return;
|
|
1195
|
-
footerTraceBadge = ref;
|
|
1196
|
-
ref.fg = verboseTrace() ? theme.warning : theme.textMuted;
|
|
1197
|
-
if (!safeSetText(ref, footerTraceText()))
|
|
1198
|
-
footerTraceBadge = undefined;
|
|
1199
|
-
};
|
|
1200
1181
|
const cycleMode = () => {
|
|
1201
|
-
|
|
1182
|
+
// Mode switching is intentionally allowed while the agent is running:
|
|
1183
|
+
// Agent.setMode() is safe mid-run and the approval controller reads the
|
|
1184
|
+
// live mode on every request, so flipping to bypass (or into plan) takes
|
|
1185
|
+
// effect from the very next tool call — no need to wait for the turn to
|
|
1186
|
+
// finish. Only pickers and the plan-approval dialog still block it,
|
|
1187
|
+
// because those surfaces own the keyboard.
|
|
1188
|
+
if (picker || pendingPlan())
|
|
1202
1189
|
return false;
|
|
1203
1190
|
const next = getNextPermissionMode(props.agent.mode);
|
|
1204
1191
|
props.agent.setMode(next);
|
|
@@ -1502,7 +1489,13 @@ function OpenTuiApp(props) {
|
|
|
1502
1489
|
setTimeout(() => {
|
|
1503
1490
|
if (!scrollbox)
|
|
1504
1491
|
return;
|
|
1505
|
-
|
|
1492
|
+
const action = resolveTranscriptScroll({
|
|
1493
|
+
forcePending: transcriptForceScrollPending,
|
|
1494
|
+
shouldFollow,
|
|
1495
|
+
following: transcriptScrollFollowing,
|
|
1496
|
+
});
|
|
1497
|
+
if (action === "scroll-bottom") {
|
|
1498
|
+
transcriptForceScrollPending = false;
|
|
1506
1499
|
scrollTranscriptToBottom();
|
|
1507
1500
|
}
|
|
1508
1501
|
else {
|
|
@@ -1511,6 +1504,7 @@ function OpenTuiApp(props) {
|
|
|
1511
1504
|
}, delay);
|
|
1512
1505
|
}
|
|
1513
1506
|
function handleTranscriptMouseScroll() {
|
|
1507
|
+
transcriptForceScrollPending = false;
|
|
1514
1508
|
setTimeout(updateTranscriptScrollFollowingFromPosition, 0);
|
|
1515
1509
|
}
|
|
1516
1510
|
function syncQuestionUI(focusCustom = false) {
|
|
@@ -2460,8 +2454,8 @@ function OpenTuiApp(props) {
|
|
|
2460
2454
|
function queuedInputLabel(count = queuedInputCount()) {
|
|
2461
2455
|
return `${count} queued message${count === 1 ? "" : "s"}`;
|
|
2462
2456
|
}
|
|
2463
|
-
function redrawTranscriptWithQueuedDisplays() {
|
|
2464
|
-
redrawTranscript(streamingDisplay, displayMessages);
|
|
2457
|
+
function redrawTranscriptWithQueuedDisplays(options = {}) {
|
|
2458
|
+
redrawTranscript(streamingDisplay, displayMessages, options);
|
|
2465
2459
|
}
|
|
2466
2460
|
function addUserInputStatusDisplay(input, inputStatus) {
|
|
2467
2461
|
const displayId = `queued-${++nextQueuedDisplayId}`;
|
|
@@ -2469,7 +2463,9 @@ function OpenTuiApp(props) {
|
|
|
2469
2463
|
...queuedDisplayMessages,
|
|
2470
2464
|
{ role: "user", content: input, clientId: displayId, inputStatus },
|
|
2471
2465
|
];
|
|
2472
|
-
|
|
2466
|
+
// Sending a message is explicit user intent to look at the newest turn:
|
|
2467
|
+
// snap to the bottom even if the transcript was scrolled up.
|
|
2468
|
+
redrawTranscriptWithQueuedDisplays({ forceFollow: true });
|
|
2473
2469
|
return displayId;
|
|
2474
2470
|
}
|
|
2475
2471
|
function addQueuedUserDisplay(input) {
|
|
@@ -2955,6 +2951,7 @@ function OpenTuiApp(props) {
|
|
|
2955
2951
|
if (options.forceFollow) {
|
|
2956
2952
|
transcriptScrollFollowing = true;
|
|
2957
2953
|
transcriptScrollInitialized = true;
|
|
2954
|
+
transcriptForceScrollPending = true;
|
|
2958
2955
|
}
|
|
2959
2956
|
const nextMessages = compactDisplayMessages([
|
|
2960
2957
|
...baseMessages,
|
|
@@ -3048,7 +3045,15 @@ function OpenTuiApp(props) {
|
|
|
3048
3045
|
step,
|
|
3049
3046
|
providerId,
|
|
3050
3047
|
query: "",
|
|
3051
|
-
index: step === "models"
|
|
3048
|
+
index: step === "models"
|
|
3049
|
+
? preferredPickerIndex("model", items)
|
|
3050
|
+
// "(current)" sits at the bottom of the rewind list and is the safe default.
|
|
3051
|
+
: step === "rewind"
|
|
3052
|
+
? Math.max(0, items.length - 1)
|
|
3053
|
+
// Sessions: start on the most recent conversation that is not the active one.
|
|
3054
|
+
: step === "sessions"
|
|
3055
|
+
? preferredSessionPickerIndex(items)
|
|
3056
|
+
: 0,
|
|
3052
3057
|
apiKey: "",
|
|
3053
3058
|
};
|
|
3054
3059
|
activePrompt()?.clear();
|
|
@@ -3079,6 +3084,12 @@ function OpenTuiApp(props) {
|
|
|
3079
3084
|
return providerId ? buildPickerItems("provider-auth", providerId) : [];
|
|
3080
3085
|
if (step === "skills")
|
|
3081
3086
|
return buildSkillItems();
|
|
3087
|
+
if (step === "rewind")
|
|
3088
|
+
return buildRewindPickerItems();
|
|
3089
|
+
if (step === "rewind-action")
|
|
3090
|
+
return buildRewindActionItems(providerId);
|
|
3091
|
+
if (step === "sessions")
|
|
3092
|
+
return buildSessionPickerItems();
|
|
3082
3093
|
if (step === "models") {
|
|
3083
3094
|
if (providerDialogModelItems?.key === modelPickerCacheKey(providerId)) {
|
|
3084
3095
|
return providerDialogModelItems.items;
|
|
@@ -3317,6 +3328,12 @@ function OpenTuiApp(props) {
|
|
|
3317
3328
|
return "Connect a provider";
|
|
3318
3329
|
if (state.step === "skills")
|
|
3319
3330
|
return "Select skill";
|
|
3331
|
+
if (state.step === "rewind")
|
|
3332
|
+
return "Rewind — restore to the point before…";
|
|
3333
|
+
if (state.step === "rewind-action")
|
|
3334
|
+
return "Rewind — what to restore?";
|
|
3335
|
+
if (state.step === "sessions")
|
|
3336
|
+
return "Resume a session";
|
|
3320
3337
|
const provider = providerDisplayName(state.providerId);
|
|
3321
3338
|
if (state.step === "auth")
|
|
3322
3339
|
return `${provider} auth method`;
|
|
@@ -3337,6 +3354,12 @@ function OpenTuiApp(props) {
|
|
|
3337
3354
|
}
|
|
3338
3355
|
if (state.step === "skills")
|
|
3339
3356
|
return `↑/↓ move · enter insert · esc close${count}`;
|
|
3357
|
+
if (state.step === "rewind")
|
|
3358
|
+
return `↑/↓ move · enter continue · esc cancel${count}`;
|
|
3359
|
+
if (state.step === "rewind-action")
|
|
3360
|
+
return "↑/↓ move · enter confirm · esc back";
|
|
3361
|
+
if (state.step === "sessions")
|
|
3362
|
+
return `↑/↓ move · enter resume · esc close${count}`;
|
|
3340
3363
|
const escLabel = state.step === "providers" ? "esc close" : "esc back";
|
|
3341
3364
|
return `↑/↓ move · enter select · ${escLabel}${count}`;
|
|
3342
3365
|
}
|
|
@@ -3354,9 +3377,19 @@ function OpenTuiApp(props) {
|
|
|
3354
3377
|
}
|
|
3355
3378
|
function providerDialogColumnWidths(state, panelWidth) {
|
|
3356
3379
|
const contentWidth = Math.max(24, panelWidth - PROVIDER_DIALOG_ROW_RESERVED_WIDTH);
|
|
3357
|
-
const footer = state.step === "skills" ? 10 : state.step === "providers" ? 9 : 8;
|
|
3380
|
+
const footer = state.step === "skills" || state.step === "sessions" ? 10 : state.step === "providers" ? 9 : 8;
|
|
3358
3381
|
const minLabel = state.step === "skills" ? 18 : 24;
|
|
3359
|
-
const desiredDetail = state.step === "skills"
|
|
3382
|
+
const desiredDetail = state.step === "skills"
|
|
3383
|
+
? 30
|
|
3384
|
+
: state.step === "providers"
|
|
3385
|
+
? 24
|
|
3386
|
+
: state.step === "rewind-action"
|
|
3387
|
+
? 40
|
|
3388
|
+
: state.step === "rewind"
|
|
3389
|
+
? 18
|
|
3390
|
+
: state.step === "sessions"
|
|
3391
|
+
? 14
|
|
3392
|
+
: 16;
|
|
3360
3393
|
const detail = Math.max(8, Math.min(desiredDetail, contentWidth - footer - minLabel));
|
|
3361
3394
|
const label = Math.max(8, contentWidth - detail - footer);
|
|
3362
3395
|
return { label, detail, footer };
|
|
@@ -3424,8 +3457,8 @@ function OpenTuiApp(props) {
|
|
|
3424
3457
|
else if (state.step === "key") {
|
|
3425
3458
|
openProviderDialog(state.providerId && registry.supportsOAuth(state.providerId) ? "auth" : "providers", state.providerId);
|
|
3426
3459
|
}
|
|
3427
|
-
else if (state.step === "
|
|
3428
|
-
|
|
3460
|
+
else if (state.step === "rewind-action") {
|
|
3461
|
+
openProviderDialog("rewind");
|
|
3429
3462
|
}
|
|
3430
3463
|
else {
|
|
3431
3464
|
closeProviderDialog();
|
|
@@ -3556,6 +3589,29 @@ function OpenTuiApp(props) {
|
|
|
3556
3589
|
await executeSlash(item.command);
|
|
3557
3590
|
return;
|
|
3558
3591
|
}
|
|
3592
|
+
if (state.step === "rewind") {
|
|
3593
|
+
if (!item.value) {
|
|
3594
|
+
// "(current)" — keep everything as is.
|
|
3595
|
+
closeProviderDialog();
|
|
3596
|
+
return;
|
|
3597
|
+
}
|
|
3598
|
+
openProviderDialog("rewind-action", item.value);
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3601
|
+
if (state.step === "sessions") {
|
|
3602
|
+
closeProviderDialog();
|
|
3603
|
+
if (!item.value || item.value === props.options.sessionManager?.getSessionFile()) {
|
|
3604
|
+
// Selecting the active session keeps everything as is.
|
|
3605
|
+
return;
|
|
3606
|
+
}
|
|
3607
|
+
await switchToSession(item.value);
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
if (state.step === "rewind-action") {
|
|
3611
|
+
closeProviderDialog();
|
|
3612
|
+
await executeSlash(item.command);
|
|
3613
|
+
return;
|
|
3614
|
+
}
|
|
3559
3615
|
if (state.step === "skills") {
|
|
3560
3616
|
closeProviderDialog();
|
|
3561
3617
|
insertSkillPrompt(item.value);
|
|
@@ -4680,9 +4736,54 @@ function OpenTuiApp(props) {
|
|
|
4680
4736
|
const references = [...pendingPastedTexts.entries()].map(([marker, content]) => ({ marker, content }));
|
|
4681
4737
|
return expandPastedContentMarkers(text, references);
|
|
4682
4738
|
}
|
|
4739
|
+
// Inserts [image#N] labels at the cursor, padding with spaces when the
|
|
4740
|
+
// paste lands glued to surrounding text. Returns false when no prompt is
|
|
4741
|
+
// mounted (the caller should leave the paste alone).
|
|
4742
|
+
function insertComposerImageLabels(event, labels) {
|
|
4743
|
+
const prompt = activePrompt();
|
|
4744
|
+
if (!prompt)
|
|
4745
|
+
return false;
|
|
4746
|
+
event.preventDefault?.();
|
|
4747
|
+
const current = prompt.plainText ?? "";
|
|
4748
|
+
const offset = Math.min(Math.max(prompt.cursorOffset ?? current.length, 0), current.length);
|
|
4749
|
+
const needsLead = offset > 0 && !/\s/.test(current[offset - 1] ?? "");
|
|
4750
|
+
const needsTrail = offset < current.length && !/\s/.test(current[offset] ?? "");
|
|
4751
|
+
const joined = labels.map((label) => `[${label}]`).join(" ");
|
|
4752
|
+
prompt.insertText(`${needsLead ? " " : ""}${joined}${needsTrail ? " " : ""}`);
|
|
4753
|
+
onPromptContentChange(readPromptText());
|
|
4754
|
+
return true;
|
|
4755
|
+
}
|
|
4683
4756
|
function handleComposerPaste(event) {
|
|
4684
4757
|
const text = typeof event.text === "string" ? event.text : decodePastedBytes(event.bytes);
|
|
4685
|
-
if (
|
|
4758
|
+
if (isImagePathPaste(text)) {
|
|
4759
|
+
// Insert the final [image#N] label at paste time and ingest the file in
|
|
4760
|
+
// the background. Inserting the raw path and swapping it later flashes
|
|
4761
|
+
// the path and resets the cursor (setPromptText jumps it to the end).
|
|
4762
|
+
const entries = splitPastedPaths(text).map((rawPath) => ({
|
|
4763
|
+
rawPath,
|
|
4764
|
+
label: imageLabelForPath(rawPath, nextImageAttachmentIndex),
|
|
4765
|
+
}));
|
|
4766
|
+
if (!insertComposerImageLabels(event, entries.map((entry) => entry.label)))
|
|
4767
|
+
return;
|
|
4768
|
+
nextImageAttachmentIndex += entries.length;
|
|
4769
|
+
trackImagePathIngestion(entries);
|
|
4770
|
+
return;
|
|
4771
|
+
}
|
|
4772
|
+
// Copying an image file in Finder pastes only the file's NAME; Cmd+V of
|
|
4773
|
+
// raw image data pastes nothing at all. Both leave the real bits on the
|
|
4774
|
+
// system clipboard, so attach from there.
|
|
4775
|
+
const bareName = bareImageFilenameFromPaste(text);
|
|
4776
|
+
if (bareName || !text.trim()) {
|
|
4777
|
+
const label = bareName
|
|
4778
|
+
? imageLabelForPath(bareName, nextImageAttachmentIndex)
|
|
4779
|
+
: `image#${nextImageAttachmentIndex}.png`;
|
|
4780
|
+
if (!insertComposerImageLabels(event, [label]))
|
|
4781
|
+
return;
|
|
4782
|
+
nextImageAttachmentIndex += 1;
|
|
4783
|
+
trackClipboardImageIngestion(label, text);
|
|
4784
|
+
return;
|
|
4785
|
+
}
|
|
4786
|
+
if (!shouldCollapsePastedContent(text))
|
|
4686
4787
|
return;
|
|
4687
4788
|
event.preventDefault?.();
|
|
4688
4789
|
const marker = createPastedContentMarker(text, nextPastedTextIndex);
|
|
@@ -4692,6 +4793,62 @@ function OpenTuiApp(props) {
|
|
|
4692
4793
|
prompt?.insertText(marker);
|
|
4693
4794
|
onPromptContentChange(readPromptText());
|
|
4694
4795
|
}
|
|
4796
|
+
function trackImageIngestion(task) {
|
|
4797
|
+
pendingImageIngestions.add(task);
|
|
4798
|
+
void task.finally(() => pendingImageIngestions.delete(task));
|
|
4799
|
+
}
|
|
4800
|
+
function trackImagePathIngestion(entries) {
|
|
4801
|
+
trackImageIngestion((async () => {
|
|
4802
|
+
for (const { rawPath, label } of entries) {
|
|
4803
|
+
const result = await ingestImagePath(rawPath);
|
|
4804
|
+
if (result.attachment) {
|
|
4805
|
+
pendingImageAttachments.set(label, result.attachment);
|
|
4806
|
+
}
|
|
4807
|
+
else {
|
|
4808
|
+
addMessage("error", `Skipped image: ${rawPath}: ${result.error ?? "could not attach image"}`);
|
|
4809
|
+
replaceComposerImageLabel(label, "");
|
|
4810
|
+
}
|
|
4811
|
+
}
|
|
4812
|
+
})());
|
|
4813
|
+
}
|
|
4814
|
+
function trackClipboardImageIngestion(label, originalText) {
|
|
4815
|
+
trackImageIngestion((async () => {
|
|
4816
|
+
const result = await ingestClipboardImage();
|
|
4817
|
+
if (result.attachment) {
|
|
4818
|
+
pendingImageAttachments.set(label, result.attachment);
|
|
4819
|
+
return;
|
|
4820
|
+
}
|
|
4821
|
+
const restored = originalText.trim();
|
|
4822
|
+
// A filename-looking text paste with no image on the clipboard is just
|
|
4823
|
+
// text — restore it quietly. Only an empty paste (Cmd+V of image data)
|
|
4824
|
+
// warrants an error, since there is nothing to restore.
|
|
4825
|
+
if (!restored)
|
|
4826
|
+
addMessage("error", `Could not attach image from clipboard: ${result.error ?? "unknown error"}`);
|
|
4827
|
+
replaceComposerImageLabel(label, restored);
|
|
4828
|
+
})());
|
|
4829
|
+
}
|
|
4830
|
+
// Swaps a failed image label for its replacement (or drops it) without
|
|
4831
|
+
// moving the cursor relative to the surrounding text.
|
|
4832
|
+
function replaceComposerImageLabel(label, replacement) {
|
|
4833
|
+
const prompt = activePrompt();
|
|
4834
|
+
const current = prompt?.plainText ?? promptText;
|
|
4835
|
+
const token = `[${label}]`;
|
|
4836
|
+
const start = current.indexOf(token);
|
|
4837
|
+
if (start < 0)
|
|
4838
|
+
return;
|
|
4839
|
+
let end = start + token.length;
|
|
4840
|
+
if (!replacement && current[end] === " ")
|
|
4841
|
+
end += 1;
|
|
4842
|
+
const next = current.slice(0, start) + replacement + current.slice(end);
|
|
4843
|
+
if (prompt) {
|
|
4844
|
+
const cursor = Math.min(Math.max(prompt.cursorOffset ?? next.length, 0), current.length);
|
|
4845
|
+
prompt.setText(next);
|
|
4846
|
+
prompt.cursorOffset = cursor <= start
|
|
4847
|
+
? cursor
|
|
4848
|
+
: Math.min(next.length, Math.max(start + replacement.length, cursor - (end - start) + replacement.length));
|
|
4849
|
+
}
|
|
4850
|
+
onPromptContentChange(next);
|
|
4851
|
+
}
|
|
4695
4852
|
async function expandTextParts(parts) {
|
|
4696
4853
|
const expandedParts = [];
|
|
4697
4854
|
for (const part of parts) {
|
|
@@ -4712,6 +4869,8 @@ function OpenTuiApp(props) {
|
|
|
4712
4869
|
}
|
|
4713
4870
|
async function handleInput(input, options = {}) {
|
|
4714
4871
|
setNotice("");
|
|
4872
|
+
if (pendingImageIngestions.size > 0)
|
|
4873
|
+
await Promise.all([...pendingImageIngestions]);
|
|
4715
4874
|
const labeledInput = buildImageContentPartsFromLabels(input, pendingImageAttachments);
|
|
4716
4875
|
if (labeledInput.actualInput) {
|
|
4717
4876
|
await runAgentInput(await expandTextParts(labeledInput.actualInput), labeledInput.displayInput, options);
|
|
@@ -4775,6 +4934,17 @@ function OpenTuiApp(props) {
|
|
|
4775
4934
|
openPicker: (kind, providerId) => {
|
|
4776
4935
|
void openPicker(kind, providerId);
|
|
4777
4936
|
},
|
|
4937
|
+
openRewindPicker: () => {
|
|
4938
|
+
openProviderDialog("rewind");
|
|
4939
|
+
},
|
|
4940
|
+
openSessionPicker: () => {
|
|
4941
|
+
openProviderDialog("sessions");
|
|
4942
|
+
},
|
|
4943
|
+
fillComposer: (text) => {
|
|
4944
|
+
resetPromptHistoryBrowse();
|
|
4945
|
+
setPromptText(text);
|
|
4946
|
+
redrawDock();
|
|
4947
|
+
},
|
|
4778
4948
|
registry,
|
|
4779
4949
|
skillRegistry: skills,
|
|
4780
4950
|
bashAllowlist: props.options.bashAllowlist,
|
|
@@ -4821,6 +4991,13 @@ function OpenTuiApp(props) {
|
|
|
4821
4991
|
redrawTranscript(undefined, displayMessages);
|
|
4822
4992
|
setTimeout(() => setNotice(""), 4000);
|
|
4823
4993
|
}
|
|
4994
|
+
else if (result.startsWith("⏪")) {
|
|
4995
|
+
// /rewind truncated agent.messages — rebuild the transcript from the
|
|
4996
|
+
// rewound state before appending the summary.
|
|
4997
|
+
displayMessages = reconstructDisplayMessages(props.agent.messages);
|
|
4998
|
+
streamingDisplay = undefined;
|
|
4999
|
+
addMessage("assistant", result);
|
|
5000
|
+
}
|
|
4824
5001
|
else {
|
|
4825
5002
|
addMessage("assistant", result);
|
|
4826
5003
|
}
|
|
@@ -5092,6 +5269,86 @@ function OpenTuiApp(props) {
|
|
|
5092
5269
|
command: `/logout ${provider.id}`,
|
|
5093
5270
|
}));
|
|
5094
5271
|
}
|
|
5272
|
+
function buildRewindPickerItems() {
|
|
5273
|
+
const session = props.options.sessionManager;
|
|
5274
|
+
if (!session)
|
|
5275
|
+
return [];
|
|
5276
|
+
const checkpoints = session.getCheckpoints();
|
|
5277
|
+
const items = session.listUserTurns().map((turn, index) => {
|
|
5278
|
+
const files = checkpoints.filesTouchedAt(turn.id).length;
|
|
5279
|
+
return {
|
|
5280
|
+
label: turn.preview,
|
|
5281
|
+
detail: files > 0 ? `${files} file${files === 1 ? "" : "s"} changed` : "No code changes",
|
|
5282
|
+
value: String(index + 1),
|
|
5283
|
+
command: `/rewind ${index + 1}`,
|
|
5284
|
+
};
|
|
5285
|
+
});
|
|
5286
|
+
// Selecting "(current)" keeps everything as is — mirrors Claude Code.
|
|
5287
|
+
items.push({ label: "(current)", value: "", command: "" });
|
|
5288
|
+
return items;
|
|
5289
|
+
}
|
|
5290
|
+
function buildSessionPickerItems() {
|
|
5291
|
+
const activeFile = props.options.sessionManager?.getSessionFile();
|
|
5292
|
+
const summaries = SessionManager.summarizeSessionsForCwd(props.args.cwd);
|
|
5293
|
+
return buildSessionPickerEntries(summaries, activeFile).map((entry) => ({
|
|
5294
|
+
label: entry.label,
|
|
5295
|
+
detail: entry.detail,
|
|
5296
|
+
value: entry.value,
|
|
5297
|
+
command: "",
|
|
5298
|
+
footer: entry.footer,
|
|
5299
|
+
gutter: entry.gutter,
|
|
5300
|
+
}));
|
|
5301
|
+
}
|
|
5302
|
+
async function switchToSession(sessionFile) {
|
|
5303
|
+
const switchSession = props.options.switchSession;
|
|
5304
|
+
if (!switchSession) {
|
|
5305
|
+
addMessage("error", "Session switching is not available in this mode.");
|
|
5306
|
+
return;
|
|
5307
|
+
}
|
|
5308
|
+
if (isRunning()) {
|
|
5309
|
+
setNotice("Stop the current run before switching sessions.");
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5312
|
+
const result = switchSession(sessionFile);
|
|
5313
|
+
if ("error" in result) {
|
|
5314
|
+
addMessage("error", `Failed to switch session: ${result.error}`);
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
5317
|
+
props.options.sessionManager = result.manager;
|
|
5318
|
+
// Same rebuild path as /rewind: the agent history was replaced wholesale,
|
|
5319
|
+
// so reconstruct the transcript from it instead of patching the display.
|
|
5320
|
+
displayMessages = reconstructDisplayMessages(props.agent.messages);
|
|
5321
|
+
streamingDisplay = undefined;
|
|
5322
|
+
redrawTranscript(undefined, displayMessages);
|
|
5323
|
+
syncTodosFromAgent();
|
|
5324
|
+
bumpSidebar();
|
|
5325
|
+
syncPromptSurfaces(true);
|
|
5326
|
+
addMessage("assistant", `⤷ Resumed session: ${sessionDisplayName(result.manager)}`);
|
|
5327
|
+
}
|
|
5328
|
+
function buildRewindActionItems(turnNumber) {
|
|
5329
|
+
if (!turnNumber)
|
|
5330
|
+
return [];
|
|
5331
|
+
return [
|
|
5332
|
+
{
|
|
5333
|
+
label: "Restore conversation and code",
|
|
5334
|
+
detail: "Rewind the chat and undo tracked file edits",
|
|
5335
|
+
value: turnNumber,
|
|
5336
|
+
command: `/rewind ${turnNumber}`,
|
|
5337
|
+
},
|
|
5338
|
+
{
|
|
5339
|
+
label: "Restore conversation only",
|
|
5340
|
+
detail: "Keep file changes on disk",
|
|
5341
|
+
value: turnNumber,
|
|
5342
|
+
command: `/rewind ${turnNumber} --chat`,
|
|
5343
|
+
},
|
|
5344
|
+
{
|
|
5345
|
+
label: "Restore code only",
|
|
5346
|
+
detail: "Undo tracked file edits, keep the conversation",
|
|
5347
|
+
value: turnNumber,
|
|
5348
|
+
command: `/rewind ${turnNumber} --code`,
|
|
5349
|
+
},
|
|
5350
|
+
];
|
|
5351
|
+
}
|
|
5095
5352
|
function buildSkillItems() {
|
|
5096
5353
|
return skills.summaries().map((skill) => {
|
|
5097
5354
|
const tags = skill.tags && skill.tags.length > 0 ? ` · ${skill.tags.join(", ")}` : "";
|
|
@@ -5216,7 +5473,9 @@ function OpenTuiApp(props) {
|
|
|
5216
5473
|
if (!reusedQueuedDisplay)
|
|
5217
5474
|
displayMessages = nextMessages;
|
|
5218
5475
|
streamingDisplay = undefined;
|
|
5219
|
-
|
|
5476
|
+
// The user just sent this message — re-engage bottom-follow so the new
|
|
5477
|
+
// turn is visible even if they had scrolled up to read earlier history.
|
|
5478
|
+
redrawTranscript(undefined, nextMessages, { forceFollow: true });
|
|
5220
5479
|
const taskStartedAt = Date.now();
|
|
5221
5480
|
const run = beginAgentRun();
|
|
5222
5481
|
traceEvent("tui_agent_run_begin", {
|
|
@@ -6916,12 +7175,8 @@ function OpenTuiApp(props) {
|
|
|
6916
7175
|
]),
|
|
6917
7176
|
renderFooter({
|
|
6918
7177
|
cwd: props.args.cwd,
|
|
6919
|
-
mode,
|
|
6920
7178
|
running: isRunning,
|
|
6921
7179
|
registerScanner: registerPromptScanner,
|
|
6922
|
-
registerModeBadge: registerFooterModeBadge,
|
|
6923
|
-
traceVerbose: verboseTrace,
|
|
6924
|
-
registerTraceBadge: registerFooterTraceBadge,
|
|
6925
7180
|
}),
|
|
6926
7181
|
renderProviderDialog(),
|
|
6927
7182
|
renderStatsPanel(),
|
|
@@ -7569,6 +7824,25 @@ function createTraceGroupRenderable(ctx, group, syntaxStyle, width = 80) {
|
|
|
7569
7824
|
const children = [
|
|
7570
7825
|
createText(ctx, traceGroupHeaderStyledText(group, width), { wrapMode: "none" }),
|
|
7571
7826
|
];
|
|
7827
|
+
const commandBlock = executeCommandBlockFor(group, width);
|
|
7828
|
+
if (commandBlock) {
|
|
7829
|
+
children.push(createBox(ctx, {
|
|
7830
|
+
paddingLeft: 2,
|
|
7831
|
+
flexDirection: "column",
|
|
7832
|
+
flexShrink: 0,
|
|
7833
|
+
}, [
|
|
7834
|
+
...commandBlock.lines.map((line, index) => createText(ctx, `${index === 0 ? "$ " : " "}${line}`, {
|
|
7835
|
+
fg: theme.toolText,
|
|
7836
|
+
wrapMode: "word",
|
|
7837
|
+
})),
|
|
7838
|
+
commandBlock.omitted > 0
|
|
7839
|
+
? createText(ctx, `... +${commandBlock.omitted} lines, Ctrl+O to view`, {
|
|
7840
|
+
fg: theme.textMuted,
|
|
7841
|
+
wrapMode: "word",
|
|
7842
|
+
})
|
|
7843
|
+
: null,
|
|
7844
|
+
].filter((node) => !!node)));
|
|
7845
|
+
}
|
|
7572
7846
|
if (detailLines.length > 0) {
|
|
7573
7847
|
children.push(createBox(ctx, {
|
|
7574
7848
|
paddingLeft: 2,
|
|
@@ -7608,6 +7882,20 @@ function shouldRenderTraceGroupAsRawTool(tool) {
|
|
|
7608
7882
|
function traceGroupDetailLines(group) {
|
|
7609
7883
|
return group.previewLines.length > 0 ? group.previewLines : group.items;
|
|
7610
7884
|
}
|
|
7885
|
+
const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
|
|
7886
|
+
function executeInlineBudget(group, width) {
|
|
7887
|
+
return Math.max(14, width - group.title.length - 20);
|
|
7888
|
+
}
|
|
7889
|
+
// Returns the wrapped command block for execute groups, or null when the
|
|
7890
|
+
// command is short enough to live inline in the header (nothing clipped).
|
|
7891
|
+
function executeCommandBlockFor(group, width) {
|
|
7892
|
+
if (group.kind !== "execute")
|
|
7893
|
+
return null;
|
|
7894
|
+
if (shouldInlineExecuteCommand(group, executeInlineBudget(group, width)))
|
|
7895
|
+
return null;
|
|
7896
|
+
const block = executeCommandBlock(group, EXECUTE_COMMAND_BLOCK_MAX_LINES);
|
|
7897
|
+
return block.lines.length > 0 ? block : null;
|
|
7898
|
+
}
|
|
7611
7899
|
function traceGroupStatus(group) {
|
|
7612
7900
|
if (group.hasError) {
|
|
7613
7901
|
const count = group.errorCount || 1;
|
|
@@ -7634,8 +7922,15 @@ function traceGroupHeaderStyledText(group, width = 80) {
|
|
|
7634
7922
|
const chunks = [
|
|
7635
7923
|
fg(titleColor)(bold(group.title)),
|
|
7636
7924
|
];
|
|
7637
|
-
if (group.
|
|
7638
|
-
chunks.push(fg(theme.toolText)(` ${truncate(group.
|
|
7925
|
+
if (group.kind === "execute" && group.description) {
|
|
7926
|
+
chunks.push(fg(theme.toolText)(` ${truncate(group.description, commandWidth)}`));
|
|
7927
|
+
}
|
|
7928
|
+
else if (group.command) {
|
|
7929
|
+
// Execute commands only render inline when they fit whole; longer ones
|
|
7930
|
+
// move to the wrapped command block below instead of being clipped here.
|
|
7931
|
+
if (group.kind !== "execute" || shouldInlineExecuteCommand(group, commandWidth)) {
|
|
7932
|
+
chunks.push(fg(theme.toolText)(` ${truncate(group.command, commandWidth)}`));
|
|
7933
|
+
}
|
|
7639
7934
|
}
|
|
7640
7935
|
else if (group.count !== undefined && group.noun) {
|
|
7641
7936
|
chunks.push(fg(theme.textMuted)(` ${group.count} ${group.noun}`));
|
|
@@ -7646,6 +7941,8 @@ function traceGroupHeaderStyledText(group, width = 80) {
|
|
|
7646
7941
|
return new StyledText(chunks);
|
|
7647
7942
|
}
|
|
7648
7943
|
function traceGroupCompactLabel(group) {
|
|
7944
|
+
if (group.description)
|
|
7945
|
+
return `${group.title} ${group.description}`;
|
|
7649
7946
|
if (group.command)
|
|
7650
7947
|
return `${group.title} ${group.command}`;
|
|
7651
7948
|
if (group.count !== undefined && group.noun)
|
|
@@ -7675,6 +7972,8 @@ function traceGroupRenderableSignature(group) {
|
|
|
7675
7972
|
group.count ?? "",
|
|
7676
7973
|
group.noun ?? "",
|
|
7677
7974
|
group.command ?? "",
|
|
7975
|
+
group.description ?? "",
|
|
7976
|
+
hashString(stableStringify(group.commandLines ?? [])),
|
|
7678
7977
|
group.omitted,
|
|
7679
7978
|
hashString(stableStringify(group.items)),
|
|
7680
7979
|
hashString(stableStringify(group.previewLines)),
|
|
@@ -8431,10 +8730,15 @@ function renderTraceGroup(group, syntaxStyle, width = 80) {
|
|
|
8431
8730
|
const status = traceGroupStatus(group);
|
|
8432
8731
|
const detailColor = traceGroupDetailColor(group);
|
|
8433
8732
|
const detailWidth = Math.max(20, width - 10);
|
|
8733
|
+
const commandBlock = executeCommandBlockFor(group, width);
|
|
8434
8734
|
return h("box", { paddingLeft: 3, marginTop: 1, flexDirection: "column", flexShrink: 0 }, h("text", {
|
|
8435
8735
|
content: traceGroupHeaderStyledText(group, width),
|
|
8436
8736
|
wrapMode: "none",
|
|
8437
|
-
}),
|
|
8737
|
+
}), commandBlock
|
|
8738
|
+
? 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
|
|
8739
|
+
? h("text", { fg: theme.textMuted, wrapMode: "word" }, `... +${commandBlock.omitted} lines, Ctrl+O to view`)
|
|
8740
|
+
: null)
|
|
8741
|
+
: null, detailLines.length > 0
|
|
8438
8742
|
? h("box", { paddingLeft: 2, flexDirection: "column", flexShrink: 0 }, detailLines.map((line, index) => h("text", {
|
|
8439
8743
|
fg: detailColor,
|
|
8440
8744
|
wrapMode: "word",
|
|
@@ -8523,13 +8827,7 @@ function renderFooter(input) {
|
|
|
8523
8827
|
idleContent: `${shortCwd(input.cwd)} idle`,
|
|
8524
8828
|
idleFg: theme.textMuted,
|
|
8525
8829
|
runningFg: theme.primary,
|
|
8526
|
-
}), h("
|
|
8527
|
-
fg: permissionModeColor(input.mode()),
|
|
8528
|
-
ref: input.registerModeBadge,
|
|
8529
|
-
}, footerPermissionModeText(input.mode())), h("text", {
|
|
8530
|
-
fg: input.traceVerbose?.() ? theme.warning : theme.textMuted,
|
|
8531
|
-
ref: input.registerTraceBadge,
|
|
8532
|
-
}, footerTraceModeText(input.traceVerbose?.() === true)), h("box", { flexGrow: 1 }));
|
|
8830
|
+
}), h("box", { flexGrow: 1 }));
|
|
8533
8831
|
}
|
|
8534
8832
|
function pickerTitle(kind, providerId) {
|
|
8535
8833
|
switch (kind) {
|
|
@@ -8799,9 +9097,22 @@ function reconstructDisplayMessages(agentMessages) {
|
|
|
8799
9097
|
: "pending",
|
|
8800
9098
|
});
|
|
8801
9099
|
}
|
|
9100
|
+
// The aborted-assistant interruption note is model-facing bookkeeping —
|
|
9101
|
+
// strip it so it never renders as something the assistant "said".
|
|
9102
|
+
const interrupted = message.error?.aborted === true;
|
|
9103
|
+
let content = message.content;
|
|
9104
|
+
if (interrupted) {
|
|
9105
|
+
content = content === INTERRUPTED_ASSISTANT_CONTENT
|
|
9106
|
+
? ""
|
|
9107
|
+
: content.endsWith(`\n\n${INTERRUPTED_ASSISTANT_CONTENT}`)
|
|
9108
|
+
? content.slice(0, -`\n\n${INTERRUPTED_ASSISTANT_CONTENT}`.length)
|
|
9109
|
+
: content;
|
|
9110
|
+
if (!content && !message.reasoning && toolCalls.length === 0)
|
|
9111
|
+
continue;
|
|
9112
|
+
}
|
|
8802
9113
|
result.push({
|
|
8803
9114
|
role: "assistant",
|
|
8804
|
-
content
|
|
9115
|
+
content,
|
|
8805
9116
|
reasoning: message.reasoning || undefined,
|
|
8806
9117
|
toolCalls: toolCalls.length ? toolCalls : undefined,
|
|
8807
9118
|
});
|
|
@@ -8977,6 +9288,15 @@ function appendTraceGroupTranscript(chunks, group) {
|
|
|
8977
9288
|
appendLine("");
|
|
8978
9289
|
if (group.pending)
|
|
8979
9290
|
return;
|
|
9291
|
+
// Verbose mode shows the full command with its original line structure
|
|
9292
|
+
// whenever the header line alone doesn't already carry it verbatim.
|
|
9293
|
+
const commandLines = group.commandLines ?? [];
|
|
9294
|
+
if (group.kind === "execute" && (group.description || commandLines.length > 1)) {
|
|
9295
|
+
for (const [index, line] of commandLines.entries()) {
|
|
9296
|
+
append(" ", theme.borderSubtle);
|
|
9297
|
+
appendLine(`${index === 0 ? "$ " : " "}${line}`, theme.toolText);
|
|
9298
|
+
}
|
|
9299
|
+
}
|
|
8980
9300
|
const detailLines = traceGroupDetailLines(group);
|
|
8981
9301
|
const detailColor = traceGroupDetailColor(group);
|
|
8982
9302
|
for (const [index, line] of detailLines.entries()) {
|
|
@@ -9089,6 +9409,17 @@ function getApprovalPanelMeta(request) {
|
|
|
9089
9409
|
path: request.path,
|
|
9090
9410
|
};
|
|
9091
9411
|
}
|
|
9412
|
+
if (request.type === "agent_profile") {
|
|
9413
|
+
return {
|
|
9414
|
+
icon: "@",
|
|
9415
|
+
title: `Trust project agent profile "${request.name}"`,
|
|
9416
|
+
subtitle: "from .bubble/agents — its prompt will drive a subagent",
|
|
9417
|
+
preview: `${shortCwd(request.path)}\n${request.promptPreview}`,
|
|
9418
|
+
previewHeight: 8,
|
|
9419
|
+
previewColor: theme.toolText,
|
|
9420
|
+
path: request.path,
|
|
9421
|
+
};
|
|
9422
|
+
}
|
|
9092
9423
|
const path = shortCwd(request.path);
|
|
9093
9424
|
if (request.type === "edit") {
|
|
9094
9425
|
return {
|
|
@@ -9162,17 +9493,6 @@ function permissionModeBadgeLabel(mode) {
|
|
|
9162
9493
|
case "bypassPermissions": return "Bypass";
|
|
9163
9494
|
}
|
|
9164
9495
|
}
|
|
9165
|
-
function footerPermissionModeText(mode) {
|
|
9166
|
-
const info = PERMISSION_MODE_INFO[mode];
|
|
9167
|
-
if (mode === "default")
|
|
9168
|
-
return " mode: build · shift+tab plan";
|
|
9169
|
-
if (mode === "plan")
|
|
9170
|
-
return " mode: plan · shift+tab bypass";
|
|
9171
|
-
return ` mode: ${info.shortTitle} · shift+tab build`;
|
|
9172
|
-
}
|
|
9173
|
-
function footerTraceModeText(verbose) {
|
|
9174
|
-
return verbose ? " trace: verbose · ctrl+o compact" : " trace: compact · ctrl+o verbose";
|
|
9175
|
-
}
|
|
9176
9496
|
function permissionModeColor(mode) {
|
|
9177
9497
|
const info = PERMISSION_MODE_INFO[mode];
|
|
9178
9498
|
switch (info.color) {
|
|
@@ -9214,6 +9534,8 @@ function displayToolName(name) {
|
|
|
9214
9534
|
wait_agent: "WaitAgent",
|
|
9215
9535
|
send_input: "SendInput",
|
|
9216
9536
|
close_agent: "CloseAgent",
|
|
9537
|
+
list_agents: "ListAgents",
|
|
9538
|
+
agent_team: "AgentTeam",
|
|
9217
9539
|
task: "Task",
|
|
9218
9540
|
todo: "Todo",
|
|
9219
9541
|
question: "Questions",
|
|
@@ -9236,6 +9558,12 @@ function toolHeader(tool) {
|
|
|
9236
9558
|
const agentId = args.agent_id ?? (Array.isArray(args.agent_ids) ? `${args.agent_ids.length} agents` : undefined);
|
|
9237
9559
|
return agentId ? `(${truncate(String(agentId), 64)})` : "";
|
|
9238
9560
|
}
|
|
9561
|
+
if (tool.name === "agent_team") {
|
|
9562
|
+
const items = Array.isArray(args.items) ? `${args.items.length} items` : "";
|
|
9563
|
+
const description = typeof args.description === "string" ? args.description : "";
|
|
9564
|
+
const label = [description, items].filter(Boolean).join(", ");
|
|
9565
|
+
return label ? `(${truncate(label, 64)})` : "";
|
|
9566
|
+
}
|
|
9239
9567
|
const value = args.path ?? args.command ?? args.pattern ?? args.url ?? args.query ?? toolPath(tool);
|
|
9240
9568
|
return value ? `(${truncate(String(value).replace(/\n/g, " "), 64)})` : "";
|
|
9241
9569
|
}
|