@bubblebrain-ai/bubble 0.0.1 → 0.0.2
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.d.ts +8 -1
- package/dist/agent.js +45 -6
- package/dist/approval/controller.d.ts +3 -3
- package/dist/approval/controller.js +5 -5
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +2 -3
- package/dist/main.js +1 -3
- package/dist/permission/mode.d.ts +3 -7
- package/dist/permission/mode.js +7 -11
- package/dist/permissions/settings.js +3 -4
- package/dist/prompt/reminders.d.ts +1 -0
- package/dist/prompt/reminders.js +10 -16
- package/dist/provider-openai-codex.js +2 -0
- package/dist/provider.js +6 -2
- package/dist/slash-commands/commands.js +2 -23
- package/dist/slash-commands/types.d.ts +1 -1
- package/dist/tools/bash.js +30 -3
- package/dist/tui/clipboard.d.ts +1 -0
- package/dist/tui/clipboard.js +53 -0
- package/dist/tui/global-key-router.d.ts +3 -0
- package/dist/tui/global-key-router.js +87 -0
- package/dist/tui/prompt-keybindings.d.ts +1 -0
- package/dist/tui/prompt-keybindings.js +7 -0
- package/dist/tui/run.d.ts +1 -2
- package/dist/tui/run.js +679 -194
- package/dist/types.d.ts +5 -5
- package/package.json +2 -2
package/dist/tui/run.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BoxRenderable, CodeRenderable, createCliRenderer, DiffRenderable, getTreeSitterClient, MarkdownRenderable, LineNumberRenderable, StyledText, RGBA, fg, bg, bold, dim, TextAttributes, TextRenderable, } from "@opentui/core";
|
|
2
|
-
import { createComponent, createElement, insert, render, spread, useKeyboard, useTerminalDimensions, } from "@opentui/solid";
|
|
2
|
+
import { createComponent, createElement, insert, render, spread, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions, } from "@opentui/solid";
|
|
3
3
|
import { createEffect, createSignal, onCleanup, onMount } from "solid-js";
|
|
4
|
+
import { AgentAbortError } from "../agent.js";
|
|
4
5
|
import { BUILTIN_PROVIDERS, decodeModel, displayModel, isUserVisibleProvider } from "../provider-registry.js";
|
|
5
6
|
import { listBuiltinModels } from "../model-catalog.js";
|
|
6
7
|
import { calculateUsageCost } from "../model-pricing.js";
|
|
@@ -12,14 +13,17 @@ import { sidebarMcpRowsFromStates, renderMcpRowMarker } from "./sidebar-mcp.js";
|
|
|
12
13
|
import { expandAtMentions, filterFileSuggestions, findAtContext, listProjectFiles } from "./file-mentions.js";
|
|
13
14
|
import { compactDisplayMessages } from "./display-history.js";
|
|
14
15
|
import { createMarkdownSyntaxStyle, createSubtleMarkdownSyntaxStyle } from "./markdown-theme.js";
|
|
15
|
-
import { getNextPermissionMode } from "../permission/mode.js";
|
|
16
|
+
import { getNextPermissionMode, PERMISSION_MODE_INFO } from "../permission/mode.js";
|
|
16
17
|
import { getContextBudget } from "../context/budget.js";
|
|
17
18
|
import { getLspService } from "../lsp/index.js";
|
|
18
19
|
import { inferBashPrefix } from "../approval/session-cache.js";
|
|
19
20
|
import { createFrames } from "./opencode-spinner.js";
|
|
21
|
+
import { copyTextToClipboard } from "./clipboard.js";
|
|
20
22
|
import { readGitSidebarState } from "./sidebar-state.js";
|
|
21
23
|
import { isModeCycleKeyEvent, isModeCycleSequence, isModifiedEnterSequence, PROMPT_TEXTAREA_KEYBINDINGS, } from "./prompt-keybindings.js";
|
|
24
|
+
import { keyNameFromEvent, keyNameFromSequence } from "./global-key-router.js";
|
|
22
25
|
const treeSitterClient = getTreeSitterClient();
|
|
26
|
+
const PROMPT_HISTORY_LIMIT = 100;
|
|
23
27
|
const PROVIDER_PRIORITY = new Map([
|
|
24
28
|
["openai", 0],
|
|
25
29
|
["deepseek", 1],
|
|
@@ -104,7 +108,7 @@ const HOME_LOGO = [
|
|
|
104
108
|
];
|
|
105
109
|
const HOME_TIPS = [
|
|
106
110
|
"Type @ followed by a filename to attach file context",
|
|
107
|
-
"Press Tab to cycle Build and
|
|
111
|
+
"Press Shift+Tab to cycle Build, Plan, and Bypass modes",
|
|
108
112
|
"Type / or press Ctrl+P to open commands",
|
|
109
113
|
"Use /compact to summarize long sessions near context limits",
|
|
110
114
|
"Shift+Enter or Ctrl+J inserts a newline in your prompt",
|
|
@@ -158,8 +162,8 @@ export async function runTui(agent, args, options = {}) {
|
|
|
158
162
|
let renderer;
|
|
159
163
|
let syntaxStyle;
|
|
160
164
|
let subtleSyntaxStyle;
|
|
161
|
-
let
|
|
162
|
-
let
|
|
165
|
+
let rawGlobalKeyHandler;
|
|
166
|
+
let rawMouseSelectionHandler;
|
|
163
167
|
const exit = () => {
|
|
164
168
|
try {
|
|
165
169
|
renderer?.destroy();
|
|
@@ -181,20 +185,29 @@ export async function runTui(agent, args, options = {}) {
|
|
|
181
185
|
exitOnCtrlC: false,
|
|
182
186
|
useKittyKeyboard: {},
|
|
183
187
|
prependInputHandlers: [
|
|
184
|
-
(sequence) =>
|
|
188
|
+
(sequence) => rawGlobalKeyHandler?.(sequence) || false,
|
|
185
189
|
],
|
|
186
190
|
autoFocus: true,
|
|
187
191
|
useMouse: true,
|
|
188
192
|
openConsoleOnError: false,
|
|
189
193
|
backgroundColor: theme.background,
|
|
190
194
|
});
|
|
191
|
-
const
|
|
192
|
-
|
|
195
|
+
const setRawGlobalKeyHandler = (handler) => {
|
|
196
|
+
rawGlobalKeyHandler = handler;
|
|
193
197
|
};
|
|
194
|
-
const
|
|
195
|
-
|
|
198
|
+
const setRawMouseSelectionHandler = (handler) => {
|
|
199
|
+
rawMouseSelectionHandler = handler;
|
|
196
200
|
};
|
|
197
|
-
|
|
201
|
+
const processSingleMouseEvent = renderer.processSingleMouseEvent;
|
|
202
|
+
if (typeof processSingleMouseEvent === "function") {
|
|
203
|
+
renderer.processSingleMouseEvent = (event) => {
|
|
204
|
+
const handled = processSingleMouseEvent.call(renderer, event);
|
|
205
|
+
if (handled)
|
|
206
|
+
rawMouseSelectionHandler?.(event);
|
|
207
|
+
return handled;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
await render(() => h(OpenTuiApp, { agent, args, options, onExit: exit, syntaxStyle, subtleSyntaxStyle, setRawGlobalKeyHandler, setRawMouseSelectionHandler }), renderer);
|
|
198
211
|
}
|
|
199
212
|
catch (error) {
|
|
200
213
|
syntaxStyle?.destroy();
|
|
@@ -223,6 +236,7 @@ function isColorValue(value) {
|
|
|
223
236
|
|| value === "none";
|
|
224
237
|
}
|
|
225
238
|
function OpenTuiApp(props) {
|
|
239
|
+
const renderer = useRenderer();
|
|
226
240
|
const dimensions = useTerminalDimensions();
|
|
227
241
|
const registry = props.options.registry;
|
|
228
242
|
const skills = props.options.skillRegistry;
|
|
@@ -245,13 +259,24 @@ function OpenTuiApp(props) {
|
|
|
245
259
|
const homeTip = HOME_TIPS[Math.floor(Math.random() * HOME_TIPS.length)] ?? HOME_TIPS[0];
|
|
246
260
|
const homePrompt = HOME_PROMPTS[Math.floor(Math.random() * HOME_PROMPTS.length)] ?? HOME_PROMPTS[0];
|
|
247
261
|
let promptText = "";
|
|
262
|
+
let promptHistory = displayMessages
|
|
263
|
+
.filter((message) => message.role === "user" && message.content !== "(multimedia)")
|
|
264
|
+
.map((message) => message.content)
|
|
265
|
+
.slice(-PROMPT_HISTORY_LIMIT);
|
|
266
|
+
let promptHistoryIndex;
|
|
267
|
+
let promptHistoryDraft = "";
|
|
248
268
|
const [isRunning, setIsRunning] = createSignal(false);
|
|
269
|
+
let activeRun;
|
|
270
|
+
let nextRunId = 0;
|
|
249
271
|
const [showThinking, setShowThinking] = createSignal(true);
|
|
250
272
|
let streamingDisplay;
|
|
251
273
|
let sidebarLspSyncTimer;
|
|
252
274
|
const [todos, setTodos] = createSignal(props.agent.getTodos());
|
|
253
275
|
const [mode, setMode] = createSignal(props.agent.mode);
|
|
254
276
|
const [notice, setNotice] = createSignal("");
|
|
277
|
+
let copyToastClearTimer;
|
|
278
|
+
let copyToastRoot;
|
|
279
|
+
let copyToastText;
|
|
255
280
|
const [sessionActive, setSessionActive] = createSignal(false);
|
|
256
281
|
const [sidebarTick, setSidebarTick] = createSignal(0);
|
|
257
282
|
// Sidebar MCP section collapsed state. Persisted across sidebarTick bumps,
|
|
@@ -260,6 +285,7 @@ function OpenTuiApp(props) {
|
|
|
260
285
|
const lspService = props.options.lspService ?? getLspService(props.args.cwd, props.options.settingsManager?.getMerged().lsp);
|
|
261
286
|
const [lspStatuses, setLspStatuses] = createSignal(lspService.status());
|
|
262
287
|
const [sidebarUsage, setSidebarUsage] = createSignal({
|
|
288
|
+
contextTokens: 0,
|
|
263
289
|
promptTokens: 0,
|
|
264
290
|
completionTokens: 0,
|
|
265
291
|
promptCacheHitTokens: 0,
|
|
@@ -282,6 +308,8 @@ function OpenTuiApp(props) {
|
|
|
282
308
|
let homePromptRef;
|
|
283
309
|
let sessionPromptRef;
|
|
284
310
|
let scrollbox;
|
|
311
|
+
let transcriptScrollFollowing = true;
|
|
312
|
+
let transcriptScrollInitialized = false;
|
|
285
313
|
let rootBox;
|
|
286
314
|
let sidebarShell;
|
|
287
315
|
let transcriptHost;
|
|
@@ -367,15 +395,211 @@ function OpenTuiApp(props) {
|
|
|
367
395
|
const activePrompt = () => isHomeSurfaceActive()
|
|
368
396
|
? homePromptRef ?? sessionPromptRef
|
|
369
397
|
: sessionPromptRef ?? homePromptRef;
|
|
398
|
+
function setPromptText(value) {
|
|
399
|
+
promptText = value;
|
|
400
|
+
const prompt = activePrompt();
|
|
401
|
+
if (!prompt)
|
|
402
|
+
return;
|
|
403
|
+
prompt.setText(value);
|
|
404
|
+
prompt.cursorOffset = value.length;
|
|
405
|
+
prompt.focus();
|
|
406
|
+
}
|
|
407
|
+
function resetPromptHistoryBrowse() {
|
|
408
|
+
promptHistoryIndex = undefined;
|
|
409
|
+
promptHistoryDraft = "";
|
|
410
|
+
}
|
|
411
|
+
function rememberPromptHistory(input) {
|
|
412
|
+
const value = input.trimEnd();
|
|
413
|
+
if (!value.trim())
|
|
414
|
+
return;
|
|
415
|
+
promptHistory.push(value);
|
|
416
|
+
if (promptHistory.length > PROMPT_HISTORY_LIMIT) {
|
|
417
|
+
promptHistory = promptHistory.slice(-PROMPT_HISTORY_LIMIT);
|
|
418
|
+
}
|
|
419
|
+
resetPromptHistoryBrowse();
|
|
420
|
+
}
|
|
421
|
+
function canBrowsePromptHistory(direction) {
|
|
422
|
+
if (promptHistoryIndex !== undefined)
|
|
423
|
+
return true;
|
|
424
|
+
const prompt = activePrompt();
|
|
425
|
+
const text = prompt?.plainText ?? promptText;
|
|
426
|
+
if (!text)
|
|
427
|
+
return true;
|
|
428
|
+
const cursor = prompt?.logicalCursor;
|
|
429
|
+
if (!cursor)
|
|
430
|
+
return true;
|
|
431
|
+
if (direction === "up")
|
|
432
|
+
return cursor.row === 0;
|
|
433
|
+
return cursor.row >= text.split("\n").length - 1;
|
|
434
|
+
}
|
|
435
|
+
function browsePromptHistory(direction) {
|
|
436
|
+
if (!promptHistory.length)
|
|
437
|
+
return false;
|
|
438
|
+
if (!canBrowsePromptHistory(direction))
|
|
439
|
+
return false;
|
|
440
|
+
if (direction === "up") {
|
|
441
|
+
if (promptHistoryIndex === undefined) {
|
|
442
|
+
promptHistoryDraft = readPromptText() || promptText;
|
|
443
|
+
promptHistoryIndex = promptHistory.length - 1;
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
promptHistoryIndex = Math.max(0, promptHistoryIndex - 1);
|
|
447
|
+
}
|
|
448
|
+
setPromptText(promptHistory[promptHistoryIndex] ?? "");
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
if (promptHistoryIndex === undefined)
|
|
452
|
+
return false;
|
|
453
|
+
if (promptHistoryIndex < promptHistory.length - 1) {
|
|
454
|
+
promptHistoryIndex += 1;
|
|
455
|
+
setPromptText(promptHistory[promptHistoryIndex] ?? "");
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
setPromptText(promptHistoryDraft);
|
|
459
|
+
resetPromptHistoryBrowse();
|
|
460
|
+
}
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
function handlePromptHistoryKey(event) {
|
|
464
|
+
if (event.shift || event.ctrl || event.meta || event.super || event.hyper)
|
|
465
|
+
return false;
|
|
466
|
+
const name = keyNameFromEvent(event);
|
|
467
|
+
if (name !== "up" && name !== "down")
|
|
468
|
+
return false;
|
|
469
|
+
if (!browsePromptHistory(name))
|
|
470
|
+
return false;
|
|
471
|
+
event.preventDefault?.();
|
|
472
|
+
event.stopPropagation?.();
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
function blurInputsForModal() {
|
|
476
|
+
homePromptRef?.blur();
|
|
477
|
+
sessionPromptRef?.blur();
|
|
478
|
+
questionCustomInput?.blur();
|
|
479
|
+
providerDialogInput?.blur();
|
|
480
|
+
}
|
|
481
|
+
function focusApprovalPanel() {
|
|
482
|
+
setTimeout(() => {
|
|
483
|
+
if (pendingApproval() || pendingPlan())
|
|
484
|
+
approvalRoot?.focus();
|
|
485
|
+
}, 0);
|
|
486
|
+
}
|
|
487
|
+
function focusQuestionPanel() {
|
|
488
|
+
setTimeout(() => {
|
|
489
|
+
const state = pendingQuestion();
|
|
490
|
+
if (!state || state.editing)
|
|
491
|
+
return;
|
|
492
|
+
questionRoot?.focus();
|
|
493
|
+
}, 0);
|
|
494
|
+
}
|
|
495
|
+
function restorePromptAfterModal() {
|
|
496
|
+
setTimeout(() => {
|
|
497
|
+
if (!activeModalKeyOwner())
|
|
498
|
+
activePrompt()?.focus();
|
|
499
|
+
}, 0);
|
|
500
|
+
}
|
|
370
501
|
const activeComposerShell = () => isHomeSurfaceActive()
|
|
371
502
|
? homeComposerShell ?? sessionComposerShell
|
|
372
503
|
: sessionComposerShell ?? homeComposerShell;
|
|
373
504
|
onCleanup(() => {
|
|
374
505
|
uiDisposed = true;
|
|
506
|
+
if (copyToastClearTimer)
|
|
507
|
+
clearTimeout(copyToastClearTimer);
|
|
375
508
|
promptModeLabels.clear();
|
|
376
509
|
promptModelLabels.clear();
|
|
377
510
|
footerModeBadge = undefined;
|
|
378
511
|
});
|
|
512
|
+
function showCopyToast(toast, ttl = 2200) {
|
|
513
|
+
if (copyToastClearTimer)
|
|
514
|
+
clearTimeout(copyToastClearTimer);
|
|
515
|
+
const sidebarOffset = sidebarVisible() ? SESSION_SIDEBAR_WIDTH : 0;
|
|
516
|
+
const mainAreaWidth = Math.max(20, dimensions().width - sidebarOffset - 4);
|
|
517
|
+
const color = toast.variant === "success"
|
|
518
|
+
? theme.success
|
|
519
|
+
: toast.variant === "error"
|
|
520
|
+
? theme.error
|
|
521
|
+
: toast.variant === "warning"
|
|
522
|
+
? theme.warning
|
|
523
|
+
: theme.info;
|
|
524
|
+
const width = Math.max(24, Math.min(60, Math.min(mainAreaWidth, toast.message.length + 6)));
|
|
525
|
+
if (copyToastRoot) {
|
|
526
|
+
copyToastRoot.visible = true;
|
|
527
|
+
copyToastRoot.width = width;
|
|
528
|
+
copyToastRoot.right = sidebarOffset + 2;
|
|
529
|
+
copyToastRoot.borderColor = color;
|
|
530
|
+
}
|
|
531
|
+
if (copyToastText) {
|
|
532
|
+
copyToastText.fg = theme.text;
|
|
533
|
+
safeSetText(copyToastText, toast.message);
|
|
534
|
+
}
|
|
535
|
+
renderer.requestRender();
|
|
536
|
+
copyToastClearTimer = setTimeout(() => {
|
|
537
|
+
if (copyToastRoot)
|
|
538
|
+
copyToastRoot.visible = false;
|
|
539
|
+
renderer.requestRender();
|
|
540
|
+
copyToastClearTimer = undefined;
|
|
541
|
+
}, ttl);
|
|
542
|
+
}
|
|
543
|
+
async function copySelectionText(text) {
|
|
544
|
+
const now = Date.now();
|
|
545
|
+
if (!text.trim())
|
|
546
|
+
return;
|
|
547
|
+
if (text === lastCopiedSelection && now - lastCopiedSelectionAt < 350)
|
|
548
|
+
return;
|
|
549
|
+
const serial = ++selectionCopySerial;
|
|
550
|
+
let copied = false;
|
|
551
|
+
try {
|
|
552
|
+
await copyTextToClipboard(text);
|
|
553
|
+
copied = true;
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
try {
|
|
557
|
+
copied = renderer.copyToClipboardOSC52(text);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
copied = false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (serial !== selectionCopySerial)
|
|
564
|
+
return;
|
|
565
|
+
if (copied) {
|
|
566
|
+
lastCopiedSelection = text;
|
|
567
|
+
lastCopiedSelectionAt = Date.now();
|
|
568
|
+
showCopyToast({ message: "Copied to clipboard", variant: "info" });
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
showCopyToast({ message: "Failed to copy selection", variant: "error" }, 3000);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
function isInsideRenderable(renderable, container) {
|
|
575
|
+
if (!container)
|
|
576
|
+
return false;
|
|
577
|
+
let current = renderable;
|
|
578
|
+
while (current) {
|
|
579
|
+
if (current === container)
|
|
580
|
+
return true;
|
|
581
|
+
current = current.parent;
|
|
582
|
+
}
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
function getOpenTuiSelectionText(selection) {
|
|
586
|
+
const selectedRenderables = Array.isArray(selection?.selectedRenderables)
|
|
587
|
+
? [...selection.selectedRenderables]
|
|
588
|
+
: undefined;
|
|
589
|
+
if (!selectedRenderables?.length) {
|
|
590
|
+
return typeof selection?.getSelectedText === "function" ? selection.getSelectedText() : "";
|
|
591
|
+
}
|
|
592
|
+
return selectedRenderables
|
|
593
|
+
.filter((renderable) => !renderable.isDestroyed && !isInsideRenderable(renderable, sidebarShell))
|
|
594
|
+
.sort((a, b) => {
|
|
595
|
+
if (a.y !== b.y)
|
|
596
|
+
return a.y - b.y;
|
|
597
|
+
return a.x - b.x;
|
|
598
|
+
})
|
|
599
|
+
.map((renderable) => typeof renderable.getSelectedText === "function" ? renderable.getSelectedText() : "")
|
|
600
|
+
.filter(Boolean)
|
|
601
|
+
.join("\n");
|
|
602
|
+
}
|
|
379
603
|
const readPromptText = () => {
|
|
380
604
|
try {
|
|
381
605
|
return activePrompt()?.plainText ?? "";
|
|
@@ -422,10 +646,13 @@ function OpenTuiApp(props) {
|
|
|
422
646
|
setSidebarText(sidebarTokenText, `${formatCompactNumber(context.tokens)} tokens`);
|
|
423
647
|
setSidebarText(sidebarPercentText, `${context.percent}% used`);
|
|
424
648
|
if (sidebarGaugeText) {
|
|
425
|
-
sidebarGaugeText.content =
|
|
649
|
+
sidebarGaugeText.content = buildContextGauge(context.percent, 30);
|
|
650
|
+
sidebarGaugeText.requestRender();
|
|
426
651
|
}
|
|
427
652
|
if (sidebarGaugeLabelText) {
|
|
428
|
-
sidebarGaugeLabelText.content = buildGaugeLabel(context.percent);
|
|
653
|
+
sidebarGaugeLabelText.content = buildGaugeLabel(context.percent, context.remainingTokens);
|
|
654
|
+
sidebarGaugeLabelText.fg = context.percent >= 80 ? theme.error : context.percent >= 60 ? theme.warning : theme.success;
|
|
655
|
+
sidebarGaugeLabelText.requestRender();
|
|
429
656
|
}
|
|
430
657
|
setSidebarText(sidebarUsageText, context.turns > 0
|
|
431
658
|
? `${formatCompactNumber(context.promptTokens)} in · ${formatCompactNumber(context.completionTokens)} out`
|
|
@@ -517,7 +744,7 @@ function OpenTuiApp(props) {
|
|
|
517
744
|
}
|
|
518
745
|
const promptModeTitle = () => mode() === "plan" ? "Plan" : "Build";
|
|
519
746
|
const promptModeBadge = () => promptModeBadgeContent(mode());
|
|
520
|
-
const footerModeText = () =>
|
|
747
|
+
const footerModeText = () => footerPermissionModeText(mode());
|
|
521
748
|
function syncModeChrome() {
|
|
522
749
|
if (uiDisposed)
|
|
523
750
|
return;
|
|
@@ -525,8 +752,11 @@ function OpenTuiApp(props) {
|
|
|
525
752
|
if (!safeSetText(label, promptModeBadge()))
|
|
526
753
|
promptModeLabels.delete(label);
|
|
527
754
|
}
|
|
528
|
-
if (footerModeBadge
|
|
529
|
-
footerModeBadge =
|
|
755
|
+
if (footerModeBadge) {
|
|
756
|
+
footerModeBadge.fg = permissionModeColor(mode());
|
|
757
|
+
if (!safeSetText(footerModeBadge, footerModeText()))
|
|
758
|
+
footerModeBadge = undefined;
|
|
759
|
+
}
|
|
530
760
|
safeRequestRender(homeComposerShell);
|
|
531
761
|
safeRequestRender(sessionComposerShell);
|
|
532
762
|
safeRequestRender(rootBox);
|
|
@@ -567,10 +797,10 @@ function OpenTuiApp(props) {
|
|
|
567
797
|
const cycleMode = () => {
|
|
568
798
|
if (picker || pendingPlan())
|
|
569
799
|
return false;
|
|
570
|
-
const next = getNextPermissionMode(props.agent.mode
|
|
800
|
+
const next = getNextPermissionMode(props.agent.mode);
|
|
571
801
|
props.agent.setMode(next);
|
|
572
802
|
setMode(next);
|
|
573
|
-
setNotice(`Mode: ${next
|
|
803
|
+
setNotice(`Mode: ${permissionModeBadgeLabel(next)}`);
|
|
574
804
|
redrawDock();
|
|
575
805
|
syncPromptSurfaces();
|
|
576
806
|
syncModeChrome();
|
|
@@ -584,9 +814,6 @@ function OpenTuiApp(props) {
|
|
|
584
814
|
return true;
|
|
585
815
|
};
|
|
586
816
|
const cycleModeFromRawSequence = (sequence) => {
|
|
587
|
-
const rawKey = keyNameFromSequence(sequence);
|
|
588
|
-
if (rawKey && handleApprovalNavigation(rawKey))
|
|
589
|
-
return true;
|
|
590
817
|
if (!isModeCycleSequence(sequence))
|
|
591
818
|
return false;
|
|
592
819
|
return cycleMode();
|
|
@@ -599,40 +826,7 @@ function OpenTuiApp(props) {
|
|
|
599
826
|
? ["Allow once", "Allow always", "Reject"]
|
|
600
827
|
: ["Allow once", "Reject"];
|
|
601
828
|
};
|
|
602
|
-
const
|
|
603
|
-
if (!sequence)
|
|
604
|
-
return "";
|
|
605
|
-
if (sequence === "\x1b[D" || /^\x1b\[[0-9;]*D$/.test(sequence))
|
|
606
|
-
return "left";
|
|
607
|
-
if (sequence === "\x1b[C" || /^\x1b\[[0-9;]*C$/.test(sequence))
|
|
608
|
-
return "right";
|
|
609
|
-
if (sequence === "\x1b[A" || /^\x1b\[[0-9;]*A$/.test(sequence))
|
|
610
|
-
return "up";
|
|
611
|
-
if (sequence === "\x1b[B" || /^\x1b\[[0-9;]*B$/.test(sequence))
|
|
612
|
-
return "down";
|
|
613
|
-
if (sequence === "\r" || sequence === "\n")
|
|
614
|
-
return "enter";
|
|
615
|
-
if (sequence === "\x1b")
|
|
616
|
-
return "escape";
|
|
617
|
-
return "";
|
|
618
|
-
};
|
|
619
|
-
const keyNameFromEvent = (event) => {
|
|
620
|
-
const rawName = String(event.name || event.key || event.input || "").toLowerCase();
|
|
621
|
-
if (rawName === "arrowleft" || rawName === "left_arrow")
|
|
622
|
-
return "left";
|
|
623
|
-
if (rawName === "arrowright" || rawName === "right_arrow")
|
|
624
|
-
return "right";
|
|
625
|
-
if (rawName === "arrowup" || rawName === "up_arrow")
|
|
626
|
-
return "up";
|
|
627
|
-
if (rawName === "arrowdown" || rawName === "down_arrow")
|
|
628
|
-
return "down";
|
|
629
|
-
if (rawName === "return")
|
|
630
|
-
return "enter";
|
|
631
|
-
if (rawName === "esc")
|
|
632
|
-
return "escape";
|
|
633
|
-
return rawName || keyNameFromSequence(event.raw || event.sequence);
|
|
634
|
-
};
|
|
635
|
-
const questionKeyNameFromSequence = (sequence) => {
|
|
829
|
+
const modalKeyNameFromSequence = (sequence) => {
|
|
636
830
|
const name = keyNameFromSequence(sequence);
|
|
637
831
|
if (name)
|
|
638
832
|
return name;
|
|
@@ -652,12 +846,16 @@ function OpenTuiApp(props) {
|
|
|
652
846
|
const rejectPendingPlan = (plan) => {
|
|
653
847
|
setPendingPlan(undefined);
|
|
654
848
|
setApprovalOptionIdx(0);
|
|
849
|
+
forceApprovalUI();
|
|
850
|
+
restorePromptAfterModal();
|
|
655
851
|
plan.resolve({ action: "reject", reason: "Rejected by user." });
|
|
656
852
|
};
|
|
657
853
|
const resolvePendingPlanSelection = (plan) => {
|
|
658
854
|
const sel = approvalOptionIdx();
|
|
659
855
|
setPendingPlan(undefined);
|
|
660
856
|
setApprovalOptionIdx(0);
|
|
857
|
+
forceApprovalUI();
|
|
858
|
+
restorePromptAfterModal();
|
|
661
859
|
if (sel === 0) {
|
|
662
860
|
plan.resolve({ action: "approve", plan: plan.plan });
|
|
663
861
|
}
|
|
@@ -710,6 +908,7 @@ function OpenTuiApp(props) {
|
|
|
710
908
|
setPendingApproval(undefined);
|
|
711
909
|
setApprovalOptionIdx(0);
|
|
712
910
|
forceApprovalUI();
|
|
911
|
+
restorePromptAfterModal();
|
|
713
912
|
if (choice === "Allow once") {
|
|
714
913
|
approval.resolve({ action: "approve" });
|
|
715
914
|
return true;
|
|
@@ -722,7 +921,7 @@ function OpenTuiApp(props) {
|
|
|
722
921
|
approval.resolve({ action: "reject", feedback: "Rejected by user." });
|
|
723
922
|
return true;
|
|
724
923
|
};
|
|
725
|
-
const handleApprovalNavigation = (name, preventOnly = false) => {
|
|
924
|
+
const handleApprovalNavigation = (name, preventOnly = false, shift = false) => {
|
|
726
925
|
const approval = pendingApproval();
|
|
727
926
|
if (approval) {
|
|
728
927
|
const opts = approvalOptionsFor(approval.request);
|
|
@@ -734,6 +933,10 @@ function OpenTuiApp(props) {
|
|
|
734
933
|
moveApprovalOption(1, opts.length);
|
|
735
934
|
return true;
|
|
736
935
|
}
|
|
936
|
+
if (name === "tab") {
|
|
937
|
+
moveApprovalOption(shift ? -1 : 1, opts.length);
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
737
940
|
if (name === "enter") {
|
|
738
941
|
if (!preventOnly)
|
|
739
942
|
resolveApprovalSelection();
|
|
@@ -745,6 +948,7 @@ function OpenTuiApp(props) {
|
|
|
745
948
|
setPendingApproval(undefined);
|
|
746
949
|
setApprovalOptionIdx(0);
|
|
747
950
|
forceApprovalUI();
|
|
951
|
+
restorePromptAfterModal();
|
|
748
952
|
approval.resolve({ action: "reject", feedback: "Rejected by user." });
|
|
749
953
|
}
|
|
750
954
|
return true;
|
|
@@ -760,6 +964,10 @@ function OpenTuiApp(props) {
|
|
|
760
964
|
moveApprovalOption(1, PLAN_OPTIONS.length);
|
|
761
965
|
return true;
|
|
762
966
|
}
|
|
967
|
+
if (name === "tab") {
|
|
968
|
+
moveApprovalOption(shift ? -1 : 1, PLAN_OPTIONS.length);
|
|
969
|
+
return true;
|
|
970
|
+
}
|
|
763
971
|
if (name === "enter") {
|
|
764
972
|
if (!preventOnly)
|
|
765
973
|
resolvePendingPlanSelection(plan);
|
|
@@ -775,7 +983,7 @@ function OpenTuiApp(props) {
|
|
|
775
983
|
};
|
|
776
984
|
const handleApprovalKey = (event) => {
|
|
777
985
|
const name = keyNameFromEvent(event);
|
|
778
|
-
if (handleApprovalNavigation(name)) {
|
|
986
|
+
if (handleApprovalNavigation(name, false, !!event.shift)) {
|
|
779
987
|
event.preventDefault?.();
|
|
780
988
|
event.stopPropagation?.();
|
|
781
989
|
return true;
|
|
@@ -787,6 +995,8 @@ function OpenTuiApp(props) {
|
|
|
787
995
|
const plan = pendingPlan();
|
|
788
996
|
syncPromptSurfaces();
|
|
789
997
|
const prompt = activePrompt();
|
|
998
|
+
if (approval || plan)
|
|
999
|
+
blurInputsForModal();
|
|
790
1000
|
if (prompt) {
|
|
791
1001
|
if (approval) {
|
|
792
1002
|
const options = approvalOptionsFor(approval.request);
|
|
@@ -803,6 +1013,8 @@ function OpenTuiApp(props) {
|
|
|
803
1013
|
}
|
|
804
1014
|
redrawDock();
|
|
805
1015
|
redrawApprovalPanel();
|
|
1016
|
+
if (approval || plan)
|
|
1017
|
+
focusApprovalPanel();
|
|
806
1018
|
redrawTranscript();
|
|
807
1019
|
};
|
|
808
1020
|
function questionStateFromRequest(request) {
|
|
@@ -851,15 +1063,67 @@ function OpenTuiApp(props) {
|
|
|
851
1063
|
questionSyncTimers.add(timer);
|
|
852
1064
|
}
|
|
853
1065
|
}
|
|
1066
|
+
function transcriptMaxScrollTop() {
|
|
1067
|
+
if (!scrollbox)
|
|
1068
|
+
return 0;
|
|
1069
|
+
return Math.max(0, scrollbox.scrollHeight - scrollbox.viewport.height);
|
|
1070
|
+
}
|
|
1071
|
+
function isTranscriptAtBottom() {
|
|
1072
|
+
if (!scrollbox)
|
|
1073
|
+
return true;
|
|
1074
|
+
return scrollbox.scrollTop >= transcriptMaxScrollTop() - 1;
|
|
1075
|
+
}
|
|
1076
|
+
function updateTranscriptScrollFollowingFromPosition() {
|
|
1077
|
+
if (!scrollbox)
|
|
1078
|
+
return;
|
|
1079
|
+
transcriptScrollFollowing = isTranscriptAtBottom();
|
|
1080
|
+
transcriptScrollInitialized = true;
|
|
1081
|
+
}
|
|
1082
|
+
function shouldFollowTranscriptBeforeUpdate() {
|
|
1083
|
+
if (!scrollbox)
|
|
1084
|
+
return transcriptScrollFollowing;
|
|
1085
|
+
if (!transcriptScrollInitialized)
|
|
1086
|
+
return true;
|
|
1087
|
+
transcriptScrollFollowing = isTranscriptAtBottom();
|
|
1088
|
+
return transcriptScrollFollowing;
|
|
1089
|
+
}
|
|
1090
|
+
function scrollTranscriptToBottom() {
|
|
1091
|
+
if (!scrollbox)
|
|
1092
|
+
return;
|
|
1093
|
+
scrollbox.scrollTo(scrollbox.scrollHeight);
|
|
1094
|
+
transcriptScrollFollowing = true;
|
|
1095
|
+
transcriptScrollInitialized = true;
|
|
1096
|
+
}
|
|
1097
|
+
function scheduleTranscriptScrollAfterUpdate(shouldFollow, delay = 50) {
|
|
1098
|
+
setTimeout(() => {
|
|
1099
|
+
if (!scrollbox)
|
|
1100
|
+
return;
|
|
1101
|
+
if (shouldFollow && transcriptScrollFollowing) {
|
|
1102
|
+
scrollTranscriptToBottom();
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
updateTranscriptScrollFollowingFromPosition();
|
|
1106
|
+
}
|
|
1107
|
+
}, delay);
|
|
1108
|
+
}
|
|
1109
|
+
function handleTranscriptMouseScroll() {
|
|
1110
|
+
setTimeout(updateTranscriptScrollFollowingFromPosition, 0);
|
|
1111
|
+
}
|
|
854
1112
|
function syncQuestionUI(focusCustom = false) {
|
|
855
1113
|
redrawQuestionPanel();
|
|
856
1114
|
syncPromptSurfaces();
|
|
1115
|
+
const question = pendingQuestion();
|
|
1116
|
+
if (question)
|
|
1117
|
+
blurInputsForModal();
|
|
857
1118
|
redrawDock();
|
|
858
1119
|
rootBox?.requestRender();
|
|
859
1120
|
scrollbox?.requestRender();
|
|
860
1121
|
if (focusCustom) {
|
|
861
1122
|
setTimeout(() => questionCustomInput?.focus(), 0);
|
|
862
1123
|
}
|
|
1124
|
+
else if (question) {
|
|
1125
|
+
focusQuestionPanel();
|
|
1126
|
+
}
|
|
863
1127
|
}
|
|
864
1128
|
function updateQuestionState(updater, focusCustom = false) {
|
|
865
1129
|
setPendingQuestion((current) => current ? updater(current) : undefined);
|
|
@@ -1095,17 +1359,49 @@ function OpenTuiApp(props) {
|
|
|
1095
1359
|
}
|
|
1096
1360
|
return false;
|
|
1097
1361
|
}
|
|
1098
|
-
function
|
|
1099
|
-
|
|
1100
|
-
|
|
1362
|
+
function activeModalKeyOwner() {
|
|
1363
|
+
if (pendingApproval() || pendingPlan())
|
|
1364
|
+
return "approval";
|
|
1365
|
+
if (pendingQuestion())
|
|
1366
|
+
return "question";
|
|
1367
|
+
if (providerDialog)
|
|
1368
|
+
return "provider";
|
|
1369
|
+
if (picker)
|
|
1370
|
+
return "picker";
|
|
1371
|
+
return undefined;
|
|
1372
|
+
}
|
|
1373
|
+
function routeModalKey(event) {
|
|
1374
|
+
const owner = activeModalKeyOwner();
|
|
1375
|
+
if (!owner)
|
|
1101
1376
|
return false;
|
|
1102
|
-
|
|
1103
|
-
|
|
1377
|
+
switch (owner) {
|
|
1378
|
+
case "approval":
|
|
1379
|
+
return handleApprovalKey(event);
|
|
1380
|
+
case "question":
|
|
1381
|
+
return handleQuestionKey(event);
|
|
1382
|
+
case "provider":
|
|
1383
|
+
return handleProviderDialogKey(event);
|
|
1384
|
+
case "picker":
|
|
1385
|
+
return handlePickerKey(event);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
function shouldModalSwallowUnhandledKey(owner) {
|
|
1389
|
+
if (owner === "approval")
|
|
1390
|
+
return true;
|
|
1391
|
+
if (owner === "question") {
|
|
1392
|
+
const state = pendingQuestion();
|
|
1393
|
+
return !state?.editing || isQuestionConfirmTab(state);
|
|
1394
|
+
}
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
function routeModalRawSequence(sequence) {
|
|
1398
|
+
const owner = activeModalKeyOwner();
|
|
1399
|
+
if (!owner)
|
|
1104
1400
|
return false;
|
|
1105
|
-
|
|
1401
|
+
const name = modalKeyNameFromSequence(sequence);
|
|
1402
|
+
if (!name)
|
|
1106
1403
|
return false;
|
|
1107
|
-
|
|
1108
|
-
return handleQuestionKey({
|
|
1404
|
+
const handled = routeModalKey({
|
|
1109
1405
|
name,
|
|
1110
1406
|
key: name,
|
|
1111
1407
|
input: sequence,
|
|
@@ -1115,11 +1411,13 @@ function OpenTuiApp(props) {
|
|
|
1115
1411
|
preventDefault() { },
|
|
1116
1412
|
stopPropagation() { },
|
|
1117
1413
|
});
|
|
1414
|
+
return handled || shouldModalSwallowUnhandledKey(owner);
|
|
1118
1415
|
}
|
|
1119
1416
|
const installInteractiveHandlers = () => {
|
|
1120
1417
|
if (props.options.planHandlerRef) {
|
|
1121
1418
|
props.options.planHandlerRef.current = (plan) => new Promise((resolve) => {
|
|
1122
1419
|
setPendingPlan({ plan, resolve });
|
|
1420
|
+
blurInputsForModal();
|
|
1123
1421
|
forceApprovalUI();
|
|
1124
1422
|
});
|
|
1125
1423
|
}
|
|
@@ -1129,6 +1427,7 @@ function OpenTuiApp(props) {
|
|
|
1129
1427
|
picker = undefined;
|
|
1130
1428
|
providerDialog = undefined;
|
|
1131
1429
|
setPendingApproval({ request, resolve });
|
|
1430
|
+
blurInputsForModal();
|
|
1132
1431
|
forceApprovalUI();
|
|
1133
1432
|
});
|
|
1134
1433
|
}
|
|
@@ -1162,8 +1461,7 @@ function OpenTuiApp(props) {
|
|
|
1162
1461
|
onCleanup(unsubscribeQuestion);
|
|
1163
1462
|
syncFirstPendingQuestion();
|
|
1164
1463
|
}
|
|
1165
|
-
props.
|
|
1166
|
-
props.setRawQuestionHandler?.(handleQuestionRawSequence);
|
|
1464
|
+
props.setRawGlobalKeyHandler?.(routeGlobalRawSequence);
|
|
1167
1465
|
const unsubscribeLsp = lspService.onStatusChange(() => {
|
|
1168
1466
|
syncSidebarLsp();
|
|
1169
1467
|
});
|
|
@@ -1172,12 +1470,11 @@ function OpenTuiApp(props) {
|
|
|
1172
1470
|
refreshGitSidebar();
|
|
1173
1471
|
setTimeout(() => {
|
|
1174
1472
|
activePrompt()?.focus();
|
|
1175
|
-
|
|
1473
|
+
scrollTranscriptToBottom();
|
|
1176
1474
|
}, 25);
|
|
1177
1475
|
});
|
|
1178
1476
|
onCleanup(() => {
|
|
1179
|
-
props.
|
|
1180
|
-
props.setRawQuestionHandler?.(undefined);
|
|
1477
|
+
props.setRawGlobalKeyHandler?.(undefined);
|
|
1181
1478
|
if (sidebarLspSyncTimer)
|
|
1182
1479
|
clearInterval(sidebarLspSyncTimer);
|
|
1183
1480
|
for (const timer of questionSyncTimers)
|
|
@@ -1189,66 +1486,38 @@ function OpenTuiApp(props) {
|
|
|
1189
1486
|
if (props.options.approvalHandlerRef)
|
|
1190
1487
|
props.options.approvalHandlerRef.current = undefined;
|
|
1191
1488
|
});
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
// Ctrl+Shift+M opens the MCP reconnect picker. Shift is required because
|
|
1199
|
-
// bare Ctrl+M is Enter on most terminals (historical TTY mapping).
|
|
1200
|
-
if (event.ctrl && event.shift && name === "m") {
|
|
1201
|
-
openMcpReconnectPicker();
|
|
1202
|
-
event.preventDefault?.();
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
if (handleApprovalKey(event))
|
|
1206
|
-
return;
|
|
1207
|
-
if (handleQuestionKey(event))
|
|
1489
|
+
let lastCopiedSelection = "";
|
|
1490
|
+
let lastCopiedSelectionAt = 0;
|
|
1491
|
+
let selectionCopySerial = 0;
|
|
1492
|
+
let rawSelectionStart;
|
|
1493
|
+
useSelectionHandler((selection) => {
|
|
1494
|
+
if (selection.isDragging)
|
|
1208
1495
|
return;
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1496
|
+
const selectedText = getOpenTuiSelectionText(selection);
|
|
1497
|
+
void copySelectionText(selectedText);
|
|
1498
|
+
});
|
|
1499
|
+
function handleRawMouseSelection(event) {
|
|
1500
|
+
if (event.button !== 0)
|
|
1212
1501
|
return;
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
if (name === "left" || name === "right" || name === "h" || name === "l") {
|
|
1216
|
-
const opts = PLAN_OPTIONS;
|
|
1217
|
-
const idx = approvalOptionIdx();
|
|
1218
|
-
const next = name === "left" || name === "h"
|
|
1219
|
-
? (idx - 1 + opts.length) % opts.length
|
|
1220
|
-
: (idx + 1) % opts.length;
|
|
1221
|
-
setApprovalOptionIdx(next);
|
|
1222
|
-
forceApprovalUI();
|
|
1223
|
-
event.preventDefault?.();
|
|
1224
|
-
return;
|
|
1225
|
-
}
|
|
1226
|
-
if (name === "return" || name === "enter") {
|
|
1227
|
-
const sel = approvalOptionIdx();
|
|
1228
|
-
setPendingPlan(undefined);
|
|
1229
|
-
setApprovalOptionIdx(0);
|
|
1230
|
-
if (sel === 0) {
|
|
1231
|
-
plan.resolve({ action: "approve", plan: plan.plan });
|
|
1232
|
-
}
|
|
1233
|
-
else {
|
|
1234
|
-
plan.resolve({ action: "reject", reason: "Rejected by user." });
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
if (name === "escape") {
|
|
1238
|
-
setPendingPlan(undefined);
|
|
1239
|
-
setApprovalOptionIdx(0);
|
|
1240
|
-
plan.resolve({ action: "reject", reason: "Rejected by user." });
|
|
1241
|
-
}
|
|
1242
|
-
event.preventDefault?.();
|
|
1502
|
+
if (event.type === "down") {
|
|
1503
|
+
rawSelectionStart = { x: event.x, y: event.y };
|
|
1243
1504
|
return;
|
|
1244
1505
|
}
|
|
1245
|
-
if (
|
|
1506
|
+
if (event.type !== "up")
|
|
1246
1507
|
return;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1508
|
+
const start = rawSelectionStart;
|
|
1509
|
+
rawSelectionStart = undefined;
|
|
1510
|
+
if (!start || (start.x === event.x && start.y === event.y))
|
|
1250
1511
|
return;
|
|
1251
|
-
|
|
1512
|
+
const selection = renderer.getSelection();
|
|
1513
|
+
if (!selection || selection.isDragging)
|
|
1514
|
+
return;
|
|
1515
|
+
void copySelectionText(getOpenTuiSelectionText(selection));
|
|
1516
|
+
}
|
|
1517
|
+
props.setRawMouseSelectionHandler?.(handleRawMouseSelection);
|
|
1518
|
+
onCleanup(() => props.setRawMouseSelectionHandler?.(undefined));
|
|
1519
|
+
useKeyboard((event) => {
|
|
1520
|
+
routeGlobalKeyEvent(event);
|
|
1252
1521
|
}, {});
|
|
1253
1522
|
function currentTranscriptMessages(extra) {
|
|
1254
1523
|
return compactDisplayMessages(extra ? [...displayMessages, extra] : displayMessages);
|
|
@@ -1298,6 +1567,74 @@ function OpenTuiApp(props) {
|
|
|
1298
1567
|
// Keep the agent loop alive even if a renderable is already gone.
|
|
1299
1568
|
}
|
|
1300
1569
|
}
|
|
1570
|
+
function beginAgentRun() {
|
|
1571
|
+
const run = { id: ++nextRunId, abortController: new AbortController() };
|
|
1572
|
+
activeRun = run;
|
|
1573
|
+
setRunningState(true);
|
|
1574
|
+
return run;
|
|
1575
|
+
}
|
|
1576
|
+
function finishAgentRun(run) {
|
|
1577
|
+
if (activeRun?.id === run.id)
|
|
1578
|
+
activeRun = undefined;
|
|
1579
|
+
setRunningState(false);
|
|
1580
|
+
}
|
|
1581
|
+
function cancelActiveAgentRun() {
|
|
1582
|
+
if (!activeRun || activeRun.abortController.signal.aborted)
|
|
1583
|
+
return false;
|
|
1584
|
+
activeRun.abortController.abort(new AgentAbortError("Agent run cancelled by user."));
|
|
1585
|
+
setNotice("Agent run cancelled");
|
|
1586
|
+
redrawDock();
|
|
1587
|
+
return true;
|
|
1588
|
+
}
|
|
1589
|
+
function preventGlobalKey(event) {
|
|
1590
|
+
event.preventDefault?.();
|
|
1591
|
+
event.stopPropagation?.();
|
|
1592
|
+
}
|
|
1593
|
+
function routeRunningCancel(name, event) {
|
|
1594
|
+
if (name !== "escape")
|
|
1595
|
+
return false;
|
|
1596
|
+
if (!cancelActiveAgentRun())
|
|
1597
|
+
return false;
|
|
1598
|
+
if (event)
|
|
1599
|
+
preventGlobalKey(event);
|
|
1600
|
+
return true;
|
|
1601
|
+
}
|
|
1602
|
+
function routeGlobalRawSequence(sequence) {
|
|
1603
|
+
const name = keyNameFromSequence(sequence);
|
|
1604
|
+
if (routeRunningCancel(name))
|
|
1605
|
+
return true;
|
|
1606
|
+
if (routeModalRawSequence(sequence))
|
|
1607
|
+
return true;
|
|
1608
|
+
if (cycleModeFromRawSequence(sequence))
|
|
1609
|
+
return true;
|
|
1610
|
+
return false;
|
|
1611
|
+
}
|
|
1612
|
+
function routeGlobalKeyEvent(event) {
|
|
1613
|
+
const name = keyNameFromEvent(event);
|
|
1614
|
+
if (event.ctrl && name === "c") {
|
|
1615
|
+
void requestExit();
|
|
1616
|
+
return true;
|
|
1617
|
+
}
|
|
1618
|
+
if (routeRunningCancel(name, event))
|
|
1619
|
+
return true;
|
|
1620
|
+
// Ctrl+Shift+M opens the MCP reconnect picker. Shift is required because
|
|
1621
|
+
// bare Ctrl+M is Enter on most terminals (historical TTY mapping).
|
|
1622
|
+
if (event.ctrl && event.shift && name === "m") {
|
|
1623
|
+
openMcpReconnectPicker();
|
|
1624
|
+
event.preventDefault?.();
|
|
1625
|
+
return true;
|
|
1626
|
+
}
|
|
1627
|
+
if (routeModalKey(event))
|
|
1628
|
+
return true;
|
|
1629
|
+
if (cycleModeFromKey(event))
|
|
1630
|
+
return true;
|
|
1631
|
+
if (event.ctrl && name === "p" && !picker && !isRunning()) {
|
|
1632
|
+
openCommandPalette();
|
|
1633
|
+
event.preventDefault?.();
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
return false;
|
|
1637
|
+
}
|
|
1301
1638
|
function transcriptOptions() {
|
|
1302
1639
|
return {
|
|
1303
1640
|
cwd: props.args.cwd,
|
|
@@ -1371,28 +1708,22 @@ function OpenTuiApp(props) {
|
|
|
1371
1708
|
}, PROMPT_SCANNER_INTERVAL_MS);
|
|
1372
1709
|
}
|
|
1373
1710
|
function redrawTranscript(extra, baseMessages = displayMessages) {
|
|
1711
|
+
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
1374
1712
|
streamingDisplay = extra;
|
|
1375
1713
|
const nextMessages = compactDisplayMessages(extra ? [...baseMessages, extra] : baseMessages);
|
|
1376
1714
|
syncSessionMessages(nextMessages);
|
|
1377
1715
|
rootBox?.requestRender();
|
|
1378
1716
|
scrollbox?.requestRender();
|
|
1379
|
-
|
|
1380
|
-
if (!scrollbox)
|
|
1381
|
-
return;
|
|
1382
|
-
if (nextMessages.length <= 3) {
|
|
1383
|
-
scrollbox.scrollTo(0);
|
|
1384
|
-
return;
|
|
1385
|
-
}
|
|
1386
|
-
scrollbox.scrollTo(scrollbox.scrollHeight);
|
|
1387
|
-
}, 50);
|
|
1717
|
+
scheduleTranscriptScrollAfterUpdate(shouldFollow);
|
|
1388
1718
|
}
|
|
1389
1719
|
createEffect(() => {
|
|
1720
|
+
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
1390
1721
|
dimensions();
|
|
1391
1722
|
sessionActive();
|
|
1392
1723
|
syncSidebarChrome();
|
|
1393
1724
|
redrawQuestionPanel();
|
|
1394
1725
|
scrollbox?.requestRender();
|
|
1395
|
-
|
|
1726
|
+
scheduleTranscriptScrollAfterUpdate(shouldFollow);
|
|
1396
1727
|
});
|
|
1397
1728
|
function redrawDock() {
|
|
1398
1729
|
if (dock) {
|
|
@@ -1479,6 +1810,8 @@ function OpenTuiApp(props) {
|
|
|
1479
1810
|
return buildProviderConnectItems();
|
|
1480
1811
|
if (step === "auth")
|
|
1481
1812
|
return providerId ? buildPickerItems("provider-auth", providerId) : [];
|
|
1813
|
+
if (step === "skills")
|
|
1814
|
+
return buildSkillItems();
|
|
1482
1815
|
if (step === "models") {
|
|
1483
1816
|
const modelItems = buildPickerItems("model", providerId);
|
|
1484
1817
|
if (modelItems.length || providerId)
|
|
@@ -1634,17 +1967,19 @@ function OpenTuiApp(props) {
|
|
|
1634
1967
|
gutter.fg = active ? activeText : providerDialogGutterColor(row.item.gutter ?? (isCurrentModelItem(row.item) ? "●" : undefined));
|
|
1635
1968
|
}
|
|
1636
1969
|
if (label) {
|
|
1637
|
-
label.content = truncate(row.item.label,
|
|
1970
|
+
label.content = truncate(row.item.label, providerDialogLabelWidth(state));
|
|
1638
1971
|
label.fg = active ? activeText : isCurrentModelItem(row.item) ? theme.primary : theme.text;
|
|
1639
1972
|
}
|
|
1640
1973
|
if (detail) {
|
|
1641
1974
|
const detailText = state.query.trim() && state.step === "models"
|
|
1642
1975
|
? row.item.category ?? row.item.detail ?? ""
|
|
1643
1976
|
: row.item.detail ?? "";
|
|
1644
|
-
detail.
|
|
1977
|
+
detail.width = providerDialogDetailWidth(state);
|
|
1978
|
+
detail.content = truncate(detailText, providerDialogDetailWidth(state));
|
|
1645
1979
|
detail.fg = active ? activeText : theme.textMuted;
|
|
1646
1980
|
}
|
|
1647
1981
|
if (footer) {
|
|
1982
|
+
footer.width = providerDialogFooterWidth(state);
|
|
1648
1983
|
footer.content = row.item.footer ?? "";
|
|
1649
1984
|
footer.fg = active ? activeText : theme.textMuted;
|
|
1650
1985
|
}
|
|
@@ -1659,6 +1994,8 @@ function OpenTuiApp(props) {
|
|
|
1659
1994
|
function providerDialogTitleFor(state) {
|
|
1660
1995
|
if (state.step === "providers")
|
|
1661
1996
|
return "Connect a provider";
|
|
1997
|
+
if (state.step === "skills")
|
|
1998
|
+
return "Select skill";
|
|
1662
1999
|
const provider = providerDisplayName(state.providerId);
|
|
1663
2000
|
if (state.step === "auth")
|
|
1664
2001
|
return `${provider} auth method`;
|
|
@@ -1677,6 +2014,8 @@ function OpenTuiApp(props) {
|
|
|
1677
2014
|
const connect = state.providerId ? "" : " · ctrl+o providers";
|
|
1678
2015
|
return `↑/↓ move · enter select · esc close${connect}${count}`;
|
|
1679
2016
|
}
|
|
2017
|
+
if (state.step === "skills")
|
|
2018
|
+
return `↑/↓ move · enter insert · esc close${count}`;
|
|
1680
2019
|
const escLabel = state.step === "providers" ? "esc close" : "esc back";
|
|
1681
2020
|
return `↑/↓ move · enter select · ${escLabel}${count}`;
|
|
1682
2021
|
}
|
|
@@ -1689,8 +2028,14 @@ function OpenTuiApp(props) {
|
|
|
1689
2028
|
return theme.warning;
|
|
1690
2029
|
return theme.textMuted;
|
|
1691
2030
|
}
|
|
1692
|
-
function
|
|
1693
|
-
return
|
|
2031
|
+
function providerDialogLabelWidth(state) {
|
|
2032
|
+
return state.step === "skills" ? 22 : 37;
|
|
2033
|
+
}
|
|
2034
|
+
function providerDialogDetailWidth(state) {
|
|
2035
|
+
return state.step === "skills" ? 26 : 16;
|
|
2036
|
+
}
|
|
2037
|
+
function providerDialogFooterWidth(state) {
|
|
2038
|
+
return state.step === "skills" ? 9 : 8;
|
|
1694
2039
|
}
|
|
1695
2040
|
function isCurrentModelItem(item) {
|
|
1696
2041
|
return item.value === props.agent.model || item.detail?.includes("current");
|
|
@@ -1755,7 +2100,7 @@ function OpenTuiApp(props) {
|
|
|
1755
2100
|
else if (state.step === "key") {
|
|
1756
2101
|
openProviderDialog(state.providerId && registry.supportsOAuth(state.providerId) ? "auth" : "providers", state.providerId);
|
|
1757
2102
|
}
|
|
1758
|
-
else if (state.step === "models") {
|
|
2103
|
+
else if (state.step === "models" || state.step === "skills") {
|
|
1759
2104
|
closeProviderDialog();
|
|
1760
2105
|
}
|
|
1761
2106
|
else {
|
|
@@ -1885,6 +2230,11 @@ function OpenTuiApp(props) {
|
|
|
1885
2230
|
}
|
|
1886
2231
|
closeProviderDialog();
|
|
1887
2232
|
await executeSlash(item.command);
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (state.step === "skills") {
|
|
2236
|
+
closeProviderDialog();
|
|
2237
|
+
insertSkillPrompt(item.value);
|
|
1888
2238
|
}
|
|
1889
2239
|
}
|
|
1890
2240
|
function ensureProviderConfiguredForKey(providerId) {
|
|
@@ -2262,6 +2612,8 @@ function OpenTuiApp(props) {
|
|
|
2262
2612
|
const clearMessages = () => {
|
|
2263
2613
|
displayMessages = [];
|
|
2264
2614
|
streamingDisplay = undefined;
|
|
2615
|
+
promptHistory = [];
|
|
2616
|
+
resetPromptHistoryBrowse();
|
|
2265
2617
|
redrawTranscript(undefined, []);
|
|
2266
2618
|
};
|
|
2267
2619
|
async function submitPrompt() {
|
|
@@ -2275,9 +2627,7 @@ function OpenTuiApp(props) {
|
|
|
2275
2627
|
}
|
|
2276
2628
|
const plan = pendingPlan();
|
|
2277
2629
|
if (plan) {
|
|
2278
|
-
|
|
2279
|
-
setApprovalOptionIdx(0);
|
|
2280
|
-
plan.resolve({ action: "approve", plan: plan.plan });
|
|
2630
|
+
resolvePendingPlanSelection(plan);
|
|
2281
2631
|
return;
|
|
2282
2632
|
}
|
|
2283
2633
|
if (isRunning())
|
|
@@ -2294,6 +2644,7 @@ function OpenTuiApp(props) {
|
|
|
2294
2644
|
return;
|
|
2295
2645
|
activePrompt()?.clear();
|
|
2296
2646
|
promptText = "";
|
|
2647
|
+
resetPromptHistoryBrowse();
|
|
2297
2648
|
if (picker?.kind === "key") {
|
|
2298
2649
|
const providerId = picker.providerId;
|
|
2299
2650
|
const after = picker.after;
|
|
@@ -2310,9 +2661,15 @@ function OpenTuiApp(props) {
|
|
|
2310
2661
|
}
|
|
2311
2662
|
if (input.startsWith("/") && !/\s/.test(input)) {
|
|
2312
2663
|
const query = input.slice(1).toLowerCase();
|
|
2313
|
-
const matches =
|
|
2664
|
+
const matches = slashCandidates().filter((command) => command.name.toLowerCase().startsWith(query));
|
|
2314
2665
|
if (matches.length === 1) {
|
|
2315
|
-
|
|
2666
|
+
const match = matches[0];
|
|
2667
|
+
if (match.source === "skill") {
|
|
2668
|
+
insertSkillPrompt(match.name);
|
|
2669
|
+
}
|
|
2670
|
+
else {
|
|
2671
|
+
await executeSlash(`/${match.name}`);
|
|
2672
|
+
}
|
|
2316
2673
|
return;
|
|
2317
2674
|
}
|
|
2318
2675
|
if (matches.length > 1) {
|
|
@@ -2332,6 +2689,10 @@ function OpenTuiApp(props) {
|
|
|
2332
2689
|
function onPromptContentChange(value) {
|
|
2333
2690
|
const nextValue = typeof value === "string" ? value : readPromptText();
|
|
2334
2691
|
promptText = nextValue;
|
|
2692
|
+
if (promptHistoryIndex !== undefined
|
|
2693
|
+
&& nextValue !== (promptHistory[promptHistoryIndex] ?? "")) {
|
|
2694
|
+
resetPromptHistoryBrowse();
|
|
2695
|
+
}
|
|
2335
2696
|
if (providerDialog)
|
|
2336
2697
|
return;
|
|
2337
2698
|
if (picker?.kind === "key")
|
|
@@ -2432,25 +2793,39 @@ function OpenTuiApp(props) {
|
|
|
2432
2793
|
activePrompt()?.focus();
|
|
2433
2794
|
redrawDock();
|
|
2434
2795
|
}
|
|
2435
|
-
function
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2796
|
+
function slashCandidates() {
|
|
2797
|
+
const skillCommands = skills.summaries().map((skill) => ({
|
|
2798
|
+
name: skill.name,
|
|
2799
|
+
description: skill.description,
|
|
2800
|
+
source: "skill",
|
|
2801
|
+
sourceLabel: skill.source,
|
|
2802
|
+
}));
|
|
2803
|
+
return [
|
|
2438
2804
|
...LOCAL_SLASH_COMMANDS.map((c) => ({ ...c, source: "builtin" })),
|
|
2805
|
+
...skillCommands,
|
|
2439
2806
|
...slashRegistry.list(),
|
|
2440
2807
|
];
|
|
2808
|
+
}
|
|
2809
|
+
function buildSlashItems(query = "") {
|
|
2810
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
2811
|
+
const commands = slashCandidates();
|
|
2441
2812
|
const matches = slashCommandMatches(commands, normalizedQuery);
|
|
2442
2813
|
return matches.map(({ command }) => {
|
|
2443
2814
|
const isMcp = command.source === "mcp";
|
|
2444
|
-
const
|
|
2815
|
+
const isSkill = command.source === "skill";
|
|
2816
|
+
const badge = isMcp ? " :mcp" : isSkill ? " :skill" : "";
|
|
2445
2817
|
const label = `/${command.name}${badge}`;
|
|
2446
2818
|
const detail = isMcp && command.sourceLabel
|
|
2447
2819
|
? `[${command.sourceLabel}] ${command.description}`
|
|
2448
|
-
: command.
|
|
2820
|
+
: isSkill && command.sourceLabel
|
|
2821
|
+
? `[${command.sourceLabel}] ${command.description}`
|
|
2822
|
+
: command.description;
|
|
2449
2823
|
return {
|
|
2450
2824
|
label,
|
|
2451
2825
|
detail,
|
|
2452
2826
|
value: command.name,
|
|
2453
2827
|
command: `/${command.name}`,
|
|
2828
|
+
action: isSkill ? "insert-skill" : undefined,
|
|
2454
2829
|
};
|
|
2455
2830
|
});
|
|
2456
2831
|
}
|
|
@@ -2575,6 +2950,12 @@ function OpenTuiApp(props) {
|
|
|
2575
2950
|
prompt.cursorOffset = next.length;
|
|
2576
2951
|
closePicker();
|
|
2577
2952
|
}
|
|
2953
|
+
function insertSkillPrompt(skillName) {
|
|
2954
|
+
closePicker();
|
|
2955
|
+
resetPromptHistoryBrowse();
|
|
2956
|
+
setPromptText(`/${skillName} `);
|
|
2957
|
+
redrawDock();
|
|
2958
|
+
}
|
|
2578
2959
|
async function handleInput(input) {
|
|
2579
2960
|
setNotice("");
|
|
2580
2961
|
if (input.startsWith("/")) {
|
|
@@ -2671,6 +3052,10 @@ function OpenTuiApp(props) {
|
|
|
2671
3052
|
openProviderDialog(kind === "provider-auth" ? "auth" : "providers", providerId);
|
|
2672
3053
|
return;
|
|
2673
3054
|
}
|
|
3055
|
+
if (kind === "skill") {
|
|
3056
|
+
openProviderDialog("skills");
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
2674
3059
|
if (kind === "key") {
|
|
2675
3060
|
picker = {
|
|
2676
3061
|
kind: "key",
|
|
@@ -2710,6 +3095,10 @@ function OpenTuiApp(props) {
|
|
|
2710
3095
|
applyFileSuggestion(item.value);
|
|
2711
3096
|
return;
|
|
2712
3097
|
}
|
|
3098
|
+
if (item.action === "insert-skill" || (picker?.kind === "select" && picker.mode === "skill")) {
|
|
3099
|
+
insertSkillPrompt(item.value);
|
|
3100
|
+
return;
|
|
3101
|
+
}
|
|
2713
3102
|
if (picker?.kind === "select" && picker.mode === "slash") {
|
|
2714
3103
|
activePrompt()?.clear();
|
|
2715
3104
|
closePicker();
|
|
@@ -2796,6 +3185,8 @@ function OpenTuiApp(props) {
|
|
|
2796
3185
|
return [];
|
|
2797
3186
|
if (kind === "mcp-reconnect")
|
|
2798
3187
|
return buildMcpReconnectItems();
|
|
3188
|
+
if (kind === "skill")
|
|
3189
|
+
return buildSkillItems();
|
|
2799
3190
|
if (kind === "model") {
|
|
2800
3191
|
const items = [];
|
|
2801
3192
|
for (const provider of registry.getEnabled()) {
|
|
@@ -2909,6 +3300,25 @@ function OpenTuiApp(props) {
|
|
|
2909
3300
|
command: `/logout ${provider.id}`,
|
|
2910
3301
|
}));
|
|
2911
3302
|
}
|
|
3303
|
+
function buildSkillItems() {
|
|
3304
|
+
return skills.summaries().map((skill) => {
|
|
3305
|
+
const tags = skill.tags && skill.tags.length > 0 ? ` · ${skill.tags.join(", ")}` : "";
|
|
3306
|
+
const category = skill.source === "project"
|
|
3307
|
+
? "Project skills"
|
|
3308
|
+
: skill.source === "configured"
|
|
3309
|
+
? "Configured skills"
|
|
3310
|
+
: "User skills";
|
|
3311
|
+
return {
|
|
3312
|
+
label: skill.name,
|
|
3313
|
+
detail: `${skill.description}${tags}`,
|
|
3314
|
+
value: skill.name,
|
|
3315
|
+
command: `/${skill.name}`,
|
|
3316
|
+
action: "insert-skill",
|
|
3317
|
+
category,
|
|
3318
|
+
footer: skill.source,
|
|
3319
|
+
};
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
2912
3322
|
function buildProviderConnectItems() {
|
|
2913
3323
|
const configuredProviders = registry.getConfigured();
|
|
2914
3324
|
const configured = new Map(configuredProviders.map((provider) => [provider.id, provider]));
|
|
@@ -3002,17 +3412,19 @@ function OpenTuiApp(props) {
|
|
|
3002
3412
|
addMessage("error", "No model selected. Use /model after /login or provider setup.");
|
|
3003
3413
|
return;
|
|
3004
3414
|
}
|
|
3415
|
+
rememberPromptHistory(displayInput);
|
|
3005
3416
|
const nextMessages = [...displayMessages, { role: "user", content: displayInput }];
|
|
3006
3417
|
displayMessages = nextMessages;
|
|
3007
3418
|
streamingDisplay = undefined;
|
|
3008
3419
|
redrawTranscript(undefined, nextMessages);
|
|
3009
|
-
|
|
3420
|
+
const run = beginAgentRun();
|
|
3010
3421
|
let assistantContent = "";
|
|
3011
3422
|
let assistantReasoning = "";
|
|
3012
3423
|
const toolCalls = [];
|
|
3013
3424
|
let runError;
|
|
3425
|
+
let runCancelled = false;
|
|
3014
3426
|
try {
|
|
3015
|
-
for await (const event of props.agent.run(actualInput, props.args.cwd)) {
|
|
3427
|
+
for await (const event of props.agent.run(actualInput, props.args.cwd, { abortSignal: run.abortController.signal })) {
|
|
3016
3428
|
if (event.type === "turn_start") {
|
|
3017
3429
|
assistantContent = "";
|
|
3018
3430
|
assistantReasoning = "";
|
|
@@ -3092,6 +3504,7 @@ function OpenTuiApp(props) {
|
|
|
3092
3504
|
else if (event.type === "turn_end") {
|
|
3093
3505
|
if (event.usage) {
|
|
3094
3506
|
setSidebarUsage((current) => ({
|
|
3507
|
+
contextTokens: event.usage.promptTokens || current.contextTokens,
|
|
3095
3508
|
promptTokens: current.promptTokens + event.usage.promptTokens,
|
|
3096
3509
|
completionTokens: current.completionTokens + event.usage.completionTokens,
|
|
3097
3510
|
promptCacheHitTokens: current.promptCacheHitTokens + (event.usage.promptCacheHitTokens ?? 0),
|
|
@@ -3121,13 +3534,16 @@ function OpenTuiApp(props) {
|
|
|
3121
3534
|
}
|
|
3122
3535
|
}
|
|
3123
3536
|
catch (error) {
|
|
3124
|
-
|
|
3537
|
+
runCancelled = error instanceof AgentAbortError || run.abortController.signal.aborted || error?.name === "AbortError";
|
|
3538
|
+
if (!runCancelled) {
|
|
3539
|
+
runError = error?.message || String(error);
|
|
3540
|
+
}
|
|
3125
3541
|
}
|
|
3126
3542
|
finally {
|
|
3127
3543
|
pendingApprovalRef = undefined;
|
|
3128
3544
|
setPendingApproval(undefined);
|
|
3129
3545
|
setApprovalOptionIdx(0);
|
|
3130
|
-
|
|
3546
|
+
finishAgentRun(run);
|
|
3131
3547
|
streamingDisplay = undefined;
|
|
3132
3548
|
if (runError) {
|
|
3133
3549
|
const errorMessage = runError;
|
|
@@ -3135,6 +3551,11 @@ function OpenTuiApp(props) {
|
|
|
3135
3551
|
displayMessages = nextMessages;
|
|
3136
3552
|
redrawTranscript(undefined, nextMessages);
|
|
3137
3553
|
}
|
|
3554
|
+
else if (runCancelled) {
|
|
3555
|
+
if (!notice())
|
|
3556
|
+
setNotice("Agent run cancelled");
|
|
3557
|
+
redrawTranscript();
|
|
3558
|
+
}
|
|
3138
3559
|
else {
|
|
3139
3560
|
redrawTranscript();
|
|
3140
3561
|
}
|
|
@@ -3145,24 +3566,20 @@ function OpenTuiApp(props) {
|
|
|
3145
3566
|
}
|
|
3146
3567
|
}
|
|
3147
3568
|
function promptUiKeyDown(event) {
|
|
3148
|
-
if (
|
|
3149
|
-
return true;
|
|
3150
|
-
if (handleQuestionKey(event))
|
|
3151
|
-
return true;
|
|
3152
|
-
const name = String(event.name || "").toLowerCase();
|
|
3153
|
-
const plan = pendingPlan();
|
|
3154
|
-
if (plan && (name === "left" || name === "right" || name === "h" || name === "l")) {
|
|
3155
|
-
const idx = approvalOptionIdx();
|
|
3156
|
-
const next = name === "left" || name === "h"
|
|
3157
|
-
? (idx - 1 + PLAN_OPTIONS.length) % PLAN_OPTIONS.length
|
|
3158
|
-
: (idx + 1) % PLAN_OPTIONS.length;
|
|
3159
|
-
setApprovalOptionIdx(next);
|
|
3160
|
-
forceApprovalUI();
|
|
3161
|
-
event.preventDefault?.();
|
|
3569
|
+
if (routeRunningCancel(keyNameFromEvent(event), event))
|
|
3162
3570
|
return true;
|
|
3571
|
+
const modalOwner = activeModalKeyOwner();
|
|
3572
|
+
if (modalOwner) {
|
|
3573
|
+
if (routeModalKey(event) || shouldModalSwallowUnhandledKey(modalOwner)) {
|
|
3574
|
+
event.preventDefault?.();
|
|
3575
|
+
event.stopPropagation?.();
|
|
3576
|
+
return true;
|
|
3577
|
+
}
|
|
3163
3578
|
}
|
|
3164
3579
|
if (cycleModeFromKey(event))
|
|
3165
3580
|
return true;
|
|
3581
|
+
if (handlePromptHistoryKey(event))
|
|
3582
|
+
return true;
|
|
3166
3583
|
return false;
|
|
3167
3584
|
}
|
|
3168
3585
|
function renderComposer() {
|
|
@@ -3267,6 +3684,8 @@ function OpenTuiApp(props) {
|
|
|
3267
3684
|
redrawQuestionPanel();
|
|
3268
3685
|
},
|
|
3269
3686
|
visible: false,
|
|
3687
|
+
focusable: true,
|
|
3688
|
+
onKeyDown: handleQuestionKey,
|
|
3270
3689
|
position: "absolute",
|
|
3271
3690
|
left: 2,
|
|
3272
3691
|
right: 2,
|
|
@@ -3680,7 +4099,7 @@ function OpenTuiApp(props) {
|
|
|
3680
4099
|
flexShrink: 0,
|
|
3681
4100
|
ref: (ref) => {
|
|
3682
4101
|
sidebarGaugeLabelText = ref;
|
|
3683
|
-
ref.content = buildGaugeLabel(context.percent);
|
|
4102
|
+
ref.content = buildGaugeLabel(context.percent, context.remainingTokens);
|
|
3684
4103
|
},
|
|
3685
4104
|
}),
|
|
3686
4105
|
h("text", {
|
|
@@ -3873,6 +4292,13 @@ function OpenTuiApp(props) {
|
|
|
3873
4292
|
? getContextBudget(providerId, modelId, props.agent.messages)
|
|
3874
4293
|
: undefined;
|
|
3875
4294
|
const usage = sidebarUsage();
|
|
4295
|
+
const contextTokens = usage.contextTokens || (budget?.estimatedTokens ?? 0);
|
|
4296
|
+
const contextPercent = budget?.contextWindow
|
|
4297
|
+
? Math.min(100, Math.round((contextTokens / budget.contextWindow) * 100))
|
|
4298
|
+
: Math.round(budget?.percent ?? 0);
|
|
4299
|
+
const remainingTokens = budget?.contextWindow !== undefined
|
|
4300
|
+
? Math.max(0, budget.contextWindow - contextTokens)
|
|
4301
|
+
: undefined;
|
|
3876
4302
|
const tokenUsage = {
|
|
3877
4303
|
promptTokens: usage.promptTokens,
|
|
3878
4304
|
completionTokens: usage.completionTokens,
|
|
@@ -3883,8 +4309,9 @@ function OpenTuiApp(props) {
|
|
|
3883
4309
|
};
|
|
3884
4310
|
const cost = providerId && modelId ? calculateUsageCost(providerId, modelId, tokenUsage) : undefined;
|
|
3885
4311
|
return {
|
|
3886
|
-
tokens:
|
|
3887
|
-
percent:
|
|
4312
|
+
tokens: contextTokens,
|
|
4313
|
+
percent: contextPercent,
|
|
4314
|
+
remainingTokens,
|
|
3888
4315
|
promptTokens: usage.promptTokens,
|
|
3889
4316
|
completionTokens: usage.completionTokens,
|
|
3890
4317
|
reasoningTokens: usage.reasoningTokens,
|
|
@@ -3914,6 +4341,7 @@ function OpenTuiApp(props) {
|
|
|
3914
4341
|
ref: (ref) => { scrollbox = ref; },
|
|
3915
4342
|
stickyScroll: true,
|
|
3916
4343
|
stickyStart: "bottom",
|
|
4344
|
+
onMouseScroll: handleTranscriptMouseScroll,
|
|
3917
4345
|
flexGrow: 1,
|
|
3918
4346
|
minHeight: 0,
|
|
3919
4347
|
}, h("box", { height: 1 }), h("box", {
|
|
@@ -3925,7 +4353,8 @@ function OpenTuiApp(props) {
|
|
|
3925
4353
|
updateTranscriptHost(ref, transcriptState, currentTranscriptMessages(streamingDisplay), transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
|
|
3926
4354
|
syncThinkingSpinner();
|
|
3927
4355
|
syncPromptSurfaces(isNewHost);
|
|
3928
|
-
|
|
4356
|
+
if (isNewHost)
|
|
4357
|
+
scheduleTranscriptScrollAfterUpdate(transcriptScrollFollowing, 0);
|
|
3929
4358
|
},
|
|
3930
4359
|
flexDirection: "column",
|
|
3931
4360
|
flexShrink: 0,
|
|
@@ -3935,6 +4364,8 @@ function OpenTuiApp(props) {
|
|
|
3935
4364
|
}, notice()) : null, renderQuestionPanelHost(), h("box", {
|
|
3936
4365
|
ref: (ref) => { approvalRoot = ref; },
|
|
3937
4366
|
visible: !!approval,
|
|
4367
|
+
focusable: true,
|
|
4368
|
+
onKeyDown: handleApprovalKey,
|
|
3938
4369
|
position: "absolute",
|
|
3939
4370
|
left: 2,
|
|
3940
4371
|
right: 2,
|
|
@@ -4046,9 +4477,38 @@ function OpenTuiApp(props) {
|
|
|
4046
4477
|
ref: (ref) => { approvalOptionTexts[index] = ref; },
|
|
4047
4478
|
fg: theme.textMuted,
|
|
4048
4479
|
content: "",
|
|
4049
|
-
})))), h("box", { flexDirection: "row", gap: 2, flexShrink: 0 }, h("text", { fg: theme.text }, "
|
|
4480
|
+
})))), h("box", { flexDirection: "row", gap: 2, flexShrink: 0 }, h("text", { fg: theme.text }, "⇆/tab ", h("span", { fg: theme.textMuted }, "select")), h("text", { fg: theme.text }, "enter ", h("span", { fg: theme.textMuted }, "confirm")), h("text", { fg: theme.text }, "esc ", h("span", { fg: theme.textMuted }, "reject")))))),
|
|
4050
4481
|
]);
|
|
4051
4482
|
}
|
|
4483
|
+
function renderNoticeOverlay() {
|
|
4484
|
+
return h("box", {
|
|
4485
|
+
ref: (ref) => {
|
|
4486
|
+
copyToastRoot = ref;
|
|
4487
|
+
ref.visible = false;
|
|
4488
|
+
},
|
|
4489
|
+
visible: false,
|
|
4490
|
+
position: "absolute",
|
|
4491
|
+
top: 2,
|
|
4492
|
+
right: sidebarVisible() ? SESSION_SIDEBAR_WIDTH + 2 : 2,
|
|
4493
|
+
zIndex: 4000,
|
|
4494
|
+
flexDirection: "column",
|
|
4495
|
+
alignItems: "flex-start",
|
|
4496
|
+
width: 24,
|
|
4497
|
+
paddingLeft: 2,
|
|
4498
|
+
paddingRight: 2,
|
|
4499
|
+
paddingTop: 1,
|
|
4500
|
+
paddingBottom: 1,
|
|
4501
|
+
backgroundColor: theme.backgroundPanel,
|
|
4502
|
+
border: ["left", "right"],
|
|
4503
|
+
borderColor: theme.info,
|
|
4504
|
+
}, h("text", {
|
|
4505
|
+
ref: (ref) => { copyToastText = ref; },
|
|
4506
|
+
fg: theme.text,
|
|
4507
|
+
wrapMode: "word",
|
|
4508
|
+
width: "100%",
|
|
4509
|
+
content: "",
|
|
4510
|
+
}));
|
|
4511
|
+
}
|
|
4052
4512
|
return h("box", {
|
|
4053
4513
|
ref: (ref) => { rootBox = ref; },
|
|
4054
4514
|
flexDirection: "column",
|
|
@@ -4080,6 +4540,7 @@ function OpenTuiApp(props) {
|
|
|
4080
4540
|
registerModeBadge: registerFooterModeBadge,
|
|
4081
4541
|
}),
|
|
4082
4542
|
renderProviderDialog(),
|
|
4543
|
+
renderNoticeOverlay(),
|
|
4083
4544
|
]);
|
|
4084
4545
|
}
|
|
4085
4546
|
function renderPrompt(input) {
|
|
@@ -4988,9 +5449,9 @@ function renderFooter(input) {
|
|
|
4988
5449
|
idleFg: theme.textMuted,
|
|
4989
5450
|
runningFg: theme.primary,
|
|
4990
5451
|
}), h("text", {
|
|
4991
|
-
fg:
|
|
5452
|
+
fg: permissionModeColor(input.mode()),
|
|
4992
5453
|
ref: input.registerModeBadge,
|
|
4993
|
-
},
|
|
5454
|
+
}, footerPermissionModeText(input.mode())), h("box", { flexGrow: 1 }));
|
|
4994
5455
|
}
|
|
4995
5456
|
function pickerTitle(kind, providerId) {
|
|
4996
5457
|
switch (kind) {
|
|
@@ -5012,6 +5473,8 @@ function pickerTitle(kind, providerId) {
|
|
|
5012
5473
|
return "Select Login Provider";
|
|
5013
5474
|
case "logout":
|
|
5014
5475
|
return "Select Logout Provider";
|
|
5476
|
+
case "skill":
|
|
5477
|
+
return "Skills";
|
|
5015
5478
|
case "slash":
|
|
5016
5479
|
return "Commands";
|
|
5017
5480
|
case "file":
|
|
@@ -5447,13 +5910,37 @@ function contrastText(color) {
|
|
|
5447
5910
|
return luminance > 160 ? "#000000" : "#ffffff";
|
|
5448
5911
|
}
|
|
5449
5912
|
function promptModeBadgeContent(mode) {
|
|
5450
|
-
const
|
|
5451
|
-
const
|
|
5452
|
-
const label = isPlan ? "Plan" : "Build";
|
|
5913
|
+
const color = permissionModeColor(mode);
|
|
5914
|
+
const label = permissionModeBadgeLabel(mode);
|
|
5453
5915
|
return new StyledText([
|
|
5454
5916
|
bg(color)(fg(contrastText(color))(bold(` ${label} `))),
|
|
5455
5917
|
]);
|
|
5456
5918
|
}
|
|
5919
|
+
function permissionModeBadgeLabel(mode) {
|
|
5920
|
+
switch (mode) {
|
|
5921
|
+
case "default": return "Build";
|
|
5922
|
+
case "plan": return "Plan";
|
|
5923
|
+
case "bypassPermissions": return "Bypass";
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
function footerPermissionModeText(mode) {
|
|
5927
|
+
const info = PERMISSION_MODE_INFO[mode];
|
|
5928
|
+
if (mode === "default")
|
|
5929
|
+
return " mode: build · shift+tab plan";
|
|
5930
|
+
if (mode === "plan")
|
|
5931
|
+
return " mode: plan · shift+tab bypass";
|
|
5932
|
+
return ` mode: ${info.shortTitle} · shift+tab build`;
|
|
5933
|
+
}
|
|
5934
|
+
function permissionModeColor(mode) {
|
|
5935
|
+
const info = PERMISSION_MODE_INFO[mode];
|
|
5936
|
+
switch (info.color) {
|
|
5937
|
+
case "accent": return theme.accent;
|
|
5938
|
+
case "success": return theme.success;
|
|
5939
|
+
case "warning": return theme.warning;
|
|
5940
|
+
case "error": return theme.error;
|
|
5941
|
+
case "muted": return theme.primary;
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5457
5944
|
function toolColor(tool) {
|
|
5458
5945
|
if (tool.isError)
|
|
5459
5946
|
return theme.toolError;
|
|
@@ -5622,18 +6109,6 @@ function assistantStatusLabel(message) {
|
|
|
5622
6109
|
return "Responding...";
|
|
5623
6110
|
return message.streaming ? "Thinking..." : "Thinking";
|
|
5624
6111
|
}
|
|
5625
|
-
function buildColoredGauge(percent, barWidth) {
|
|
5626
|
-
const clamped = Math.max(0, Math.min(100, percent));
|
|
5627
|
-
const filled = Math.round((clamped / 100) * barWidth);
|
|
5628
|
-
const empty = barWidth - filled;
|
|
5629
|
-
const gaugeColor = clamped >= 80 ? theme.error : clamped >= 60 ? theme.warning : theme.success;
|
|
5630
|
-
const chunks = [];
|
|
5631
|
-
if (filled > 0)
|
|
5632
|
-
chunks.push(fg(gaugeColor)("█".repeat(filled)));
|
|
5633
|
-
if (empty > 0)
|
|
5634
|
-
chunks.push(fg(theme.borderSubtle)("░".repeat(empty)));
|
|
5635
|
-
return new StyledText(chunks);
|
|
5636
|
-
}
|
|
5637
6112
|
function buildContextGauge(percent, barWidth) {
|
|
5638
6113
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
5639
6114
|
const filled = Math.round((clamped / 100) * barWidth);
|
|
@@ -5642,17 +6117,27 @@ function buildContextGauge(percent, barWidth) {
|
|
|
5642
6117
|
const space = empty > 0 ? "░".repeat(empty) : "";
|
|
5643
6118
|
return `${block}${space}`;
|
|
5644
6119
|
}
|
|
5645
|
-
function buildGaugeLabel(percent) {
|
|
6120
|
+
function buildGaugeLabel(percent, remainingTokens) {
|
|
6121
|
+
const remaining = remainingTokens === undefined ? "" : ` · ${formatContextRemaining(remainingTokens)} left`;
|
|
5646
6122
|
if (percent >= 95)
|
|
5647
|
-
return
|
|
6123
|
+
return `⚠ Compact imminent${remaining}`;
|
|
5648
6124
|
if (percent >= 80)
|
|
5649
|
-
return
|
|
6125
|
+
return `⚠ Approaching limit${remaining}`;
|
|
5650
6126
|
if (percent >= 60)
|
|
5651
|
-
return
|
|
6127
|
+
return `● Context growing${remaining}`;
|
|
5652
6128
|
if (percent > 0)
|
|
5653
|
-
return
|
|
6129
|
+
return `○ Available${remaining}`;
|
|
5654
6130
|
return "○ Empty";
|
|
5655
6131
|
}
|
|
6132
|
+
function formatContextRemaining(value) {
|
|
6133
|
+
if (value >= 1_000_000)
|
|
6134
|
+
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
6135
|
+
if (value >= 100_000)
|
|
6136
|
+
return `${Math.round(value / 1_000)}K`;
|
|
6137
|
+
if (value >= 1_000)
|
|
6138
|
+
return `${(value / 1_000).toFixed(1)}K`;
|
|
6139
|
+
return String(value);
|
|
6140
|
+
}
|
|
5656
6141
|
function thinkingToggleLabel(expanded, streaming = false, spinnerFrame = "") {
|
|
5657
6142
|
const arrow = expanded ? "▼" : "▶";
|
|
5658
6143
|
return streaming && spinnerFrame ? `${spinnerFrame} ${arrow} Thinking` : `${arrow} Thinking`;
|