@bubblebrain-ai/bubble 0.0.1 → 0.0.3
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.d.ts +1 -1
- package/dist/main.js +2 -4
- 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 +796 -203
- package/dist/types.d.ts +5 -5
- package/package.json +5 -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;
|
|
@@ -359,6 +387,10 @@ function OpenTuiApp(props) {
|
|
|
359
387
|
const sidebarLspRows = [];
|
|
360
388
|
const sidebarLspMarkers = [];
|
|
361
389
|
const sidebarLspLabels = [];
|
|
390
|
+
let sidebarTodoSection;
|
|
391
|
+
const sidebarTodoRows = [];
|
|
392
|
+
const sidebarTodoMarkers = [];
|
|
393
|
+
const sidebarTodoLabels = [];
|
|
362
394
|
const sidebarFileRows = [];
|
|
363
395
|
const sidebarFileLabels = [];
|
|
364
396
|
const sidebarFileAdditions = [];
|
|
@@ -367,15 +399,211 @@ function OpenTuiApp(props) {
|
|
|
367
399
|
const activePrompt = () => isHomeSurfaceActive()
|
|
368
400
|
? homePromptRef ?? sessionPromptRef
|
|
369
401
|
: sessionPromptRef ?? homePromptRef;
|
|
402
|
+
function setPromptText(value) {
|
|
403
|
+
promptText = value;
|
|
404
|
+
const prompt = activePrompt();
|
|
405
|
+
if (!prompt)
|
|
406
|
+
return;
|
|
407
|
+
prompt.setText(value);
|
|
408
|
+
prompt.cursorOffset = value.length;
|
|
409
|
+
prompt.focus();
|
|
410
|
+
}
|
|
411
|
+
function resetPromptHistoryBrowse() {
|
|
412
|
+
promptHistoryIndex = undefined;
|
|
413
|
+
promptHistoryDraft = "";
|
|
414
|
+
}
|
|
415
|
+
function rememberPromptHistory(input) {
|
|
416
|
+
const value = input.trimEnd();
|
|
417
|
+
if (!value.trim())
|
|
418
|
+
return;
|
|
419
|
+
promptHistory.push(value);
|
|
420
|
+
if (promptHistory.length > PROMPT_HISTORY_LIMIT) {
|
|
421
|
+
promptHistory = promptHistory.slice(-PROMPT_HISTORY_LIMIT);
|
|
422
|
+
}
|
|
423
|
+
resetPromptHistoryBrowse();
|
|
424
|
+
}
|
|
425
|
+
function canBrowsePromptHistory(direction) {
|
|
426
|
+
if (promptHistoryIndex !== undefined)
|
|
427
|
+
return true;
|
|
428
|
+
const prompt = activePrompt();
|
|
429
|
+
const text = prompt?.plainText ?? promptText;
|
|
430
|
+
if (!text)
|
|
431
|
+
return true;
|
|
432
|
+
const cursor = prompt?.logicalCursor;
|
|
433
|
+
if (!cursor)
|
|
434
|
+
return true;
|
|
435
|
+
if (direction === "up")
|
|
436
|
+
return cursor.row === 0;
|
|
437
|
+
return cursor.row >= text.split("\n").length - 1;
|
|
438
|
+
}
|
|
439
|
+
function browsePromptHistory(direction) {
|
|
440
|
+
if (!promptHistory.length)
|
|
441
|
+
return false;
|
|
442
|
+
if (!canBrowsePromptHistory(direction))
|
|
443
|
+
return false;
|
|
444
|
+
if (direction === "up") {
|
|
445
|
+
if (promptHistoryIndex === undefined) {
|
|
446
|
+
promptHistoryDraft = readPromptText() || promptText;
|
|
447
|
+
promptHistoryIndex = promptHistory.length - 1;
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
promptHistoryIndex = Math.max(0, promptHistoryIndex - 1);
|
|
451
|
+
}
|
|
452
|
+
setPromptText(promptHistory[promptHistoryIndex] ?? "");
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
if (promptHistoryIndex === undefined)
|
|
456
|
+
return false;
|
|
457
|
+
if (promptHistoryIndex < promptHistory.length - 1) {
|
|
458
|
+
promptHistoryIndex += 1;
|
|
459
|
+
setPromptText(promptHistory[promptHistoryIndex] ?? "");
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
setPromptText(promptHistoryDraft);
|
|
463
|
+
resetPromptHistoryBrowse();
|
|
464
|
+
}
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
function handlePromptHistoryKey(event) {
|
|
468
|
+
if (event.shift || event.ctrl || event.meta || event.super || event.hyper)
|
|
469
|
+
return false;
|
|
470
|
+
const name = keyNameFromEvent(event);
|
|
471
|
+
if (name !== "up" && name !== "down")
|
|
472
|
+
return false;
|
|
473
|
+
if (!browsePromptHistory(name))
|
|
474
|
+
return false;
|
|
475
|
+
event.preventDefault?.();
|
|
476
|
+
event.stopPropagation?.();
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
function blurInputsForModal() {
|
|
480
|
+
homePromptRef?.blur();
|
|
481
|
+
sessionPromptRef?.blur();
|
|
482
|
+
questionCustomInput?.blur();
|
|
483
|
+
providerDialogInput?.blur();
|
|
484
|
+
}
|
|
485
|
+
function focusApprovalPanel() {
|
|
486
|
+
setTimeout(() => {
|
|
487
|
+
if (pendingApproval() || pendingPlan())
|
|
488
|
+
approvalRoot?.focus();
|
|
489
|
+
}, 0);
|
|
490
|
+
}
|
|
491
|
+
function focusQuestionPanel() {
|
|
492
|
+
setTimeout(() => {
|
|
493
|
+
const state = pendingQuestion();
|
|
494
|
+
if (!state || state.editing)
|
|
495
|
+
return;
|
|
496
|
+
questionRoot?.focus();
|
|
497
|
+
}, 0);
|
|
498
|
+
}
|
|
499
|
+
function restorePromptAfterModal() {
|
|
500
|
+
setTimeout(() => {
|
|
501
|
+
if (!activeModalKeyOwner())
|
|
502
|
+
activePrompt()?.focus();
|
|
503
|
+
}, 0);
|
|
504
|
+
}
|
|
370
505
|
const activeComposerShell = () => isHomeSurfaceActive()
|
|
371
506
|
? homeComposerShell ?? sessionComposerShell
|
|
372
507
|
: sessionComposerShell ?? homeComposerShell;
|
|
373
508
|
onCleanup(() => {
|
|
374
509
|
uiDisposed = true;
|
|
510
|
+
if (copyToastClearTimer)
|
|
511
|
+
clearTimeout(copyToastClearTimer);
|
|
375
512
|
promptModeLabels.clear();
|
|
376
513
|
promptModelLabels.clear();
|
|
377
514
|
footerModeBadge = undefined;
|
|
378
515
|
});
|
|
516
|
+
function showCopyToast(toast, ttl = 2200) {
|
|
517
|
+
if (copyToastClearTimer)
|
|
518
|
+
clearTimeout(copyToastClearTimer);
|
|
519
|
+
const sidebarOffset = sidebarVisible() ? SESSION_SIDEBAR_WIDTH : 0;
|
|
520
|
+
const mainAreaWidth = Math.max(20, dimensions().width - sidebarOffset - 4);
|
|
521
|
+
const color = toast.variant === "success"
|
|
522
|
+
? theme.success
|
|
523
|
+
: toast.variant === "error"
|
|
524
|
+
? theme.error
|
|
525
|
+
: toast.variant === "warning"
|
|
526
|
+
? theme.warning
|
|
527
|
+
: theme.info;
|
|
528
|
+
const width = Math.max(24, Math.min(60, Math.min(mainAreaWidth, toast.message.length + 6)));
|
|
529
|
+
if (copyToastRoot) {
|
|
530
|
+
copyToastRoot.visible = true;
|
|
531
|
+
copyToastRoot.width = width;
|
|
532
|
+
copyToastRoot.right = sidebarOffset + 2;
|
|
533
|
+
copyToastRoot.borderColor = color;
|
|
534
|
+
}
|
|
535
|
+
if (copyToastText) {
|
|
536
|
+
copyToastText.fg = theme.text;
|
|
537
|
+
safeSetText(copyToastText, toast.message);
|
|
538
|
+
}
|
|
539
|
+
renderer.requestRender();
|
|
540
|
+
copyToastClearTimer = setTimeout(() => {
|
|
541
|
+
if (copyToastRoot)
|
|
542
|
+
copyToastRoot.visible = false;
|
|
543
|
+
renderer.requestRender();
|
|
544
|
+
copyToastClearTimer = undefined;
|
|
545
|
+
}, ttl);
|
|
546
|
+
}
|
|
547
|
+
async function copySelectionText(text) {
|
|
548
|
+
const now = Date.now();
|
|
549
|
+
if (!text.trim())
|
|
550
|
+
return;
|
|
551
|
+
if (text === lastCopiedSelection && now - lastCopiedSelectionAt < 350)
|
|
552
|
+
return;
|
|
553
|
+
const serial = ++selectionCopySerial;
|
|
554
|
+
let copied = false;
|
|
555
|
+
try {
|
|
556
|
+
await copyTextToClipboard(text);
|
|
557
|
+
copied = true;
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
try {
|
|
561
|
+
copied = renderer.copyToClipboardOSC52(text);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
copied = false;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (serial !== selectionCopySerial)
|
|
568
|
+
return;
|
|
569
|
+
if (copied) {
|
|
570
|
+
lastCopiedSelection = text;
|
|
571
|
+
lastCopiedSelectionAt = Date.now();
|
|
572
|
+
showCopyToast({ message: "Copied to clipboard", variant: "info" });
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
showCopyToast({ message: "Failed to copy selection", variant: "error" }, 3000);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function isInsideRenderable(renderable, container) {
|
|
579
|
+
if (!container)
|
|
580
|
+
return false;
|
|
581
|
+
let current = renderable;
|
|
582
|
+
while (current) {
|
|
583
|
+
if (current === container)
|
|
584
|
+
return true;
|
|
585
|
+
current = current.parent;
|
|
586
|
+
}
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
function getOpenTuiSelectionText(selection) {
|
|
590
|
+
const selectedRenderables = Array.isArray(selection?.selectedRenderables)
|
|
591
|
+
? [...selection.selectedRenderables]
|
|
592
|
+
: undefined;
|
|
593
|
+
if (!selectedRenderables?.length) {
|
|
594
|
+
return typeof selection?.getSelectedText === "function" ? selection.getSelectedText() : "";
|
|
595
|
+
}
|
|
596
|
+
return selectedRenderables
|
|
597
|
+
.filter((renderable) => !renderable.isDestroyed && !isInsideRenderable(renderable, sidebarShell))
|
|
598
|
+
.sort((a, b) => {
|
|
599
|
+
if (a.y !== b.y)
|
|
600
|
+
return a.y - b.y;
|
|
601
|
+
return a.x - b.x;
|
|
602
|
+
})
|
|
603
|
+
.map((renderable) => typeof renderable.getSelectedText === "function" ? renderable.getSelectedText() : "")
|
|
604
|
+
.filter(Boolean)
|
|
605
|
+
.join("\n");
|
|
606
|
+
}
|
|
379
607
|
const readPromptText = () => {
|
|
380
608
|
try {
|
|
381
609
|
return activePrompt()?.plainText ?? "";
|
|
@@ -397,6 +625,12 @@ function OpenTuiApp(props) {
|
|
|
397
625
|
setSidebarTick((value) => value + 1);
|
|
398
626
|
syncSidebarContext();
|
|
399
627
|
};
|
|
628
|
+
const syncTodosFromAgent = () => {
|
|
629
|
+
const nextTodos = props.agent.getTodos();
|
|
630
|
+
setTodos(nextTodos);
|
|
631
|
+
syncSidebarTodos(nextTodos);
|
|
632
|
+
bumpSidebar();
|
|
633
|
+
};
|
|
400
634
|
function refreshGitSidebar() {
|
|
401
635
|
setGitState(readGitSidebarState(props.args.cwd));
|
|
402
636
|
syncSidebarFiles();
|
|
@@ -422,10 +656,13 @@ function OpenTuiApp(props) {
|
|
|
422
656
|
setSidebarText(sidebarTokenText, `${formatCompactNumber(context.tokens)} tokens`);
|
|
423
657
|
setSidebarText(sidebarPercentText, `${context.percent}% used`);
|
|
424
658
|
if (sidebarGaugeText) {
|
|
425
|
-
sidebarGaugeText.content =
|
|
659
|
+
sidebarGaugeText.content = buildContextGauge(context.percent, 30);
|
|
660
|
+
sidebarGaugeText.requestRender();
|
|
426
661
|
}
|
|
427
662
|
if (sidebarGaugeLabelText) {
|
|
428
|
-
sidebarGaugeLabelText.content = buildGaugeLabel(context.percent);
|
|
663
|
+
sidebarGaugeLabelText.content = buildGaugeLabel(context.percent, context.remainingTokens);
|
|
664
|
+
sidebarGaugeLabelText.fg = context.percent >= 80 ? theme.error : context.percent >= 60 ? theme.warning : theme.success;
|
|
665
|
+
sidebarGaugeLabelText.requestRender();
|
|
429
666
|
}
|
|
430
667
|
setSidebarText(sidebarUsageText, context.turns > 0
|
|
431
668
|
? `${formatCompactNumber(context.promptTokens)} in · ${formatCompactNumber(context.completionTokens)} out`
|
|
@@ -494,6 +731,35 @@ function OpenTuiApp(props) {
|
|
|
494
731
|
}
|
|
495
732
|
sidebarShell?.requestRender();
|
|
496
733
|
}
|
|
734
|
+
function syncSidebarTodos(nextTodos = todos()) {
|
|
735
|
+
const visible = nextTodos.slice(0, 8);
|
|
736
|
+
if (sidebarTodoSection) {
|
|
737
|
+
sidebarTodoSection.visible = visible.length > 0;
|
|
738
|
+
}
|
|
739
|
+
for (let index = 0; index < 8; index++) {
|
|
740
|
+
const row = sidebarTodoRows[index];
|
|
741
|
+
const marker = sidebarTodoMarkers[index];
|
|
742
|
+
const label = sidebarTodoLabels[index];
|
|
743
|
+
const todo = visible[index];
|
|
744
|
+
if (!row || !marker || !label)
|
|
745
|
+
continue;
|
|
746
|
+
row.visible = !!todo;
|
|
747
|
+
if (!todo) {
|
|
748
|
+
safeRequestRender(row);
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
const completed = todo.status === "completed";
|
|
752
|
+
const inProgress = todo.status === "in_progress";
|
|
753
|
+
const labelText = inProgress ? (todo.activeForm || todo.content) : todo.content;
|
|
754
|
+
marker.content = completed ? "✓" : inProgress ? "◉" : "○";
|
|
755
|
+
marker.fg = completed ? theme.success : inProgress ? theme.warning : theme.textMuted;
|
|
756
|
+
label.content = labelText;
|
|
757
|
+
label.fg = completed ? theme.success : inProgress ? theme.warning : theme.textMuted;
|
|
758
|
+
safeRequestRender(row);
|
|
759
|
+
}
|
|
760
|
+
sidebarShell?.requestRender();
|
|
761
|
+
rootBox?.requestRender();
|
|
762
|
+
}
|
|
497
763
|
function showSidebarLspRows(statuses) {
|
|
498
764
|
for (let index = 0; index < sidebarLspRows.length; index++) {
|
|
499
765
|
const row = sidebarLspRows[index];
|
|
@@ -517,7 +783,7 @@ function OpenTuiApp(props) {
|
|
|
517
783
|
}
|
|
518
784
|
const promptModeTitle = () => mode() === "plan" ? "Plan" : "Build";
|
|
519
785
|
const promptModeBadge = () => promptModeBadgeContent(mode());
|
|
520
|
-
const footerModeText = () =>
|
|
786
|
+
const footerModeText = () => footerPermissionModeText(mode());
|
|
521
787
|
function syncModeChrome() {
|
|
522
788
|
if (uiDisposed)
|
|
523
789
|
return;
|
|
@@ -525,8 +791,11 @@ function OpenTuiApp(props) {
|
|
|
525
791
|
if (!safeSetText(label, promptModeBadge()))
|
|
526
792
|
promptModeLabels.delete(label);
|
|
527
793
|
}
|
|
528
|
-
if (footerModeBadge
|
|
529
|
-
footerModeBadge =
|
|
794
|
+
if (footerModeBadge) {
|
|
795
|
+
footerModeBadge.fg = permissionModeColor(mode());
|
|
796
|
+
if (!safeSetText(footerModeBadge, footerModeText()))
|
|
797
|
+
footerModeBadge = undefined;
|
|
798
|
+
}
|
|
530
799
|
safeRequestRender(homeComposerShell);
|
|
531
800
|
safeRequestRender(sessionComposerShell);
|
|
532
801
|
safeRequestRender(rootBox);
|
|
@@ -567,10 +836,10 @@ function OpenTuiApp(props) {
|
|
|
567
836
|
const cycleMode = () => {
|
|
568
837
|
if (picker || pendingPlan())
|
|
569
838
|
return false;
|
|
570
|
-
const next = getNextPermissionMode(props.agent.mode
|
|
839
|
+
const next = getNextPermissionMode(props.agent.mode);
|
|
571
840
|
props.agent.setMode(next);
|
|
572
841
|
setMode(next);
|
|
573
|
-
setNotice(`Mode: ${next
|
|
842
|
+
setNotice(`Mode: ${permissionModeBadgeLabel(next)}`);
|
|
574
843
|
redrawDock();
|
|
575
844
|
syncPromptSurfaces();
|
|
576
845
|
syncModeChrome();
|
|
@@ -584,9 +853,6 @@ function OpenTuiApp(props) {
|
|
|
584
853
|
return true;
|
|
585
854
|
};
|
|
586
855
|
const cycleModeFromRawSequence = (sequence) => {
|
|
587
|
-
const rawKey = keyNameFromSequence(sequence);
|
|
588
|
-
if (rawKey && handleApprovalNavigation(rawKey))
|
|
589
|
-
return true;
|
|
590
856
|
if (!isModeCycleSequence(sequence))
|
|
591
857
|
return false;
|
|
592
858
|
return cycleMode();
|
|
@@ -599,40 +865,7 @@ function OpenTuiApp(props) {
|
|
|
599
865
|
? ["Allow once", "Allow always", "Reject"]
|
|
600
866
|
: ["Allow once", "Reject"];
|
|
601
867
|
};
|
|
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) => {
|
|
868
|
+
const modalKeyNameFromSequence = (sequence) => {
|
|
636
869
|
const name = keyNameFromSequence(sequence);
|
|
637
870
|
if (name)
|
|
638
871
|
return name;
|
|
@@ -652,12 +885,16 @@ function OpenTuiApp(props) {
|
|
|
652
885
|
const rejectPendingPlan = (plan) => {
|
|
653
886
|
setPendingPlan(undefined);
|
|
654
887
|
setApprovalOptionIdx(0);
|
|
888
|
+
forceApprovalUI();
|
|
889
|
+
restorePromptAfterModal();
|
|
655
890
|
plan.resolve({ action: "reject", reason: "Rejected by user." });
|
|
656
891
|
};
|
|
657
892
|
const resolvePendingPlanSelection = (plan) => {
|
|
658
893
|
const sel = approvalOptionIdx();
|
|
659
894
|
setPendingPlan(undefined);
|
|
660
895
|
setApprovalOptionIdx(0);
|
|
896
|
+
forceApprovalUI();
|
|
897
|
+
restorePromptAfterModal();
|
|
661
898
|
if (sel === 0) {
|
|
662
899
|
plan.resolve({ action: "approve", plan: plan.plan });
|
|
663
900
|
}
|
|
@@ -710,6 +947,7 @@ function OpenTuiApp(props) {
|
|
|
710
947
|
setPendingApproval(undefined);
|
|
711
948
|
setApprovalOptionIdx(0);
|
|
712
949
|
forceApprovalUI();
|
|
950
|
+
restorePromptAfterModal();
|
|
713
951
|
if (choice === "Allow once") {
|
|
714
952
|
approval.resolve({ action: "approve" });
|
|
715
953
|
return true;
|
|
@@ -722,7 +960,7 @@ function OpenTuiApp(props) {
|
|
|
722
960
|
approval.resolve({ action: "reject", feedback: "Rejected by user." });
|
|
723
961
|
return true;
|
|
724
962
|
};
|
|
725
|
-
const handleApprovalNavigation = (name, preventOnly = false) => {
|
|
963
|
+
const handleApprovalNavigation = (name, preventOnly = false, shift = false) => {
|
|
726
964
|
const approval = pendingApproval();
|
|
727
965
|
if (approval) {
|
|
728
966
|
const opts = approvalOptionsFor(approval.request);
|
|
@@ -734,6 +972,10 @@ function OpenTuiApp(props) {
|
|
|
734
972
|
moveApprovalOption(1, opts.length);
|
|
735
973
|
return true;
|
|
736
974
|
}
|
|
975
|
+
if (name === "tab") {
|
|
976
|
+
moveApprovalOption(shift ? -1 : 1, opts.length);
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
737
979
|
if (name === "enter") {
|
|
738
980
|
if (!preventOnly)
|
|
739
981
|
resolveApprovalSelection();
|
|
@@ -745,6 +987,7 @@ function OpenTuiApp(props) {
|
|
|
745
987
|
setPendingApproval(undefined);
|
|
746
988
|
setApprovalOptionIdx(0);
|
|
747
989
|
forceApprovalUI();
|
|
990
|
+
restorePromptAfterModal();
|
|
748
991
|
approval.resolve({ action: "reject", feedback: "Rejected by user." });
|
|
749
992
|
}
|
|
750
993
|
return true;
|
|
@@ -760,6 +1003,10 @@ function OpenTuiApp(props) {
|
|
|
760
1003
|
moveApprovalOption(1, PLAN_OPTIONS.length);
|
|
761
1004
|
return true;
|
|
762
1005
|
}
|
|
1006
|
+
if (name === "tab") {
|
|
1007
|
+
moveApprovalOption(shift ? -1 : 1, PLAN_OPTIONS.length);
|
|
1008
|
+
return true;
|
|
1009
|
+
}
|
|
763
1010
|
if (name === "enter") {
|
|
764
1011
|
if (!preventOnly)
|
|
765
1012
|
resolvePendingPlanSelection(plan);
|
|
@@ -775,7 +1022,7 @@ function OpenTuiApp(props) {
|
|
|
775
1022
|
};
|
|
776
1023
|
const handleApprovalKey = (event) => {
|
|
777
1024
|
const name = keyNameFromEvent(event);
|
|
778
|
-
if (handleApprovalNavigation(name)) {
|
|
1025
|
+
if (handleApprovalNavigation(name, false, !!event.shift)) {
|
|
779
1026
|
event.preventDefault?.();
|
|
780
1027
|
event.stopPropagation?.();
|
|
781
1028
|
return true;
|
|
@@ -787,6 +1034,8 @@ function OpenTuiApp(props) {
|
|
|
787
1034
|
const plan = pendingPlan();
|
|
788
1035
|
syncPromptSurfaces();
|
|
789
1036
|
const prompt = activePrompt();
|
|
1037
|
+
if (approval || plan)
|
|
1038
|
+
blurInputsForModal();
|
|
790
1039
|
if (prompt) {
|
|
791
1040
|
if (approval) {
|
|
792
1041
|
const options = approvalOptionsFor(approval.request);
|
|
@@ -803,6 +1052,8 @@ function OpenTuiApp(props) {
|
|
|
803
1052
|
}
|
|
804
1053
|
redrawDock();
|
|
805
1054
|
redrawApprovalPanel();
|
|
1055
|
+
if (approval || plan)
|
|
1056
|
+
focusApprovalPanel();
|
|
806
1057
|
redrawTranscript();
|
|
807
1058
|
};
|
|
808
1059
|
function questionStateFromRequest(request) {
|
|
@@ -851,15 +1102,67 @@ function OpenTuiApp(props) {
|
|
|
851
1102
|
questionSyncTimers.add(timer);
|
|
852
1103
|
}
|
|
853
1104
|
}
|
|
1105
|
+
function transcriptMaxScrollTop() {
|
|
1106
|
+
if (!scrollbox)
|
|
1107
|
+
return 0;
|
|
1108
|
+
return Math.max(0, scrollbox.scrollHeight - scrollbox.viewport.height);
|
|
1109
|
+
}
|
|
1110
|
+
function isTranscriptAtBottom() {
|
|
1111
|
+
if (!scrollbox)
|
|
1112
|
+
return true;
|
|
1113
|
+
return scrollbox.scrollTop >= transcriptMaxScrollTop() - 1;
|
|
1114
|
+
}
|
|
1115
|
+
function updateTranscriptScrollFollowingFromPosition() {
|
|
1116
|
+
if (!scrollbox)
|
|
1117
|
+
return;
|
|
1118
|
+
transcriptScrollFollowing = isTranscriptAtBottom();
|
|
1119
|
+
transcriptScrollInitialized = true;
|
|
1120
|
+
}
|
|
1121
|
+
function shouldFollowTranscriptBeforeUpdate() {
|
|
1122
|
+
if (!scrollbox)
|
|
1123
|
+
return transcriptScrollFollowing;
|
|
1124
|
+
if (!transcriptScrollInitialized)
|
|
1125
|
+
return true;
|
|
1126
|
+
transcriptScrollFollowing = isTranscriptAtBottom();
|
|
1127
|
+
return transcriptScrollFollowing;
|
|
1128
|
+
}
|
|
1129
|
+
function scrollTranscriptToBottom() {
|
|
1130
|
+
if (!scrollbox)
|
|
1131
|
+
return;
|
|
1132
|
+
scrollbox.scrollTo(scrollbox.scrollHeight);
|
|
1133
|
+
transcriptScrollFollowing = true;
|
|
1134
|
+
transcriptScrollInitialized = true;
|
|
1135
|
+
}
|
|
1136
|
+
function scheduleTranscriptScrollAfterUpdate(shouldFollow, delay = 50) {
|
|
1137
|
+
setTimeout(() => {
|
|
1138
|
+
if (!scrollbox)
|
|
1139
|
+
return;
|
|
1140
|
+
if (shouldFollow && transcriptScrollFollowing) {
|
|
1141
|
+
scrollTranscriptToBottom();
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
updateTranscriptScrollFollowingFromPosition();
|
|
1145
|
+
}
|
|
1146
|
+
}, delay);
|
|
1147
|
+
}
|
|
1148
|
+
function handleTranscriptMouseScroll() {
|
|
1149
|
+
setTimeout(updateTranscriptScrollFollowingFromPosition, 0);
|
|
1150
|
+
}
|
|
854
1151
|
function syncQuestionUI(focusCustom = false) {
|
|
855
1152
|
redrawQuestionPanel();
|
|
856
1153
|
syncPromptSurfaces();
|
|
1154
|
+
const question = pendingQuestion();
|
|
1155
|
+
if (question)
|
|
1156
|
+
blurInputsForModal();
|
|
857
1157
|
redrawDock();
|
|
858
1158
|
rootBox?.requestRender();
|
|
859
1159
|
scrollbox?.requestRender();
|
|
860
1160
|
if (focusCustom) {
|
|
861
1161
|
setTimeout(() => questionCustomInput?.focus(), 0);
|
|
862
1162
|
}
|
|
1163
|
+
else if (question) {
|
|
1164
|
+
focusQuestionPanel();
|
|
1165
|
+
}
|
|
863
1166
|
}
|
|
864
1167
|
function updateQuestionState(updater, focusCustom = false) {
|
|
865
1168
|
setPendingQuestion((current) => current ? updater(current) : undefined);
|
|
@@ -1095,17 +1398,49 @@ function OpenTuiApp(props) {
|
|
|
1095
1398
|
}
|
|
1096
1399
|
return false;
|
|
1097
1400
|
}
|
|
1098
|
-
function
|
|
1099
|
-
|
|
1100
|
-
|
|
1401
|
+
function activeModalKeyOwner() {
|
|
1402
|
+
if (pendingApproval() || pendingPlan())
|
|
1403
|
+
return "approval";
|
|
1404
|
+
if (pendingQuestion())
|
|
1405
|
+
return "question";
|
|
1406
|
+
if (providerDialog)
|
|
1407
|
+
return "provider";
|
|
1408
|
+
if (picker)
|
|
1409
|
+
return "picker";
|
|
1410
|
+
return undefined;
|
|
1411
|
+
}
|
|
1412
|
+
function routeModalKey(event) {
|
|
1413
|
+
const owner = activeModalKeyOwner();
|
|
1414
|
+
if (!owner)
|
|
1101
1415
|
return false;
|
|
1102
|
-
|
|
1103
|
-
|
|
1416
|
+
switch (owner) {
|
|
1417
|
+
case "approval":
|
|
1418
|
+
return handleApprovalKey(event);
|
|
1419
|
+
case "question":
|
|
1420
|
+
return handleQuestionKey(event);
|
|
1421
|
+
case "provider":
|
|
1422
|
+
return handleProviderDialogKey(event);
|
|
1423
|
+
case "picker":
|
|
1424
|
+
return handlePickerKey(event);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
function shouldModalSwallowUnhandledKey(owner) {
|
|
1428
|
+
if (owner === "approval")
|
|
1429
|
+
return true;
|
|
1430
|
+
if (owner === "question") {
|
|
1431
|
+
const state = pendingQuestion();
|
|
1432
|
+
return !state?.editing || isQuestionConfirmTab(state);
|
|
1433
|
+
}
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
function routeModalRawSequence(sequence) {
|
|
1437
|
+
const owner = activeModalKeyOwner();
|
|
1438
|
+
if (!owner)
|
|
1104
1439
|
return false;
|
|
1105
|
-
|
|
1440
|
+
const name = modalKeyNameFromSequence(sequence);
|
|
1441
|
+
if (!name)
|
|
1106
1442
|
return false;
|
|
1107
|
-
|
|
1108
|
-
return handleQuestionKey({
|
|
1443
|
+
const handled = routeModalKey({
|
|
1109
1444
|
name,
|
|
1110
1445
|
key: name,
|
|
1111
1446
|
input: sequence,
|
|
@@ -1115,11 +1450,13 @@ function OpenTuiApp(props) {
|
|
|
1115
1450
|
preventDefault() { },
|
|
1116
1451
|
stopPropagation() { },
|
|
1117
1452
|
});
|
|
1453
|
+
return handled || shouldModalSwallowUnhandledKey(owner);
|
|
1118
1454
|
}
|
|
1119
1455
|
const installInteractiveHandlers = () => {
|
|
1120
1456
|
if (props.options.planHandlerRef) {
|
|
1121
1457
|
props.options.planHandlerRef.current = (plan) => new Promise((resolve) => {
|
|
1122
1458
|
setPendingPlan({ plan, resolve });
|
|
1459
|
+
blurInputsForModal();
|
|
1123
1460
|
forceApprovalUI();
|
|
1124
1461
|
});
|
|
1125
1462
|
}
|
|
@@ -1129,6 +1466,7 @@ function OpenTuiApp(props) {
|
|
|
1129
1466
|
picker = undefined;
|
|
1130
1467
|
providerDialog = undefined;
|
|
1131
1468
|
setPendingApproval({ request, resolve });
|
|
1469
|
+
blurInputsForModal();
|
|
1132
1470
|
forceApprovalUI();
|
|
1133
1471
|
});
|
|
1134
1472
|
}
|
|
@@ -1162,8 +1500,7 @@ function OpenTuiApp(props) {
|
|
|
1162
1500
|
onCleanup(unsubscribeQuestion);
|
|
1163
1501
|
syncFirstPendingQuestion();
|
|
1164
1502
|
}
|
|
1165
|
-
props.
|
|
1166
|
-
props.setRawQuestionHandler?.(handleQuestionRawSequence);
|
|
1503
|
+
props.setRawGlobalKeyHandler?.(routeGlobalRawSequence);
|
|
1167
1504
|
const unsubscribeLsp = lspService.onStatusChange(() => {
|
|
1168
1505
|
syncSidebarLsp();
|
|
1169
1506
|
});
|
|
@@ -1172,12 +1509,11 @@ function OpenTuiApp(props) {
|
|
|
1172
1509
|
refreshGitSidebar();
|
|
1173
1510
|
setTimeout(() => {
|
|
1174
1511
|
activePrompt()?.focus();
|
|
1175
|
-
|
|
1512
|
+
scrollTranscriptToBottom();
|
|
1176
1513
|
}, 25);
|
|
1177
1514
|
});
|
|
1178
1515
|
onCleanup(() => {
|
|
1179
|
-
props.
|
|
1180
|
-
props.setRawQuestionHandler?.(undefined);
|
|
1516
|
+
props.setRawGlobalKeyHandler?.(undefined);
|
|
1181
1517
|
if (sidebarLspSyncTimer)
|
|
1182
1518
|
clearInterval(sidebarLspSyncTimer);
|
|
1183
1519
|
for (const timer of questionSyncTimers)
|
|
@@ -1189,66 +1525,38 @@ function OpenTuiApp(props) {
|
|
|
1189
1525
|
if (props.options.approvalHandlerRef)
|
|
1190
1526
|
props.options.approvalHandlerRef.current = undefined;
|
|
1191
1527
|
});
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1528
|
+
let lastCopiedSelection = "";
|
|
1529
|
+
let lastCopiedSelectionAt = 0;
|
|
1530
|
+
let selectionCopySerial = 0;
|
|
1531
|
+
let rawSelectionStart;
|
|
1532
|
+
useSelectionHandler((selection) => {
|
|
1533
|
+
if (selection.isDragging)
|
|
1196
1534
|
return;
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
event.preventDefault?.();
|
|
1203
|
-
return;
|
|
1204
|
-
}
|
|
1205
|
-
if (handleApprovalKey(event))
|
|
1206
|
-
return;
|
|
1207
|
-
if (handleQuestionKey(event))
|
|
1208
|
-
return;
|
|
1209
|
-
if (handleProviderDialogKey(event))
|
|
1535
|
+
const selectedText = getOpenTuiSelectionText(selection);
|
|
1536
|
+
void copySelectionText(selectedText);
|
|
1537
|
+
});
|
|
1538
|
+
function handleRawMouseSelection(event) {
|
|
1539
|
+
if (event.button !== 0)
|
|
1210
1540
|
return;
|
|
1211
|
-
if (
|
|
1212
|
-
|
|
1213
|
-
const plan = pendingPlan();
|
|
1214
|
-
if (plan) {
|
|
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?.();
|
|
1541
|
+
if (event.type === "down") {
|
|
1542
|
+
rawSelectionStart = { x: event.x, y: event.y };
|
|
1243
1543
|
return;
|
|
1244
1544
|
}
|
|
1245
|
-
if (
|
|
1545
|
+
if (event.type !== "up")
|
|
1246
1546
|
return;
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1547
|
+
const start = rawSelectionStart;
|
|
1548
|
+
rawSelectionStart = undefined;
|
|
1549
|
+
if (!start || (start.x === event.x && start.y === event.y))
|
|
1250
1550
|
return;
|
|
1251
|
-
|
|
1551
|
+
const selection = renderer.getSelection();
|
|
1552
|
+
if (!selection || selection.isDragging)
|
|
1553
|
+
return;
|
|
1554
|
+
void copySelectionText(getOpenTuiSelectionText(selection));
|
|
1555
|
+
}
|
|
1556
|
+
props.setRawMouseSelectionHandler?.(handleRawMouseSelection);
|
|
1557
|
+
onCleanup(() => props.setRawMouseSelectionHandler?.(undefined));
|
|
1558
|
+
useKeyboard((event) => {
|
|
1559
|
+
routeGlobalKeyEvent(event);
|
|
1252
1560
|
}, {});
|
|
1253
1561
|
function currentTranscriptMessages(extra) {
|
|
1254
1562
|
return compactDisplayMessages(extra ? [...displayMessages, extra] : displayMessages);
|
|
@@ -1298,6 +1606,74 @@ function OpenTuiApp(props) {
|
|
|
1298
1606
|
// Keep the agent loop alive even if a renderable is already gone.
|
|
1299
1607
|
}
|
|
1300
1608
|
}
|
|
1609
|
+
function beginAgentRun() {
|
|
1610
|
+
const run = { id: ++nextRunId, abortController: new AbortController() };
|
|
1611
|
+
activeRun = run;
|
|
1612
|
+
setRunningState(true);
|
|
1613
|
+
return run;
|
|
1614
|
+
}
|
|
1615
|
+
function finishAgentRun(run) {
|
|
1616
|
+
if (activeRun?.id === run.id)
|
|
1617
|
+
activeRun = undefined;
|
|
1618
|
+
setRunningState(false);
|
|
1619
|
+
}
|
|
1620
|
+
function cancelActiveAgentRun() {
|
|
1621
|
+
if (!activeRun || activeRun.abortController.signal.aborted)
|
|
1622
|
+
return false;
|
|
1623
|
+
activeRun.abortController.abort(new AgentAbortError("Agent run cancelled by user."));
|
|
1624
|
+
setNotice("Agent run cancelled");
|
|
1625
|
+
redrawDock();
|
|
1626
|
+
return true;
|
|
1627
|
+
}
|
|
1628
|
+
function preventGlobalKey(event) {
|
|
1629
|
+
event.preventDefault?.();
|
|
1630
|
+
event.stopPropagation?.();
|
|
1631
|
+
}
|
|
1632
|
+
function routeRunningCancel(name, event) {
|
|
1633
|
+
if (name !== "escape")
|
|
1634
|
+
return false;
|
|
1635
|
+
if (!cancelActiveAgentRun())
|
|
1636
|
+
return false;
|
|
1637
|
+
if (event)
|
|
1638
|
+
preventGlobalKey(event);
|
|
1639
|
+
return true;
|
|
1640
|
+
}
|
|
1641
|
+
function routeGlobalRawSequence(sequence) {
|
|
1642
|
+
const name = keyNameFromSequence(sequence);
|
|
1643
|
+
if (routeRunningCancel(name))
|
|
1644
|
+
return true;
|
|
1645
|
+
if (routeModalRawSequence(sequence))
|
|
1646
|
+
return true;
|
|
1647
|
+
if (cycleModeFromRawSequence(sequence))
|
|
1648
|
+
return true;
|
|
1649
|
+
return false;
|
|
1650
|
+
}
|
|
1651
|
+
function routeGlobalKeyEvent(event) {
|
|
1652
|
+
const name = keyNameFromEvent(event);
|
|
1653
|
+
if (event.ctrl && name === "c") {
|
|
1654
|
+
void requestExit();
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
if (routeRunningCancel(name, event))
|
|
1658
|
+
return true;
|
|
1659
|
+
// Ctrl+Shift+M opens the MCP reconnect picker. Shift is required because
|
|
1660
|
+
// bare Ctrl+M is Enter on most terminals (historical TTY mapping).
|
|
1661
|
+
if (event.ctrl && event.shift && name === "m") {
|
|
1662
|
+
openMcpReconnectPicker();
|
|
1663
|
+
event.preventDefault?.();
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
if (routeModalKey(event))
|
|
1667
|
+
return true;
|
|
1668
|
+
if (cycleModeFromKey(event))
|
|
1669
|
+
return true;
|
|
1670
|
+
if (event.ctrl && name === "p" && !picker && !isRunning()) {
|
|
1671
|
+
openCommandPalette();
|
|
1672
|
+
event.preventDefault?.();
|
|
1673
|
+
return true;
|
|
1674
|
+
}
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1301
1677
|
function transcriptOptions() {
|
|
1302
1678
|
return {
|
|
1303
1679
|
cwd: props.args.cwd,
|
|
@@ -1371,28 +1747,22 @@ function OpenTuiApp(props) {
|
|
|
1371
1747
|
}, PROMPT_SCANNER_INTERVAL_MS);
|
|
1372
1748
|
}
|
|
1373
1749
|
function redrawTranscript(extra, baseMessages = displayMessages) {
|
|
1750
|
+
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
1374
1751
|
streamingDisplay = extra;
|
|
1375
1752
|
const nextMessages = compactDisplayMessages(extra ? [...baseMessages, extra] : baseMessages);
|
|
1376
1753
|
syncSessionMessages(nextMessages);
|
|
1377
1754
|
rootBox?.requestRender();
|
|
1378
1755
|
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);
|
|
1756
|
+
scheduleTranscriptScrollAfterUpdate(shouldFollow);
|
|
1388
1757
|
}
|
|
1389
1758
|
createEffect(() => {
|
|
1759
|
+
const shouldFollow = shouldFollowTranscriptBeforeUpdate();
|
|
1390
1760
|
dimensions();
|
|
1391
1761
|
sessionActive();
|
|
1392
1762
|
syncSidebarChrome();
|
|
1393
1763
|
redrawQuestionPanel();
|
|
1394
1764
|
scrollbox?.requestRender();
|
|
1395
|
-
|
|
1765
|
+
scheduleTranscriptScrollAfterUpdate(shouldFollow);
|
|
1396
1766
|
});
|
|
1397
1767
|
function redrawDock() {
|
|
1398
1768
|
if (dock) {
|
|
@@ -1479,6 +1849,8 @@ function OpenTuiApp(props) {
|
|
|
1479
1849
|
return buildProviderConnectItems();
|
|
1480
1850
|
if (step === "auth")
|
|
1481
1851
|
return providerId ? buildPickerItems("provider-auth", providerId) : [];
|
|
1852
|
+
if (step === "skills")
|
|
1853
|
+
return buildSkillItems();
|
|
1482
1854
|
if (step === "models") {
|
|
1483
1855
|
const modelItems = buildPickerItems("model", providerId);
|
|
1484
1856
|
if (modelItems.length || providerId)
|
|
@@ -1634,17 +2006,19 @@ function OpenTuiApp(props) {
|
|
|
1634
2006
|
gutter.fg = active ? activeText : providerDialogGutterColor(row.item.gutter ?? (isCurrentModelItem(row.item) ? "●" : undefined));
|
|
1635
2007
|
}
|
|
1636
2008
|
if (label) {
|
|
1637
|
-
label.content = truncate(row.item.label,
|
|
2009
|
+
label.content = truncate(row.item.label, providerDialogLabelWidth(state));
|
|
1638
2010
|
label.fg = active ? activeText : isCurrentModelItem(row.item) ? theme.primary : theme.text;
|
|
1639
2011
|
}
|
|
1640
2012
|
if (detail) {
|
|
1641
2013
|
const detailText = state.query.trim() && state.step === "models"
|
|
1642
2014
|
? row.item.category ?? row.item.detail ?? ""
|
|
1643
2015
|
: row.item.detail ?? "";
|
|
1644
|
-
detail.
|
|
2016
|
+
detail.width = providerDialogDetailWidth(state);
|
|
2017
|
+
detail.content = truncate(detailText, providerDialogDetailWidth(state));
|
|
1645
2018
|
detail.fg = active ? activeText : theme.textMuted;
|
|
1646
2019
|
}
|
|
1647
2020
|
if (footer) {
|
|
2021
|
+
footer.width = providerDialogFooterWidth(state);
|
|
1648
2022
|
footer.content = row.item.footer ?? "";
|
|
1649
2023
|
footer.fg = active ? activeText : theme.textMuted;
|
|
1650
2024
|
}
|
|
@@ -1659,6 +2033,8 @@ function OpenTuiApp(props) {
|
|
|
1659
2033
|
function providerDialogTitleFor(state) {
|
|
1660
2034
|
if (state.step === "providers")
|
|
1661
2035
|
return "Connect a provider";
|
|
2036
|
+
if (state.step === "skills")
|
|
2037
|
+
return "Select skill";
|
|
1662
2038
|
const provider = providerDisplayName(state.providerId);
|
|
1663
2039
|
if (state.step === "auth")
|
|
1664
2040
|
return `${provider} auth method`;
|
|
@@ -1677,6 +2053,8 @@ function OpenTuiApp(props) {
|
|
|
1677
2053
|
const connect = state.providerId ? "" : " · ctrl+o providers";
|
|
1678
2054
|
return `↑/↓ move · enter select · esc close${connect}${count}`;
|
|
1679
2055
|
}
|
|
2056
|
+
if (state.step === "skills")
|
|
2057
|
+
return `↑/↓ move · enter insert · esc close${count}`;
|
|
1680
2058
|
const escLabel = state.step === "providers" ? "esc close" : "esc back";
|
|
1681
2059
|
return `↑/↓ move · enter select · ${escLabel}${count}`;
|
|
1682
2060
|
}
|
|
@@ -1689,8 +2067,14 @@ function OpenTuiApp(props) {
|
|
|
1689
2067
|
return theme.warning;
|
|
1690
2068
|
return theme.textMuted;
|
|
1691
2069
|
}
|
|
1692
|
-
function
|
|
1693
|
-
return
|
|
2070
|
+
function providerDialogLabelWidth(state) {
|
|
2071
|
+
return state.step === "skills" ? 22 : 37;
|
|
2072
|
+
}
|
|
2073
|
+
function providerDialogDetailWidth(state) {
|
|
2074
|
+
return state.step === "skills" ? 26 : 16;
|
|
2075
|
+
}
|
|
2076
|
+
function providerDialogFooterWidth(state) {
|
|
2077
|
+
return state.step === "skills" ? 9 : 8;
|
|
1694
2078
|
}
|
|
1695
2079
|
function isCurrentModelItem(item) {
|
|
1696
2080
|
return item.value === props.agent.model || item.detail?.includes("current");
|
|
@@ -1755,7 +2139,7 @@ function OpenTuiApp(props) {
|
|
|
1755
2139
|
else if (state.step === "key") {
|
|
1756
2140
|
openProviderDialog(state.providerId && registry.supportsOAuth(state.providerId) ? "auth" : "providers", state.providerId);
|
|
1757
2141
|
}
|
|
1758
|
-
else if (state.step === "models") {
|
|
2142
|
+
else if (state.step === "models" || state.step === "skills") {
|
|
1759
2143
|
closeProviderDialog();
|
|
1760
2144
|
}
|
|
1761
2145
|
else {
|
|
@@ -1885,6 +2269,11 @@ function OpenTuiApp(props) {
|
|
|
1885
2269
|
}
|
|
1886
2270
|
closeProviderDialog();
|
|
1887
2271
|
await executeSlash(item.command);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
if (state.step === "skills") {
|
|
2275
|
+
closeProviderDialog();
|
|
2276
|
+
insertSkillPrompt(item.value);
|
|
1888
2277
|
}
|
|
1889
2278
|
}
|
|
1890
2279
|
function ensureProviderConfiguredForKey(providerId) {
|
|
@@ -2262,6 +2651,8 @@ function OpenTuiApp(props) {
|
|
|
2262
2651
|
const clearMessages = () => {
|
|
2263
2652
|
displayMessages = [];
|
|
2264
2653
|
streamingDisplay = undefined;
|
|
2654
|
+
promptHistory = [];
|
|
2655
|
+
resetPromptHistoryBrowse();
|
|
2265
2656
|
redrawTranscript(undefined, []);
|
|
2266
2657
|
};
|
|
2267
2658
|
async function submitPrompt() {
|
|
@@ -2275,9 +2666,7 @@ function OpenTuiApp(props) {
|
|
|
2275
2666
|
}
|
|
2276
2667
|
const plan = pendingPlan();
|
|
2277
2668
|
if (plan) {
|
|
2278
|
-
|
|
2279
|
-
setApprovalOptionIdx(0);
|
|
2280
|
-
plan.resolve({ action: "approve", plan: plan.plan });
|
|
2669
|
+
resolvePendingPlanSelection(plan);
|
|
2281
2670
|
return;
|
|
2282
2671
|
}
|
|
2283
2672
|
if (isRunning())
|
|
@@ -2294,6 +2683,7 @@ function OpenTuiApp(props) {
|
|
|
2294
2683
|
return;
|
|
2295
2684
|
activePrompt()?.clear();
|
|
2296
2685
|
promptText = "";
|
|
2686
|
+
resetPromptHistoryBrowse();
|
|
2297
2687
|
if (picker?.kind === "key") {
|
|
2298
2688
|
const providerId = picker.providerId;
|
|
2299
2689
|
const after = picker.after;
|
|
@@ -2310,9 +2700,15 @@ function OpenTuiApp(props) {
|
|
|
2310
2700
|
}
|
|
2311
2701
|
if (input.startsWith("/") && !/\s/.test(input)) {
|
|
2312
2702
|
const query = input.slice(1).toLowerCase();
|
|
2313
|
-
const matches =
|
|
2703
|
+
const matches = slashCandidates().filter((command) => command.name.toLowerCase().startsWith(query));
|
|
2314
2704
|
if (matches.length === 1) {
|
|
2315
|
-
|
|
2705
|
+
const match = matches[0];
|
|
2706
|
+
if (match.source === "skill") {
|
|
2707
|
+
insertSkillPrompt(match.name);
|
|
2708
|
+
}
|
|
2709
|
+
else {
|
|
2710
|
+
await executeSlash(`/${match.name}`);
|
|
2711
|
+
}
|
|
2316
2712
|
return;
|
|
2317
2713
|
}
|
|
2318
2714
|
if (matches.length > 1) {
|
|
@@ -2332,6 +2728,10 @@ function OpenTuiApp(props) {
|
|
|
2332
2728
|
function onPromptContentChange(value) {
|
|
2333
2729
|
const nextValue = typeof value === "string" ? value : readPromptText();
|
|
2334
2730
|
promptText = nextValue;
|
|
2731
|
+
if (promptHistoryIndex !== undefined
|
|
2732
|
+
&& nextValue !== (promptHistory[promptHistoryIndex] ?? "")) {
|
|
2733
|
+
resetPromptHistoryBrowse();
|
|
2734
|
+
}
|
|
2335
2735
|
if (providerDialog)
|
|
2336
2736
|
return;
|
|
2337
2737
|
if (picker?.kind === "key")
|
|
@@ -2432,25 +2832,39 @@ function OpenTuiApp(props) {
|
|
|
2432
2832
|
activePrompt()?.focus();
|
|
2433
2833
|
redrawDock();
|
|
2434
2834
|
}
|
|
2435
|
-
function
|
|
2436
|
-
const
|
|
2437
|
-
|
|
2835
|
+
function slashCandidates() {
|
|
2836
|
+
const skillCommands = skills.summaries().map((skill) => ({
|
|
2837
|
+
name: skill.name,
|
|
2838
|
+
description: skill.description,
|
|
2839
|
+
source: "skill",
|
|
2840
|
+
sourceLabel: skill.source,
|
|
2841
|
+
}));
|
|
2842
|
+
return [
|
|
2438
2843
|
...LOCAL_SLASH_COMMANDS.map((c) => ({ ...c, source: "builtin" })),
|
|
2844
|
+
...skillCommands,
|
|
2439
2845
|
...slashRegistry.list(),
|
|
2440
2846
|
];
|
|
2847
|
+
}
|
|
2848
|
+
function buildSlashItems(query = "") {
|
|
2849
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
2850
|
+
const commands = slashCandidates();
|
|
2441
2851
|
const matches = slashCommandMatches(commands, normalizedQuery);
|
|
2442
2852
|
return matches.map(({ command }) => {
|
|
2443
2853
|
const isMcp = command.source === "mcp";
|
|
2444
|
-
const
|
|
2854
|
+
const isSkill = command.source === "skill";
|
|
2855
|
+
const badge = isMcp ? " :mcp" : isSkill ? " :skill" : "";
|
|
2445
2856
|
const label = `/${command.name}${badge}`;
|
|
2446
2857
|
const detail = isMcp && command.sourceLabel
|
|
2447
2858
|
? `[${command.sourceLabel}] ${command.description}`
|
|
2448
|
-
: command.
|
|
2859
|
+
: isSkill && command.sourceLabel
|
|
2860
|
+
? `[${command.sourceLabel}] ${command.description}`
|
|
2861
|
+
: command.description;
|
|
2449
2862
|
return {
|
|
2450
2863
|
label,
|
|
2451
2864
|
detail,
|
|
2452
2865
|
value: command.name,
|
|
2453
2866
|
command: `/${command.name}`,
|
|
2867
|
+
action: isSkill ? "insert-skill" : undefined,
|
|
2454
2868
|
};
|
|
2455
2869
|
});
|
|
2456
2870
|
}
|
|
@@ -2575,6 +2989,12 @@ function OpenTuiApp(props) {
|
|
|
2575
2989
|
prompt.cursorOffset = next.length;
|
|
2576
2990
|
closePicker();
|
|
2577
2991
|
}
|
|
2992
|
+
function insertSkillPrompt(skillName) {
|
|
2993
|
+
closePicker();
|
|
2994
|
+
resetPromptHistoryBrowse();
|
|
2995
|
+
setPromptText(`/${skillName} `);
|
|
2996
|
+
redrawDock();
|
|
2997
|
+
}
|
|
2578
2998
|
async function handleInput(input) {
|
|
2579
2999
|
setNotice("");
|
|
2580
3000
|
if (input.startsWith("/")) {
|
|
@@ -2635,6 +3055,7 @@ function OpenTuiApp(props) {
|
|
|
2635
3055
|
return true;
|
|
2636
3056
|
if (props.agent.mode !== mode())
|
|
2637
3057
|
setMode(props.agent.mode);
|
|
3058
|
+
syncTodosFromAgent();
|
|
2638
3059
|
syncModelChrome();
|
|
2639
3060
|
syncModeChrome();
|
|
2640
3061
|
if (uiDisposed)
|
|
@@ -2671,6 +3092,10 @@ function OpenTuiApp(props) {
|
|
|
2671
3092
|
openProviderDialog(kind === "provider-auth" ? "auth" : "providers", providerId);
|
|
2672
3093
|
return;
|
|
2673
3094
|
}
|
|
3095
|
+
if (kind === "skill") {
|
|
3096
|
+
openProviderDialog("skills");
|
|
3097
|
+
return;
|
|
3098
|
+
}
|
|
2674
3099
|
if (kind === "key") {
|
|
2675
3100
|
picker = {
|
|
2676
3101
|
kind: "key",
|
|
@@ -2710,6 +3135,10 @@ function OpenTuiApp(props) {
|
|
|
2710
3135
|
applyFileSuggestion(item.value);
|
|
2711
3136
|
return;
|
|
2712
3137
|
}
|
|
3138
|
+
if (item.action === "insert-skill" || (picker?.kind === "select" && picker.mode === "skill")) {
|
|
3139
|
+
insertSkillPrompt(item.value);
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
2713
3142
|
if (picker?.kind === "select" && picker.mode === "slash") {
|
|
2714
3143
|
activePrompt()?.clear();
|
|
2715
3144
|
closePicker();
|
|
@@ -2796,6 +3225,8 @@ function OpenTuiApp(props) {
|
|
|
2796
3225
|
return [];
|
|
2797
3226
|
if (kind === "mcp-reconnect")
|
|
2798
3227
|
return buildMcpReconnectItems();
|
|
3228
|
+
if (kind === "skill")
|
|
3229
|
+
return buildSkillItems();
|
|
2799
3230
|
if (kind === "model") {
|
|
2800
3231
|
const items = [];
|
|
2801
3232
|
for (const provider of registry.getEnabled()) {
|
|
@@ -2909,6 +3340,25 @@ function OpenTuiApp(props) {
|
|
|
2909
3340
|
command: `/logout ${provider.id}`,
|
|
2910
3341
|
}));
|
|
2911
3342
|
}
|
|
3343
|
+
function buildSkillItems() {
|
|
3344
|
+
return skills.summaries().map((skill) => {
|
|
3345
|
+
const tags = skill.tags && skill.tags.length > 0 ? ` · ${skill.tags.join(", ")}` : "";
|
|
3346
|
+
const category = skill.source === "project"
|
|
3347
|
+
? "Project skills"
|
|
3348
|
+
: skill.source === "configured"
|
|
3349
|
+
? "Configured skills"
|
|
3350
|
+
: "User skills";
|
|
3351
|
+
return {
|
|
3352
|
+
label: skill.name,
|
|
3353
|
+
detail: `${skill.description}${tags}`,
|
|
3354
|
+
value: skill.name,
|
|
3355
|
+
command: `/${skill.name}`,
|
|
3356
|
+
action: "insert-skill",
|
|
3357
|
+
category,
|
|
3358
|
+
footer: skill.source,
|
|
3359
|
+
};
|
|
3360
|
+
});
|
|
3361
|
+
}
|
|
2912
3362
|
function buildProviderConnectItems() {
|
|
2913
3363
|
const configuredProviders = registry.getConfigured();
|
|
2914
3364
|
const configured = new Map(configuredProviders.map((provider) => [provider.id, provider]));
|
|
@@ -3002,17 +3452,19 @@ function OpenTuiApp(props) {
|
|
|
3002
3452
|
addMessage("error", "No model selected. Use /model after /login or provider setup.");
|
|
3003
3453
|
return;
|
|
3004
3454
|
}
|
|
3455
|
+
rememberPromptHistory(displayInput);
|
|
3005
3456
|
const nextMessages = [...displayMessages, { role: "user", content: displayInput }];
|
|
3006
3457
|
displayMessages = nextMessages;
|
|
3007
3458
|
streamingDisplay = undefined;
|
|
3008
3459
|
redrawTranscript(undefined, nextMessages);
|
|
3009
|
-
|
|
3460
|
+
const run = beginAgentRun();
|
|
3010
3461
|
let assistantContent = "";
|
|
3011
3462
|
let assistantReasoning = "";
|
|
3012
3463
|
const toolCalls = [];
|
|
3013
3464
|
let runError;
|
|
3465
|
+
let runCancelled = false;
|
|
3014
3466
|
try {
|
|
3015
|
-
for await (const event of props.agent.run(actualInput, props.args.cwd)) {
|
|
3467
|
+
for await (const event of props.agent.run(actualInput, props.args.cwd, { abortSignal: run.abortController.signal })) {
|
|
3016
3468
|
if (event.type === "turn_start") {
|
|
3017
3469
|
assistantContent = "";
|
|
3018
3470
|
assistantReasoning = "";
|
|
@@ -3082,6 +3534,7 @@ function OpenTuiApp(props) {
|
|
|
3082
3534
|
}
|
|
3083
3535
|
else if (event.type === "todos_updated") {
|
|
3084
3536
|
setTodos(event.todos);
|
|
3537
|
+
syncSidebarTodos(event.todos);
|
|
3085
3538
|
bumpSidebar();
|
|
3086
3539
|
}
|
|
3087
3540
|
else if (event.type === "mode_changed") {
|
|
@@ -3092,6 +3545,7 @@ function OpenTuiApp(props) {
|
|
|
3092
3545
|
else if (event.type === "turn_end") {
|
|
3093
3546
|
if (event.usage) {
|
|
3094
3547
|
setSidebarUsage((current) => ({
|
|
3548
|
+
contextTokens: event.usage.promptTokens || current.contextTokens,
|
|
3095
3549
|
promptTokens: current.promptTokens + event.usage.promptTokens,
|
|
3096
3550
|
completionTokens: current.completionTokens + event.usage.completionTokens,
|
|
3097
3551
|
promptCacheHitTokens: current.promptCacheHitTokens + (event.usage.promptCacheHitTokens ?? 0),
|
|
@@ -3121,13 +3575,16 @@ function OpenTuiApp(props) {
|
|
|
3121
3575
|
}
|
|
3122
3576
|
}
|
|
3123
3577
|
catch (error) {
|
|
3124
|
-
|
|
3578
|
+
runCancelled = error instanceof AgentAbortError || run.abortController.signal.aborted || error?.name === "AbortError";
|
|
3579
|
+
if (!runCancelled) {
|
|
3580
|
+
runError = error?.message || String(error);
|
|
3581
|
+
}
|
|
3125
3582
|
}
|
|
3126
3583
|
finally {
|
|
3127
3584
|
pendingApprovalRef = undefined;
|
|
3128
3585
|
setPendingApproval(undefined);
|
|
3129
3586
|
setApprovalOptionIdx(0);
|
|
3130
|
-
|
|
3587
|
+
finishAgentRun(run);
|
|
3131
3588
|
streamingDisplay = undefined;
|
|
3132
3589
|
if (runError) {
|
|
3133
3590
|
const errorMessage = runError;
|
|
@@ -3135,6 +3592,11 @@ function OpenTuiApp(props) {
|
|
|
3135
3592
|
displayMessages = nextMessages;
|
|
3136
3593
|
redrawTranscript(undefined, nextMessages);
|
|
3137
3594
|
}
|
|
3595
|
+
else if (runCancelled) {
|
|
3596
|
+
if (!notice())
|
|
3597
|
+
setNotice("Agent run cancelled");
|
|
3598
|
+
redrawTranscript();
|
|
3599
|
+
}
|
|
3138
3600
|
else {
|
|
3139
3601
|
redrawTranscript();
|
|
3140
3602
|
}
|
|
@@ -3145,24 +3607,20 @@ function OpenTuiApp(props) {
|
|
|
3145
3607
|
}
|
|
3146
3608
|
}
|
|
3147
3609
|
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?.();
|
|
3610
|
+
if (routeRunningCancel(keyNameFromEvent(event), event))
|
|
3162
3611
|
return true;
|
|
3612
|
+
const modalOwner = activeModalKeyOwner();
|
|
3613
|
+
if (modalOwner) {
|
|
3614
|
+
if (routeModalKey(event) || shouldModalSwallowUnhandledKey(modalOwner)) {
|
|
3615
|
+
event.preventDefault?.();
|
|
3616
|
+
event.stopPropagation?.();
|
|
3617
|
+
return true;
|
|
3618
|
+
}
|
|
3163
3619
|
}
|
|
3164
3620
|
if (cycleModeFromKey(event))
|
|
3165
3621
|
return true;
|
|
3622
|
+
if (handlePromptHistoryKey(event))
|
|
3623
|
+
return true;
|
|
3166
3624
|
return false;
|
|
3167
3625
|
}
|
|
3168
3626
|
function renderComposer() {
|
|
@@ -3267,6 +3725,8 @@ function OpenTuiApp(props) {
|
|
|
3267
3725
|
redrawQuestionPanel();
|
|
3268
3726
|
},
|
|
3269
3727
|
visible: false,
|
|
3728
|
+
focusable: true,
|
|
3729
|
+
onKeyDown: handleQuestionKey,
|
|
3270
3730
|
position: "absolute",
|
|
3271
3731
|
left: 2,
|
|
3272
3732
|
right: 2,
|
|
@@ -3649,7 +4109,6 @@ function OpenTuiApp(props) {
|
|
|
3649
4109
|
const context = sidebarContextState();
|
|
3650
4110
|
const mcpStates = sidebarMcpStates();
|
|
3651
4111
|
const files = gitState().files;
|
|
3652
|
-
const activeTodos = todos().filter((todo) => todo.status !== "completed");
|
|
3653
4112
|
return h("box", {
|
|
3654
4113
|
ref: (ref) => {
|
|
3655
4114
|
sidebarShell = ref;
|
|
@@ -3680,7 +4139,7 @@ function OpenTuiApp(props) {
|
|
|
3680
4139
|
flexShrink: 0,
|
|
3681
4140
|
ref: (ref) => {
|
|
3682
4141
|
sidebarGaugeLabelText = ref;
|
|
3683
|
-
ref.content = buildGaugeLabel(context.percent);
|
|
4142
|
+
ref.content = buildGaugeLabel(context.percent, context.remainingTokens);
|
|
3684
4143
|
},
|
|
3685
4144
|
}),
|
|
3686
4145
|
h("text", {
|
|
@@ -3735,7 +4194,7 @@ function OpenTuiApp(props) {
|
|
|
3735
4194
|
? renderSidebarSection("Compactions", [
|
|
3736
4195
|
h("text", { fg: theme.info, wrapMode: "word" }, `${currentTranscriptMessages().filter((m) => m.syntheticKind === "ui_compact_card").length} in this session`),
|
|
3737
4196
|
])
|
|
3738
|
-
: null, renderSidebarMcp(mcpStates), renderSidebarLsp(),
|
|
4197
|
+
: null, renderSidebarMcp(mcpStates), renderSidebarLsp(), renderSidebarTodos(todos()), renderSidebarFiles(files))),
|
|
3739
4198
|
renderSidebarFooter(),
|
|
3740
4199
|
]);
|
|
3741
4200
|
}
|
|
@@ -3820,11 +4279,37 @@ function OpenTuiApp(props) {
|
|
|
3820
4279
|
}),
|
|
3821
4280
|
]);
|
|
3822
4281
|
}
|
|
3823
|
-
function renderSidebarTodos(
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
4282
|
+
function renderSidebarTodos(todos) {
|
|
4283
|
+
const visible = todos.slice(0, 8);
|
|
4284
|
+
return h("box", {
|
|
4285
|
+
flexDirection: "column",
|
|
4286
|
+
flexShrink: 0,
|
|
4287
|
+
visible: visible.length > 0,
|
|
4288
|
+
ref: (ref) => {
|
|
4289
|
+
sidebarTodoSection = ref;
|
|
4290
|
+
syncSidebarTodos();
|
|
4291
|
+
},
|
|
4292
|
+
}, h("text", { fg: theme.text }, "Todo"), ...Array.from({ length: 8 }, (_, index) => {
|
|
4293
|
+
const todo = visible[index];
|
|
4294
|
+
const completed = todo?.status === "completed";
|
|
4295
|
+
const inProgress = todo?.status === "in_progress";
|
|
4296
|
+
const labelText = todo
|
|
4297
|
+
? (inProgress ? (todo.activeForm || todo.content) : todo.content)
|
|
4298
|
+
: "";
|
|
4299
|
+
return h("box", {
|
|
4300
|
+
flexDirection: "row",
|
|
4301
|
+
gap: 1,
|
|
4302
|
+
visible: !!todo,
|
|
4303
|
+
ref: (ref) => { sidebarTodoRows[index] = ref; },
|
|
4304
|
+
}, h("text", {
|
|
4305
|
+
fg: completed ? theme.success : inProgress ? theme.warning : theme.textMuted,
|
|
4306
|
+
flexShrink: 0,
|
|
4307
|
+
ref: (ref) => { sidebarTodoMarkers[index] = ref; },
|
|
4308
|
+
}, completed ? "✓" : inProgress ? "◉" : "○"), h("text", {
|
|
4309
|
+
fg: completed ? theme.success : inProgress ? theme.warning : theme.textMuted,
|
|
4310
|
+
wrapMode: "word",
|
|
4311
|
+
ref: (ref) => { sidebarTodoLabels[index] = ref; },
|
|
4312
|
+
}, labelText));
|
|
3828
4313
|
}));
|
|
3829
4314
|
}
|
|
3830
4315
|
function renderSidebarFiles(files) {
|
|
@@ -3873,6 +4358,13 @@ function OpenTuiApp(props) {
|
|
|
3873
4358
|
? getContextBudget(providerId, modelId, props.agent.messages)
|
|
3874
4359
|
: undefined;
|
|
3875
4360
|
const usage = sidebarUsage();
|
|
4361
|
+
const contextTokens = usage.contextTokens || (budget?.estimatedTokens ?? 0);
|
|
4362
|
+
const contextPercent = budget?.contextWindow
|
|
4363
|
+
? Math.min(100, Math.round((contextTokens / budget.contextWindow) * 100))
|
|
4364
|
+
: Math.round(budget?.percent ?? 0);
|
|
4365
|
+
const remainingTokens = budget?.contextWindow !== undefined
|
|
4366
|
+
? Math.max(0, budget.contextWindow - contextTokens)
|
|
4367
|
+
: undefined;
|
|
3876
4368
|
const tokenUsage = {
|
|
3877
4369
|
promptTokens: usage.promptTokens,
|
|
3878
4370
|
completionTokens: usage.completionTokens,
|
|
@@ -3883,8 +4375,9 @@ function OpenTuiApp(props) {
|
|
|
3883
4375
|
};
|
|
3884
4376
|
const cost = providerId && modelId ? calculateUsageCost(providerId, modelId, tokenUsage) : undefined;
|
|
3885
4377
|
return {
|
|
3886
|
-
tokens:
|
|
3887
|
-
percent:
|
|
4378
|
+
tokens: contextTokens,
|
|
4379
|
+
percent: contextPercent,
|
|
4380
|
+
remainingTokens,
|
|
3888
4381
|
promptTokens: usage.promptTokens,
|
|
3889
4382
|
completionTokens: usage.completionTokens,
|
|
3890
4383
|
reasoningTokens: usage.reasoningTokens,
|
|
@@ -3914,6 +4407,7 @@ function OpenTuiApp(props) {
|
|
|
3914
4407
|
ref: (ref) => { scrollbox = ref; },
|
|
3915
4408
|
stickyScroll: true,
|
|
3916
4409
|
stickyStart: "bottom",
|
|
4410
|
+
onMouseScroll: handleTranscriptMouseScroll,
|
|
3917
4411
|
flexGrow: 1,
|
|
3918
4412
|
minHeight: 0,
|
|
3919
4413
|
}, h("box", { height: 1 }), h("box", {
|
|
@@ -3925,7 +4419,8 @@ function OpenTuiApp(props) {
|
|
|
3925
4419
|
updateTranscriptHost(ref, transcriptState, currentTranscriptMessages(streamingDisplay), transcriptOptions(), props.syntaxStyle, props.subtleSyntaxStyle);
|
|
3926
4420
|
syncThinkingSpinner();
|
|
3927
4421
|
syncPromptSurfaces(isNewHost);
|
|
3928
|
-
|
|
4422
|
+
if (isNewHost)
|
|
4423
|
+
scheduleTranscriptScrollAfterUpdate(transcriptScrollFollowing, 0);
|
|
3929
4424
|
},
|
|
3930
4425
|
flexDirection: "column",
|
|
3931
4426
|
flexShrink: 0,
|
|
@@ -3935,6 +4430,8 @@ function OpenTuiApp(props) {
|
|
|
3935
4430
|
}, notice()) : null, renderQuestionPanelHost(), h("box", {
|
|
3936
4431
|
ref: (ref) => { approvalRoot = ref; },
|
|
3937
4432
|
visible: !!approval,
|
|
4433
|
+
focusable: true,
|
|
4434
|
+
onKeyDown: handleApprovalKey,
|
|
3938
4435
|
position: "absolute",
|
|
3939
4436
|
left: 2,
|
|
3940
4437
|
right: 2,
|
|
@@ -4046,9 +4543,38 @@ function OpenTuiApp(props) {
|
|
|
4046
4543
|
ref: (ref) => { approvalOptionTexts[index] = ref; },
|
|
4047
4544
|
fg: theme.textMuted,
|
|
4048
4545
|
content: "",
|
|
4049
|
-
})))), h("box", { flexDirection: "row", gap: 2, flexShrink: 0 }, h("text", { fg: theme.text }, "
|
|
4546
|
+
})))), 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
4547
|
]);
|
|
4051
4548
|
}
|
|
4549
|
+
function renderNoticeOverlay() {
|
|
4550
|
+
return h("box", {
|
|
4551
|
+
ref: (ref) => {
|
|
4552
|
+
copyToastRoot = ref;
|
|
4553
|
+
ref.visible = false;
|
|
4554
|
+
},
|
|
4555
|
+
visible: false,
|
|
4556
|
+
position: "absolute",
|
|
4557
|
+
top: 2,
|
|
4558
|
+
right: sidebarVisible() ? SESSION_SIDEBAR_WIDTH + 2 : 2,
|
|
4559
|
+
zIndex: 4000,
|
|
4560
|
+
flexDirection: "column",
|
|
4561
|
+
alignItems: "flex-start",
|
|
4562
|
+
width: 24,
|
|
4563
|
+
paddingLeft: 2,
|
|
4564
|
+
paddingRight: 2,
|
|
4565
|
+
paddingTop: 1,
|
|
4566
|
+
paddingBottom: 1,
|
|
4567
|
+
backgroundColor: theme.backgroundPanel,
|
|
4568
|
+
border: ["left", "right"],
|
|
4569
|
+
borderColor: theme.info,
|
|
4570
|
+
}, h("text", {
|
|
4571
|
+
ref: (ref) => { copyToastText = ref; },
|
|
4572
|
+
fg: theme.text,
|
|
4573
|
+
wrapMode: "word",
|
|
4574
|
+
width: "100%",
|
|
4575
|
+
content: "",
|
|
4576
|
+
}));
|
|
4577
|
+
}
|
|
4052
4578
|
return h("box", {
|
|
4053
4579
|
ref: (ref) => { rootBox = ref; },
|
|
4054
4580
|
flexDirection: "column",
|
|
@@ -4080,6 +4606,7 @@ function OpenTuiApp(props) {
|
|
|
4080
4606
|
registerModeBadge: registerFooterModeBadge,
|
|
4081
4607
|
}),
|
|
4082
4608
|
renderProviderDialog(),
|
|
4609
|
+
renderNoticeOverlay(),
|
|
4083
4610
|
]);
|
|
4084
4611
|
}
|
|
4085
4612
|
function renderPrompt(input) {
|
|
@@ -4788,10 +5315,50 @@ function createModelSwitchEntry(ctx, model, key, signature) {
|
|
|
4788
5315
|
]);
|
|
4789
5316
|
return { key, signature, node, refs: {} };
|
|
4790
5317
|
}
|
|
5318
|
+
function createTodoWriteRenderable(ctx, tool) {
|
|
5319
|
+
const todos = tool.args.todos || [];
|
|
5320
|
+
const summary = tool.result || "";
|
|
5321
|
+
if (!isToolFinished(tool)) {
|
|
5322
|
+
return createBox(ctx, {
|
|
5323
|
+
paddingLeft: 3,
|
|
5324
|
+
marginTop: 1,
|
|
5325
|
+
flexDirection: "column",
|
|
5326
|
+
flexShrink: 0,
|
|
5327
|
+
}, [
|
|
5328
|
+
createText(ctx, `~ → Planning tasks...`, { fg: toolColor(tool) }),
|
|
5329
|
+
]);
|
|
5330
|
+
}
|
|
5331
|
+
return createBox(ctx, {
|
|
5332
|
+
border: ["left"],
|
|
5333
|
+
borderColor: theme.borderSubtle,
|
|
5334
|
+
backgroundColor: theme.backgroundPanel,
|
|
5335
|
+
marginTop: 1,
|
|
5336
|
+
paddingTop: 1,
|
|
5337
|
+
paddingBottom: 1,
|
|
5338
|
+
paddingLeft: 2,
|
|
5339
|
+
flexDirection: "column",
|
|
5340
|
+
flexShrink: 0,
|
|
5341
|
+
}, [
|
|
5342
|
+
createText(ctx, `# Todo ${summary ? `— ${summary}` : ""}`, { fg: theme.textMuted }),
|
|
5343
|
+
...todos.map((todo, index) => {
|
|
5344
|
+
const completed = todo.status === "completed";
|
|
5345
|
+
const inProgress = todo.status === "in_progress";
|
|
5346
|
+
const marker = completed ? "✓" : inProgress ? "◉" : "○";
|
|
5347
|
+
const fg = completed ? theme.success : inProgress ? theme.warning : theme.textMuted;
|
|
5348
|
+
return createText(ctx, ` ${marker} ${todo.content}`, {
|
|
5349
|
+
fg,
|
|
5350
|
+
marginTop: index === 0 ? 1 : 0,
|
|
5351
|
+
});
|
|
5352
|
+
}),
|
|
5353
|
+
]);
|
|
5354
|
+
}
|
|
4791
5355
|
function createToolRenderable(ctx, tool, syntaxStyle, width = 80) {
|
|
4792
5356
|
if (tool.name === "question") {
|
|
4793
5357
|
return createQuestionToolRenderable(ctx, tool);
|
|
4794
5358
|
}
|
|
5359
|
+
if (tool.name === "todo_write") {
|
|
5360
|
+
return createTodoWriteRenderable(ctx, tool);
|
|
5361
|
+
}
|
|
4795
5362
|
const icon = tool.name === "bash" ? "$" : tool.name === "edit" || tool.name === "write" ? "✎" : "●";
|
|
4796
5363
|
const color = toolColor(tool);
|
|
4797
5364
|
const header = toolHeader(tool);
|
|
@@ -4988,9 +5555,9 @@ function renderFooter(input) {
|
|
|
4988
5555
|
idleFg: theme.textMuted,
|
|
4989
5556
|
runningFg: theme.primary,
|
|
4990
5557
|
}), h("text", {
|
|
4991
|
-
fg:
|
|
5558
|
+
fg: permissionModeColor(input.mode()),
|
|
4992
5559
|
ref: input.registerModeBadge,
|
|
4993
|
-
},
|
|
5560
|
+
}, footerPermissionModeText(input.mode())), h("box", { flexGrow: 1 }));
|
|
4994
5561
|
}
|
|
4995
5562
|
function pickerTitle(kind, providerId) {
|
|
4996
5563
|
switch (kind) {
|
|
@@ -5012,6 +5579,8 @@ function pickerTitle(kind, providerId) {
|
|
|
5012
5579
|
return "Select Login Provider";
|
|
5013
5580
|
case "logout":
|
|
5014
5581
|
return "Select Logout Provider";
|
|
5582
|
+
case "skill":
|
|
5583
|
+
return "Skills";
|
|
5015
5584
|
case "slash":
|
|
5016
5585
|
return "Commands";
|
|
5017
5586
|
case "file":
|
|
@@ -5447,13 +6016,37 @@ function contrastText(color) {
|
|
|
5447
6016
|
return luminance > 160 ? "#000000" : "#ffffff";
|
|
5448
6017
|
}
|
|
5449
6018
|
function promptModeBadgeContent(mode) {
|
|
5450
|
-
const
|
|
5451
|
-
const
|
|
5452
|
-
const label = isPlan ? "Plan" : "Build";
|
|
6019
|
+
const color = permissionModeColor(mode);
|
|
6020
|
+
const label = permissionModeBadgeLabel(mode);
|
|
5453
6021
|
return new StyledText([
|
|
5454
6022
|
bg(color)(fg(contrastText(color))(bold(` ${label} `))),
|
|
5455
6023
|
]);
|
|
5456
6024
|
}
|
|
6025
|
+
function permissionModeBadgeLabel(mode) {
|
|
6026
|
+
switch (mode) {
|
|
6027
|
+
case "default": return "Build";
|
|
6028
|
+
case "plan": return "Plan";
|
|
6029
|
+
case "bypassPermissions": return "Bypass";
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
function footerPermissionModeText(mode) {
|
|
6033
|
+
const info = PERMISSION_MODE_INFO[mode];
|
|
6034
|
+
if (mode === "default")
|
|
6035
|
+
return " mode: build · shift+tab plan";
|
|
6036
|
+
if (mode === "plan")
|
|
6037
|
+
return " mode: plan · shift+tab bypass";
|
|
6038
|
+
return ` mode: ${info.shortTitle} · shift+tab build`;
|
|
6039
|
+
}
|
|
6040
|
+
function permissionModeColor(mode) {
|
|
6041
|
+
const info = PERMISSION_MODE_INFO[mode];
|
|
6042
|
+
switch (info.color) {
|
|
6043
|
+
case "accent": return theme.accent;
|
|
6044
|
+
case "success": return theme.success;
|
|
6045
|
+
case "warning": return theme.warning;
|
|
6046
|
+
case "error": return theme.error;
|
|
6047
|
+
case "muted": return theme.primary;
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
5457
6050
|
function toolColor(tool) {
|
|
5458
6051
|
if (tool.isError)
|
|
5459
6052
|
return theme.toolError;
|
|
@@ -5501,8 +6094,10 @@ function extractToolDiff(tool) {
|
|
|
5501
6094
|
const index = tool.result.indexOf(marker);
|
|
5502
6095
|
if (index === -1)
|
|
5503
6096
|
return undefined;
|
|
5504
|
-
const
|
|
5505
|
-
|
|
6097
|
+
const rawDiff = tool.result.slice(index + marker.length);
|
|
6098
|
+
const diagnosticsIndex = rawDiff.search(/\n\nLSP diagnostics in /);
|
|
6099
|
+
const diff = diagnosticsIndex === -1 ? rawDiff : rawDiff.slice(0, diagnosticsIndex);
|
|
6100
|
+
return diff.trim().length > 0 ? diff : undefined;
|
|
5506
6101
|
}
|
|
5507
6102
|
function diffViewMode(width = 80) {
|
|
5508
6103
|
return width > 120 ? "split" : "unified";
|
|
@@ -5622,18 +6217,6 @@ function assistantStatusLabel(message) {
|
|
|
5622
6217
|
return "Responding...";
|
|
5623
6218
|
return message.streaming ? "Thinking..." : "Thinking";
|
|
5624
6219
|
}
|
|
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
6220
|
function buildContextGauge(percent, barWidth) {
|
|
5638
6221
|
const clamped = Math.max(0, Math.min(100, percent));
|
|
5639
6222
|
const filled = Math.round((clamped / 100) * barWidth);
|
|
@@ -5642,17 +6225,27 @@ function buildContextGauge(percent, barWidth) {
|
|
|
5642
6225
|
const space = empty > 0 ? "░".repeat(empty) : "";
|
|
5643
6226
|
return `${block}${space}`;
|
|
5644
6227
|
}
|
|
5645
|
-
function buildGaugeLabel(percent) {
|
|
6228
|
+
function buildGaugeLabel(percent, remainingTokens) {
|
|
6229
|
+
const remaining = remainingTokens === undefined ? "" : ` · ${formatContextRemaining(remainingTokens)} left`;
|
|
5646
6230
|
if (percent >= 95)
|
|
5647
|
-
return
|
|
6231
|
+
return `⚠ Compact imminent${remaining}`;
|
|
5648
6232
|
if (percent >= 80)
|
|
5649
|
-
return
|
|
6233
|
+
return `⚠ Approaching limit${remaining}`;
|
|
5650
6234
|
if (percent >= 60)
|
|
5651
|
-
return
|
|
6235
|
+
return `● Context growing${remaining}`;
|
|
5652
6236
|
if (percent > 0)
|
|
5653
|
-
return
|
|
6237
|
+
return `○ Available${remaining}`;
|
|
5654
6238
|
return "○ Empty";
|
|
5655
6239
|
}
|
|
6240
|
+
function formatContextRemaining(value) {
|
|
6241
|
+
if (value >= 1_000_000)
|
|
6242
|
+
return `${(value / 1_000_000).toFixed(2)}M`;
|
|
6243
|
+
if (value >= 100_000)
|
|
6244
|
+
return `${Math.round(value / 1_000)}K`;
|
|
6245
|
+
if (value >= 1_000)
|
|
6246
|
+
return `${(value / 1_000).toFixed(1)}K`;
|
|
6247
|
+
return String(value);
|
|
6248
|
+
}
|
|
5656
6249
|
function thinkingToggleLabel(expanded, streaming = false, spinnerFrame = "") {
|
|
5657
6250
|
const arrow = expanded ? "▼" : "▶";
|
|
5658
6251
|
return streaming && spinnerFrame ? `${spinnerFrame} ${arrow} Thinking` : `${arrow} Thinking`;
|