@bubblebrain-ai/bubble 0.0.29 → 0.0.31
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/README.md +2 -3
- package/dist/agent/child-runner.js +12 -0
- package/dist/feishu/card/run-state.js +9 -1
- package/dist/goal/format.d.ts +1 -1
- package/dist/goal/format.js +1 -1
- package/dist/main.js +14 -1
- package/dist/slash-commands/commands.js +1 -119
- package/dist/slash-commands/types.d.ts +1 -11
- package/dist/tui-ink/app.d.ts +0 -18
- package/dist/tui-ink/app.js +101 -231
- package/dist/tui-ink/input-box.d.ts +3 -0
- package/dist/tui-ink/input-box.js +8 -1
- package/dist/tui-ink/markdown.js +34 -7
- package/dist/tui-ink/message-list.d.ts +1 -2
- package/dist/tui-ink/message-list.js +13 -14
- package/dist/tui-ink/subagent-inspector.d.ts +1 -1
- package/dist/tui-ink/subagent-inspector.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,7 +66,6 @@ Bubble ships with a catalog of built-in providers. Configure them inside the app
|
|
|
66
66
|
| --- | --- |
|
|
67
67
|
| `/login` | OAuth sign-in for ChatGPT; unlocks the OpenAI Codex models without an API key. |
|
|
68
68
|
| `/provider` | Open a picker to connect, switch, add, or remove a provider. |
|
|
69
|
-
| `/key <provider> <key>` | Set the API key for a provider. |
|
|
70
69
|
| `/model` | Pick the active model and reasoning effort. |
|
|
71
70
|
|
|
72
71
|
Built-in providers include OpenAI, Anthropic, Google, DeepSeek, Moonshot (CN and international), Kimi for Coding, Zhipu AI, Z.AI, Alibaba DashScope, Doubao (Volcengine Ark), MiniMax, StepFun, Groq, Together AI, Fireworks, and a `local` profile for any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio, etc.).
|
|
@@ -155,7 +154,7 @@ Rules use a simple pattern syntax, for example:
|
|
|
155
154
|
| --- | --- |
|
|
156
155
|
| `/help` | List available commands. |
|
|
157
156
|
| `/model` | Switch model and reasoning effort. |
|
|
158
|
-
| `/provider`, `/login`, `/logout
|
|
157
|
+
| `/provider`, `/login`, `/logout` | Connect and manage providers. |
|
|
159
158
|
| `/session`, `/rewind`, `/clear` | Manage conversation history. |
|
|
160
159
|
| `/skills` | Open the searchable skills picker. |
|
|
161
160
|
| `/mcp` | List or reconnect MCP servers. |
|
|
@@ -163,7 +162,7 @@ Rules use a simple pattern syntax, for example:
|
|
|
163
162
|
| `/permissions` | View or edit allow/deny rules. |
|
|
164
163
|
| `/context`, `/stats`, `/compact` | Inspect context usage, model stats, and compact the session. |
|
|
165
164
|
| `/lsp`, `/hooks` | Manage language servers and lifecycle hooks. |
|
|
166
|
-
| `/theme
|
|
165
|
+
| `/theme` | Adjust the interface theme. |
|
|
167
166
|
| `/feedback` | Send feedback or report a bug. |
|
|
168
167
|
|
|
169
168
|
## Non-interactive mode
|
|
@@ -88,6 +88,13 @@ export class ChildRunner {
|
|
|
88
88
|
record.abortController.signal,
|
|
89
89
|
]);
|
|
90
90
|
for await (const event of subAgent.run(input, runCwd, { abortSignal: childAbortSignal, resumeWithoutInput })) {
|
|
91
|
+
if (event.type === "turn_start") {
|
|
92
|
+
// Leftovers here belong to a half-built attempt the agent discarded
|
|
93
|
+
// (stream-interruption retry re-issues the whole request); keeping
|
|
94
|
+
// them would duplicate the retried text in the turn summary.
|
|
95
|
+
turnSummaryBuffer = "";
|
|
96
|
+
turnHadToolCall = false;
|
|
97
|
+
}
|
|
91
98
|
if (event.type === "text_delta") {
|
|
92
99
|
turnSummaryBuffer += event.content;
|
|
93
100
|
}
|
|
@@ -213,6 +220,11 @@ export class ChildRunner {
|
|
|
213
220
|
let finalHadToolCall = false;
|
|
214
221
|
const finalAbortSignal = composeAbortSignals([abortSignal, record.abortController.signal]);
|
|
215
222
|
for await (const event of subAgent.run(prompt, cwd, { abortSignal: finalAbortSignal })) {
|
|
223
|
+
if (event.type === "turn_start") {
|
|
224
|
+
// Discarded stream-interruption attempt — drop its partial text so the
|
|
225
|
+
// retried response doesn't carry a duplicated prefix.
|
|
226
|
+
finalBuffer = "";
|
|
227
|
+
}
|
|
216
228
|
if (event.type === "text_delta") {
|
|
217
229
|
finalBuffer += event.content;
|
|
218
230
|
}
|
|
@@ -13,7 +13,12 @@ export function reduceRunState(state, event) {
|
|
|
13
13
|
state.updatedAt = Date.now();
|
|
14
14
|
switch (event.type) {
|
|
15
15
|
case "turn_start":
|
|
16
|
-
//
|
|
16
|
+
// A new LLM round trip. turn_end settles (closes) the blocks of every
|
|
17
|
+
// finished call, so anything still marked streaming here belongs to a
|
|
18
|
+
// half-built attempt the agent discarded (its stream-interruption retry
|
|
19
|
+
// re-issues the whole request). Drop it, or the retry re-streams the
|
|
20
|
+
// same opening text into the block and the card shows it twice.
|
|
21
|
+
state.blocks = state.blocks.filter((block) => !((block.kind === "text" || block.kind === "thinking") && block.streaming));
|
|
17
22
|
return state;
|
|
18
23
|
case "text_delta": {
|
|
19
24
|
const last = state.blocks[state.blocks.length - 1];
|
|
@@ -81,6 +86,9 @@ export function reduceRunState(state, event) {
|
|
|
81
86
|
return state;
|
|
82
87
|
}
|
|
83
88
|
case "turn_end": {
|
|
89
|
+
// Settle this call's output so the turn_start cleanup above can tell
|
|
90
|
+
// kept content (closed here) apart from a discarded retry attempt.
|
|
91
|
+
closeStreamingBlocks(state);
|
|
84
92
|
if (event.usage) {
|
|
85
93
|
state.usage = mergeUsage(state.usage, event.usage);
|
|
86
94
|
}
|
package/dist/goal/format.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ export declare function goalSummaryText(goal: GoalState): string;
|
|
|
14
14
|
* update_goal tool can't report this — see goal/tools.ts).
|
|
15
15
|
*/
|
|
16
16
|
export declare function goalCompleteNotice(goal: GoalState): string;
|
|
17
|
-
/** Compact single-line indicator for
|
|
17
|
+
/** Compact single-line indicator for status surfaces. */
|
|
18
18
|
export declare function goalIndicatorLine(goal: GoalState, maxObjective?: number): string;
|
package/dist/goal/format.js
CHANGED
|
@@ -95,7 +95,7 @@ function completionTokenUsagePhrase(goal) {
|
|
|
95
95
|
? `${formatTokensCompact(goal.tokensUsed)}/${formatTokensCompact(goal.tokenBudget)} tok used`
|
|
96
96
|
: `${formatTokensCompact(goal.tokensUsed)} tok used`;
|
|
97
97
|
}
|
|
98
|
-
/** Compact single-line indicator for
|
|
98
|
+
/** Compact single-line indicator for status surfaces. */
|
|
99
99
|
export function goalIndicatorLine(goal, maxObjective = 48) {
|
|
100
100
|
const segments = [`goal: ${goalStatusLabel(goal.status)}`, `${goal.turnsSpent} turns`];
|
|
101
101
|
const tokens = tokensPart(goal);
|
package/dist/main.js
CHANGED
|
@@ -483,9 +483,22 @@ async function main() {
|
|
|
483
483
|
console.error(chalk.red("Error: No prompt provided."));
|
|
484
484
|
process.exit(1);
|
|
485
485
|
}
|
|
486
|
+
let printedTurnText = false;
|
|
486
487
|
for await (const event of agent.run(prompt, args.cwd)) {
|
|
487
488
|
traceEvent("print_agent_event", summarizeAgentEventForTrace(event));
|
|
488
|
-
if (event.type === "
|
|
489
|
+
if (event.type === "turn_start") {
|
|
490
|
+
printedTurnText = false;
|
|
491
|
+
}
|
|
492
|
+
else if (event.type === "provider_retry") {
|
|
493
|
+
// The stream died mid-response and the agent re-issues the whole
|
|
494
|
+
// request. Text already on stdout cannot be un-printed, so at least
|
|
495
|
+
// separate the retried response and say what happened.
|
|
496
|
+
if (printedTurnText)
|
|
497
|
+
process.stdout.write("\n");
|
|
498
|
+
console.error(chalk.yellow(`[Stream interrupted; retrying (${event.attempt}/${event.maxAttempts}) — the partial text above is superseded by the retried response]`));
|
|
499
|
+
}
|
|
500
|
+
else if (event.type === "text_delta") {
|
|
501
|
+
printedTurnText = true;
|
|
489
502
|
process.stdout.write(event.content);
|
|
490
503
|
}
|
|
491
504
|
else if (event.type === "tool_start") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { UserConfig
|
|
1
|
+
import { UserConfig } from "../config.js";
|
|
2
2
|
import { formatContextUsage } from "../context/usage.js";
|
|
3
3
|
import { formatDiagnostics } from "../lsp/index.js";
|
|
4
4
|
import { normalizeNameForMCP } from "../mcp/name.js";
|
|
@@ -8,7 +8,6 @@ import { getAvailableThinkingLevels, getDefaultThinkingLevel, normalizeThinkingL
|
|
|
8
8
|
import { SessionManager } from "../session.js";
|
|
9
9
|
import { buildSystemPrompt } from "../system-prompt.js";
|
|
10
10
|
import { normalizeSingleLine } from "../text-display.js";
|
|
11
|
-
import { copyToClipboard } from "../clipboard.js";
|
|
12
11
|
import { formatRelativeTime } from "../tui/recent-activity.js";
|
|
13
12
|
import { HOOK_EVENT_NAMES, isHookEventName } from "../hooks/index.js";
|
|
14
13
|
import { isThinkingLevel } from "../variant/thinking-level.js";
|
|
@@ -287,17 +286,6 @@ async function handleMemoryCommand(args, ctx) {
|
|
|
287
286
|
}
|
|
288
287
|
return "Usage: /memory [status|search|compact|summarize|refresh|reset]";
|
|
289
288
|
}
|
|
290
|
-
function parseKeyArgs(args, ctx) {
|
|
291
|
-
const trimmed = args.trim();
|
|
292
|
-
const [first, ...rest] = trimmed.split(/\s+/);
|
|
293
|
-
const explicitProvider = first
|
|
294
|
-
? ctx.registry.getConfigured().find((provider) => provider.id === first)
|
|
295
|
-
: undefined;
|
|
296
|
-
if (explicitProvider) {
|
|
297
|
-
return { provider: explicitProvider, apiKey: rest.join(" ") };
|
|
298
|
-
}
|
|
299
|
-
return { provider: ctx.registry.getDefault(), apiKey: trimmed };
|
|
300
|
-
}
|
|
301
289
|
const builtinSlashCommandEntries = [
|
|
302
290
|
{
|
|
303
291
|
name: "skills",
|
|
@@ -306,13 +294,6 @@ const builtinSlashCommandEntries = [
|
|
|
306
294
|
ctx.openPicker("skill");
|
|
307
295
|
},
|
|
308
296
|
},
|
|
309
|
-
{
|
|
310
|
-
name: "agents",
|
|
311
|
-
description: "Inspect spawned subagents and their working traces (also Ctrl+G)",
|
|
312
|
-
async handler(_args, ctx) {
|
|
313
|
-
ctx.openPicker("agents");
|
|
314
|
-
},
|
|
315
|
-
},
|
|
316
297
|
{
|
|
317
298
|
name: "help",
|
|
318
299
|
description: "Show available slash commands",
|
|
@@ -370,33 +351,6 @@ const builtinSlashCommandEntries = [
|
|
|
370
351
|
return `Theme set to ${arg}${arg === "auto" ? ` (resolved to ${resolved})` : ""}.`;
|
|
371
352
|
},
|
|
372
353
|
},
|
|
373
|
-
{
|
|
374
|
-
name: "sidebar",
|
|
375
|
-
description: "Toggle the right sidebar. Usage: /sidebar [open|close|auto]",
|
|
376
|
-
async handler(args, ctx) {
|
|
377
|
-
if (!ctx.toggleSidebar || !ctx.setSidebarMode) {
|
|
378
|
-
return "Sidebar control is only available inside the TUI.";
|
|
379
|
-
}
|
|
380
|
-
const arg = args.trim().toLowerCase();
|
|
381
|
-
if (!arg) {
|
|
382
|
-
ctx.toggleSidebar();
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (["open", "show", "expand", "expanded", "on"].includes(arg)) {
|
|
386
|
-
ctx.setSidebarMode("expanded");
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (["close", "hide", "collapse", "collapsed", "off"].includes(arg)) {
|
|
390
|
-
ctx.setSidebarMode("collapsed");
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
if (arg === "auto") {
|
|
394
|
-
ctx.setSidebarMode("auto");
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
return "Usage: /sidebar [open|close|auto]";
|
|
398
|
-
},
|
|
399
|
-
},
|
|
400
354
|
{
|
|
401
355
|
name: "clear",
|
|
402
356
|
description: "Clear the current conversation history",
|
|
@@ -410,27 +364,6 @@ const builtinSlashCommandEntries = [
|
|
|
410
364
|
ctx.clearMessages();
|
|
411
365
|
},
|
|
412
366
|
},
|
|
413
|
-
{
|
|
414
|
-
name: "copy",
|
|
415
|
-
description: "Copy the last assistant message to the system clipboard",
|
|
416
|
-
async handler(args, ctx) {
|
|
417
|
-
const lastAssistant = [...ctx.agent.messages]
|
|
418
|
-
.reverse()
|
|
419
|
-
.find((m) => m.role === "assistant" && typeof m.content === "string" && m.content.trim().length > 0);
|
|
420
|
-
if (!lastAssistant || typeof lastAssistant.content !== "string") {
|
|
421
|
-
return "No assistant message to copy yet.";
|
|
422
|
-
}
|
|
423
|
-
const text = lastAssistant.content;
|
|
424
|
-
try {
|
|
425
|
-
await copyToClipboard(text);
|
|
426
|
-
}
|
|
427
|
-
catch (err) {
|
|
428
|
-
return `Failed to copy to clipboard: ${err?.message || String(err)}`;
|
|
429
|
-
}
|
|
430
|
-
const chars = text.length;
|
|
431
|
-
return `Copied last assistant message to clipboard (${chars} character${chars === 1 ? "" : "s"}).`;
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
367
|
{
|
|
435
368
|
name: "rewind",
|
|
436
369
|
description: "Rewind conversation and/or file edits to before an earlier message. Usage: /rewind [n] [--code|--chat]",
|
|
@@ -674,31 +607,6 @@ const builtinSlashCommandEntries = [
|
|
|
674
607
|
return `Model switched to ${displaySelectedModel(next, ctx.agent.thinking)}.`;
|
|
675
608
|
},
|
|
676
609
|
},
|
|
677
|
-
{
|
|
678
|
-
name: "key",
|
|
679
|
-
description: "Set API key for the current or a specific provider. Usage: /key [provider-id] <key>",
|
|
680
|
-
async handler(args, ctx) {
|
|
681
|
-
if (!args) {
|
|
682
|
-
ctx.openPicker("key");
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
const { provider, apiKey } = parseKeyArgs(args, ctx);
|
|
686
|
-
if (!provider) {
|
|
687
|
-
return "No provider configured. Use /provider --add <id> first.";
|
|
688
|
-
}
|
|
689
|
-
if (!apiKey) {
|
|
690
|
-
return `Usage: /key ${provider.id} <key>`;
|
|
691
|
-
}
|
|
692
|
-
if (ctx.registry.getModelConfig().hasProvider(provider.id)) {
|
|
693
|
-
return `API key for ${provider.name} is managed in ~/.bubble/models.json. Please edit that file directly.`;
|
|
694
|
-
}
|
|
695
|
-
ctx.registry.updateProviderKey(provider.id, apiKey);
|
|
696
|
-
ctx.registry.setDefault(provider.id);
|
|
697
|
-
ctx.agent.setProvider(ctx.createProvider(provider.id, apiKey, provider.baseURL));
|
|
698
|
-
ctx.agent.providerId = provider.id;
|
|
699
|
-
return `API key updated for ${provider.name} to ${maskKey(apiKey)}.`;
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
610
|
{
|
|
703
611
|
name: "logout",
|
|
704
612
|
description: "Remove OAuth credentials for a provider. Usage: /logout [openai]",
|
|
@@ -739,32 +647,6 @@ const builtinSlashCommandEntries = [
|
|
|
739
647
|
: "Exited plan mode.";
|
|
740
648
|
},
|
|
741
649
|
},
|
|
742
|
-
{
|
|
743
|
-
name: "todos",
|
|
744
|
-
description: "Show the current todo list. Use /todos clear to reset it.",
|
|
745
|
-
async handler(args, ctx) {
|
|
746
|
-
const sub = args.trim();
|
|
747
|
-
if (sub === "clear") {
|
|
748
|
-
const previous = ctx.agent.getTodos().length;
|
|
749
|
-
if (previous === 0) {
|
|
750
|
-
return "Todo list is already empty.";
|
|
751
|
-
}
|
|
752
|
-
ctx.agent.setTodos([]);
|
|
753
|
-
return `Cleared ${previous} todo item${previous === 1 ? "" : "s"}.`;
|
|
754
|
-
}
|
|
755
|
-
const todos = ctx.agent.getTodos();
|
|
756
|
-
if (todos.length === 0) {
|
|
757
|
-
return "No todos yet. The assistant will create some when working on multi-step tasks.";
|
|
758
|
-
}
|
|
759
|
-
const glyph = (status) => status === "completed" ? "✔" : status === "in_progress" ? "▶" : "○";
|
|
760
|
-
const lines = ["Todos:"];
|
|
761
|
-
for (const todo of todos) {
|
|
762
|
-
const label = todo.status === "in_progress" ? (todo.activeForm || todo.content) : todo.content;
|
|
763
|
-
lines.push(` ${glyph(todo.status)} ${label}`);
|
|
764
|
-
}
|
|
765
|
-
return lines.join("\n");
|
|
766
|
-
},
|
|
767
|
-
},
|
|
768
650
|
{
|
|
769
651
|
name: "permissions",
|
|
770
652
|
description: "Inspect or edit allow/deny rules. Subcommands: add <scope> <list> <rule>, remove <scope> <list> <rule>, clear (session allowlist), reload.",
|
|
@@ -10,7 +10,6 @@ import type { LspService } from "../lsp/index.js";
|
|
|
10
10
|
import type { MemoryScope } from "../memory/index.js";
|
|
11
11
|
import type { ThemeMode } from "../config.js";
|
|
12
12
|
import type { ExternalHookController } from "../hooks/controller.js";
|
|
13
|
-
export type SidebarMode = "auto" | "expanded" | "collapsed";
|
|
14
13
|
/**
|
|
15
14
|
* Live progress for a manual `/compact` run, pushed to the TUI so it can render
|
|
16
15
|
* a progress bar. `phase` advances collecting → summarizing → applying;
|
|
@@ -21,11 +20,6 @@ export interface CompactionProgress {
|
|
|
21
20
|
phase: "collecting" | "summarizing" | "applying";
|
|
22
21
|
streamedChars: number;
|
|
23
22
|
}
|
|
24
|
-
export interface SidebarCommandState {
|
|
25
|
-
mode: SidebarMode;
|
|
26
|
-
visible: boolean;
|
|
27
|
-
active: boolean;
|
|
28
|
-
}
|
|
29
23
|
export interface SlashCommandContext {
|
|
30
24
|
agent: Agent;
|
|
31
25
|
addMessage: (role: "user" | "assistant" | "error", content: string) => void;
|
|
@@ -34,7 +28,7 @@ export interface SlashCommandContext {
|
|
|
34
28
|
exit: () => void;
|
|
35
29
|
sessionManager?: SessionManager;
|
|
36
30
|
createProvider: (providerId: string, apiKey: string, baseURL: string) => Provider;
|
|
37
|
-
openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup"
|
|
31
|
+
openPicker: (mode: "model" | "key" | "provider" | "provider-add" | "login" | "logout" | "skill" | "feishu-setup", providerId?: string) => void;
|
|
38
32
|
registry: ProviderRegistry;
|
|
39
33
|
skillRegistry: SkillRegistry;
|
|
40
34
|
bashAllowlist?: BashAllowlist;
|
|
@@ -52,10 +46,6 @@ export interface SlashCommandContext {
|
|
|
52
46
|
getResolvedTheme?: () => "light" | "dark";
|
|
53
47
|
/** Persist a new theme mode AND apply it to the running TUI. */
|
|
54
48
|
setThemeMode?: (mode: ThemeMode) => void;
|
|
55
|
-
/** Toggle the right session sidebar in the running TUI. */
|
|
56
|
-
toggleSidebar?: () => SidebarCommandState;
|
|
57
|
-
/** Set the right session sidebar mode in the running TUI. */
|
|
58
|
-
setSidebarMode?: (mode: SidebarMode) => SidebarCommandState;
|
|
59
49
|
/** Open the feedback dialog. `initialDescription` prefills the description field. */
|
|
60
50
|
openFeedback?: (initialDescription: string) => void;
|
|
61
51
|
/** Open the interactive rewind picker. When absent, /rewind falls back to a text listing. */
|
package/dist/tui-ink/app.d.ts
CHANGED
|
@@ -60,26 +60,8 @@ export interface ExitSummary {
|
|
|
60
60
|
wallMs: number;
|
|
61
61
|
}
|
|
62
62
|
export declare const INK_LOCAL_SLASH_COMMANDS: readonly [{
|
|
63
|
-
readonly name: "thinking";
|
|
64
|
-
readonly description: "Toggle thinking block visibility";
|
|
65
|
-
}, {
|
|
66
|
-
readonly name: "toggle-thinking";
|
|
67
|
-
readonly description: "Toggle thinking block visibility";
|
|
68
|
-
}, {
|
|
69
63
|
readonly name: "goal";
|
|
70
64
|
readonly description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)";
|
|
71
|
-
}, {
|
|
72
|
-
readonly name: "trace";
|
|
73
|
-
readonly description: "Toggle verbose trace output";
|
|
74
|
-
}, {
|
|
75
|
-
readonly name: "verbose";
|
|
76
|
-
readonly description: "Toggle verbose trace output";
|
|
77
|
-
}, {
|
|
78
|
-
readonly name: "debug";
|
|
79
|
-
readonly description: "Toggle verbose trace output";
|
|
80
|
-
}, {
|
|
81
|
-
readonly name: "write-previews";
|
|
82
|
-
readonly description: "Toggle write preview expansion";
|
|
83
65
|
}];
|
|
84
66
|
export declare function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, onExit }: AppProps): import("react/jsx-runtime").JSX.Element;
|
|
85
67
|
export {};
|
package/dist/tui-ink/app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { isHiddenToolMetadata } from "../agent/discovery-barrier.js";
|
|
|
6
6
|
import { SessionManager } from "../session.js";
|
|
7
7
|
import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
8
8
|
import { UserConfig, maskKey } from "../config.js";
|
|
9
|
-
import { InputBox, isCtrlCInput, } from "./input-box.js";
|
|
9
|
+
import { InputBox, isCtrlCInput, isCtrlLetterInput, } from "./input-box.js";
|
|
10
10
|
import { MessageList } from "./message-list.js";
|
|
11
11
|
import { isMultiplexedTerminal } from "./terminal-env.js";
|
|
12
12
|
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
|
|
@@ -227,34 +227,10 @@ function withMessageKey(message) {
|
|
|
227
227
|
// 40ms keeps perceived latency invisible while capping layout work at 25fps.
|
|
228
228
|
const STREAMING_FLUSH_INTERVAL_MS = 40;
|
|
229
229
|
export const INK_LOCAL_SLASH_COMMANDS = [
|
|
230
|
-
{
|
|
231
|
-
name: "thinking",
|
|
232
|
-
description: "Toggle thinking block visibility",
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
name: "toggle-thinking",
|
|
236
|
-
description: "Toggle thinking block visibility",
|
|
237
|
-
},
|
|
238
230
|
{
|
|
239
231
|
name: "goal",
|
|
240
232
|
description: "Set/manage an autonomous goal (/goal <objective>|clear|pause|resume|edit)",
|
|
241
233
|
},
|
|
242
|
-
{
|
|
243
|
-
name: "trace",
|
|
244
|
-
description: "Toggle verbose trace output",
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: "verbose",
|
|
248
|
-
description: "Toggle verbose trace output",
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: "debug",
|
|
252
|
-
description: "Toggle verbose trace output",
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
name: "write-previews",
|
|
256
|
-
description: "Toggle write preview expansion",
|
|
257
|
-
},
|
|
258
234
|
];
|
|
259
235
|
export function App({ agent, args, sessionManager: initialSessionManager, switchSession, createProvider, registry, skillRegistry, planHandlerRef, approvalHandlerRef, questionController, bashAllowlist, settingsManager, lspService, mcpManager, themeMode: initialThemeMode, themeOverrides, detectedTheme, onThemeModeChange, flushMemory, runMemoryCompaction, runMemorySummary, runMemoryRefresh, goalStore, bypassEnabled, updateNotice, updateNoticeRefresh, hookController, onExit }) {
|
|
260
236
|
const [sessionManager, setSessionManager] = useState(initialSessionManager);
|
|
@@ -281,8 +257,8 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
281
257
|
const [streamingReasoning, setStreamingReasoning] = useState("");
|
|
282
258
|
const [streamingTools, setStreamingTools] = useState([]);
|
|
283
259
|
const [streamingParts, setStreamingParts] = useState([]);
|
|
284
|
-
// Live subagent groups for the
|
|
285
|
-
// reflects members as their events stream into the transcript.
|
|
260
|
+
// Live subagent groups for the inspector opened from the subagent entry line;
|
|
261
|
+
// recomputed each render so it reflects members as their events stream into the transcript.
|
|
286
262
|
const subagentGroups = useMemo(() => collectSubagentGroups(messages, streamingTools), [messages, streamingTools]);
|
|
287
263
|
const subagentMembers = useMemo(() => subagentGroups.flatMap((g) => g.members), [subagentGroups]);
|
|
288
264
|
// Down-arrow from the composer focuses the subagent entry line; Enter then
|
|
@@ -320,9 +296,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
320
296
|
const [composerDraft, setComposerDraft] = useState(null);
|
|
321
297
|
const [keyProviderId, setKeyProviderId] = useState(null);
|
|
322
298
|
const [showThinking, setShowThinking] = useState(false);
|
|
323
|
-
const [expandedToolOutput, setExpandedToolOutput] = useState(false);
|
|
324
299
|
const [verboseTrace, setVerboseTrace] = useState(false);
|
|
325
|
-
const [sidebarMode, setSidebarMode] = useState("collapsed");
|
|
326
300
|
const startedWithVisibleHistoryRef = useRef(messages.some((message) => message.syntheticKind !== "ui_summary"));
|
|
327
301
|
const { columns: terminalColumns, rows: terminalRows } = useTerminalSize();
|
|
328
302
|
const showWelcome = shouldShowWelcomeBanner({
|
|
@@ -338,6 +312,12 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
338
312
|
// MessageList so Ink discards its already-printed rows and re-prints the
|
|
339
313
|
// rebuilt list onto a freshly-cleared screen instead of appending duplicates.
|
|
340
314
|
const [staticGeneration, setStaticGeneration] = useState(0);
|
|
315
|
+
const reprintTranscript = useCallback(() => {
|
|
316
|
+
if (process.stdout.isTTY) {
|
|
317
|
+
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
318
|
+
}
|
|
319
|
+
setStaticGeneration((generation) => generation + 1);
|
|
320
|
+
}, []);
|
|
341
321
|
// Steer/queue while the agent runs:
|
|
342
322
|
// Enter steers the current run via the agent's input controller; Tab (or an
|
|
343
323
|
// ineligible input) queues for the next turn. Both render placeholder user
|
|
@@ -567,7 +547,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
567
547
|
setSubagentEntryFocused(false);
|
|
568
548
|
return;
|
|
569
549
|
}
|
|
570
|
-
if (
|
|
550
|
+
if (isCtrlLetterInput(input, key, "p") && !pickerMode && !activeAbortRef.current) {
|
|
571
551
|
setStatsPanel(null);
|
|
572
552
|
setPickerMode("slash");
|
|
573
553
|
return;
|
|
@@ -582,7 +562,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
582
562
|
}
|
|
583
563
|
return;
|
|
584
564
|
}
|
|
585
|
-
if (
|
|
565
|
+
if (isCtrlLetterInput(input, key, "t") && !pickerMode) {
|
|
586
566
|
setShowThinking((current) => {
|
|
587
567
|
const next = !current;
|
|
588
568
|
addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
|
|
@@ -590,12 +570,13 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
590
570
|
});
|
|
591
571
|
return;
|
|
592
572
|
}
|
|
593
|
-
if (
|
|
573
|
+
if (isCtrlLetterInput(input, key, "o") && !pickerMode) {
|
|
594
574
|
setVerboseTrace((v) => !v);
|
|
575
|
+
reprintTranscript();
|
|
595
576
|
return;
|
|
596
577
|
}
|
|
597
578
|
// Ctrl+R: cycle thinking level (formerly Shift+Tab)
|
|
598
|
-
if (
|
|
579
|
+
if (isCtrlLetterInput(input, key, "r") && !pickerMode) {
|
|
599
580
|
const modelParts = agent.model.includes(":")
|
|
600
581
|
? agent.model.split(":")
|
|
601
582
|
: [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
|
|
@@ -639,12 +620,9 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
639
620
|
// un-printed, so we wipe the screen + scrollback and bump the Static key:
|
|
640
621
|
// Ink then re-prints the rebuilt list fresh instead of appending duplicates.
|
|
641
622
|
const resetTranscript = useCallback((updater) => {
|
|
642
|
-
|
|
643
|
-
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
644
|
-
}
|
|
645
|
-
setStaticGeneration((generation) => generation + 1);
|
|
623
|
+
reprintTranscript();
|
|
646
624
|
updateDisplayMessages(updater);
|
|
647
|
-
}, [updateDisplayMessages]);
|
|
625
|
+
}, [reprintTranscript, updateDisplayMessages]);
|
|
648
626
|
const addMessage = useCallback((role, content) => {
|
|
649
627
|
updateDisplayMessages((prev) => [...prev, withMessageKey({ role, content })]);
|
|
650
628
|
}, [updateDisplayMessages]);
|
|
@@ -759,21 +737,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
759
737
|
const { description: _drop, ...rest } = base;
|
|
760
738
|
setPendingFeedback({ base: rest, initialDescription });
|
|
761
739
|
}, [agent]);
|
|
762
|
-
const sidebarFits = terminalColumns > 120;
|
|
763
|
-
const sidebarVisible = sidebarMode === "expanded" ? sidebarFits : sidebarMode === "auto" && sidebarFits;
|
|
764
|
-
const currentSidebarCommandState = useCallback((mode = sidebarMode) => {
|
|
765
|
-
const visible = mode === "expanded" ? sidebarFits : mode === "auto" && sidebarFits;
|
|
766
|
-
return { mode, visible, active: visible };
|
|
767
|
-
}, [sidebarFits, sidebarMode]);
|
|
768
|
-
const toggleSidebar = useCallback(() => {
|
|
769
|
-
const next = sidebarVisible ? "collapsed" : "expanded";
|
|
770
|
-
setSidebarMode(next);
|
|
771
|
-
return currentSidebarCommandState(next);
|
|
772
|
-
}, [currentSidebarCommandState, sidebarVisible]);
|
|
773
|
-
const applySidebarMode = useCallback((mode) => {
|
|
774
|
-
setSidebarMode(mode);
|
|
775
|
-
return currentSidebarCommandState(mode);
|
|
776
|
-
}, [currentSidebarCommandState]);
|
|
777
740
|
const openSessionPicker = useCallback(() => {
|
|
778
741
|
if (activeAbortRef.current) {
|
|
779
742
|
addMessage("error", "Stop the current run before switching sessions.");
|
|
@@ -1162,6 +1125,16 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1162
1125
|
inputController,
|
|
1163
1126
|
})) {
|
|
1164
1127
|
switch (event.type) {
|
|
1128
|
+
case "turn_start":
|
|
1129
|
+
// A fresh provider call is starting. Everything worth keeping
|
|
1130
|
+
// was committed at the preceding turn_end, so leftovers here
|
|
1131
|
+
// can only be a half-built attempt the agent discarded (its
|
|
1132
|
+
// stream-interruption retry re-issues the whole request and
|
|
1133
|
+
// never appends the partial message — see agent.ts). Drop the
|
|
1134
|
+
// stale buffer, or the retry re-streams the same opening text
|
|
1135
|
+
// on top of it and the answer duplicates on screen.
|
|
1136
|
+
clearAssistantStream();
|
|
1137
|
+
break;
|
|
1165
1138
|
case "text_delta":
|
|
1166
1139
|
assistantContent += event.content;
|
|
1167
1140
|
appendTextPart(assistantParts, event.content);
|
|
@@ -1553,30 +1526,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1553
1526
|
requestExit();
|
|
1554
1527
|
return;
|
|
1555
1528
|
}
|
|
1556
|
-
if (/^\/(?:thinking|toggle-thinking)(?:\s|$)/.test(input.trim())) {
|
|
1557
|
-
setShowThinking((current) => {
|
|
1558
|
-
const next = !current;
|
|
1559
|
-
addMessage("assistant", next ? "Thinking blocks visible" : "Thinking blocks hidden");
|
|
1560
|
-
return next;
|
|
1561
|
-
});
|
|
1562
|
-
return;
|
|
1563
|
-
}
|
|
1564
|
-
if (/^\/(?:trace|verbose|debug)(?:\s|$)/.test(input.trim())) {
|
|
1565
|
-
setVerboseTrace((current) => {
|
|
1566
|
-
const next = !current;
|
|
1567
|
-
addMessage("assistant", next ? "Verbose trace visible" : "Compact trace visible");
|
|
1568
|
-
return next;
|
|
1569
|
-
});
|
|
1570
|
-
return;
|
|
1571
|
-
}
|
|
1572
|
-
if (/^\/write-previews(?:\s|$)/.test(input.trim())) {
|
|
1573
|
-
setExpandedToolOutput((current) => {
|
|
1574
|
-
const next = !current;
|
|
1575
|
-
addMessage("assistant", next ? "Write previews expanded" : "Write previews collapsed");
|
|
1576
|
-
return next;
|
|
1577
|
-
});
|
|
1578
|
-
return;
|
|
1579
|
-
}
|
|
1580
1529
|
if (/^\/goal(?:\s|$)/.test(input.trim())) {
|
|
1581
1530
|
await handleGoalCommand(input);
|
|
1582
1531
|
return;
|
|
@@ -1615,8 +1564,6 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1615
1564
|
getThemeMode: () => themeMode,
|
|
1616
1565
|
getResolvedTheme: () => themeResolved,
|
|
1617
1566
|
setThemeMode: applyThemeMode,
|
|
1618
|
-
toggleSidebar,
|
|
1619
|
-
setSidebarMode: applySidebarMode,
|
|
1620
1567
|
openStats: openStatsPanel,
|
|
1621
1568
|
compactionProgress: setCompaction,
|
|
1622
1569
|
});
|
|
@@ -1682,7 +1629,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1682
1629
|
setStartingSubmit(null);
|
|
1683
1630
|
}
|
|
1684
1631
|
}
|
|
1685
|
-
}, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, fillComposer, prepareSubmitDisplay, safeRegistry, safeSkillRegistry, updateDisplayMessages, queueInput, submitSteer, requestExit,
|
|
1632
|
+
}, [addMessage, agent, args.cwd, openPicker, openSessionPicker, openRewindPicker, openStatsPanel, createProvider, currentSessionFile, fillComposer, prepareSubmitDisplay, safeRegistry, safeSkillRegistry, updateDisplayMessages, queueInput, submitSteer, requestExit, setStartingSubmit]);
|
|
1686
1633
|
// Drain the queue once the run ends and no modal needs the user first.
|
|
1687
1634
|
// The placeholder row is removed right before resubmitting — handleSubmit
|
|
1688
1635
|
// renders the message again as a regular user row.
|
|
@@ -1747,81 +1694,80 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1747
1694
|
// tail, pickers, composer, footer) occupies the live region. Letting it size
|
|
1748
1695
|
// to its content keeps the composer pinned just below the latest output the
|
|
1749
1696
|
// way ordinary shell programs do.
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
}, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, localSlashCommands: [...INK_LOCAL_SLASH_COMMANDS], terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, backgroundColor: palette.background, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({ mode: permissionMode, goalLine }) }) }))] }), sidebarVisible && (_jsx(InkSidebar, { width: sidebarWidth, agent: agent, sessionManager: sessionManager, cwd: args.cwd, mode: permissionMode, goalLine: goalLine, todos: todos, mcpManager: mcpManager, lspService: lspService }))] }) }));
|
|
1697
|
+
const mainWidth = Math.max(40, terminalColumns);
|
|
1698
|
+
return (_jsx(ThemeProvider, { value: palette, children: _jsxs(Box, { flexDirection: "column", width: mainWidth, backgroundColor: palette.background, children: [_jsx(MessageList, { messages: messages, streamingContent: streamingContent, streamingReasoning: streamingReasoning, streamingTools: streamingTools, streamingParts: streamingParts, terminalColumns: mainWidth, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: approvalHint, nowTick: nowTick, welcomeBanner: welcomeBannerNode, staticGeneration: staticGeneration, paddingX: 1, maxStreamRows: Math.max(6, terminalRows - 10) }), pickerMode === "model" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ModelPicker, { registry: safeRegistry, current: agent.model, currentThinkingLevel: thinkingLevel, recent: userConfig.getRecentModels(), onSelect: handleModelSelect, onCancel: closePicker }) })), pickerMode === "provider" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1699
|
+
.filter((p) => isUserVisibleProvider(p.id))
|
|
1700
|
+
.map((p) => {
|
|
1701
|
+
const configured = safeRegistry.getConfigured().find((item) => item.id === p.id);
|
|
1702
|
+
const configuredLabel = configured?.apiKey ? "configured" : "needs key";
|
|
1703
|
+
return {
|
|
1704
|
+
id: p.id,
|
|
1705
|
+
name: `${p.name} [${configuredLabel}]`,
|
|
1706
|
+
enabled: true,
|
|
1707
|
+
};
|
|
1708
|
+
}), current: currentProviderId, onSelect: handleProviderSelect, onCancel: closePicker }) })), pickerMode === "provider-add" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1709
|
+
.filter((p) => isUserVisibleProvider(p.id))
|
|
1710
|
+
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleProviderAddSelect, onCancel: closePicker, title: "Add Provider" }) })), pickerMode === "login" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: BUILTIN_PROVIDERS
|
|
1711
|
+
.filter((p) => isUserVisibleProvider(p.id) && safeRegistry.supportsOAuth(p.id))
|
|
1712
|
+
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLoginProviderSelect, onCancel: closePicker, title: "Select Login Provider" }) })), pickerMode === "logout" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ProviderPicker, { providers: safeRegistry.getConfigured()
|
|
1713
|
+
.filter((p) => safeRegistry.getAuthStorage().has(p.id))
|
|
1714
|
+
.map((p) => ({ id: p.id, name: p.name, enabled: true })), current: currentProviderId, onSelect: handleLogoutProviderSelect, onCancel: closePicker, title: "Select Logout Provider" }) })), pickerMode === "key" && keyTarget && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(KeyPicker, { providerName: keyTarget.name, onSubmit: handleKeySubmit, onCancel: () => {
|
|
1715
|
+
closePicker();
|
|
1716
|
+
setKeyProviderId(null);
|
|
1717
|
+
} }) })), pickerMode === "skill" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SkillPicker, { skills: safeSkillRegistry.summaries(), onSelect: (name) => {
|
|
1718
|
+
fillComposer(`/${name} `);
|
|
1719
|
+
closePicker();
|
|
1720
|
+
}, onCancel: closePicker }) })), pickerMode === "slash" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(CommandPalette, { items: commandPaletteItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
|
|
1721
|
+
closePicker();
|
|
1722
|
+
if (item.action === "insert-skill") {
|
|
1723
|
+
fillComposer(`/${item.value} `);
|
|
1724
|
+
}
|
|
1725
|
+
else {
|
|
1726
|
+
void handleSubmit(item.command);
|
|
1727
|
+
}
|
|
1728
|
+
}, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
|
|
1729
|
+
closePicker();
|
|
1730
|
+
void handleSubmit(item.command);
|
|
1731
|
+
}, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "agents" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SubagentInspector, { groups: subagentGroups, onCancel: closePicker }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
|
|
1732
|
+
closePicker();
|
|
1733
|
+
void handleSubmit(command);
|
|
1734
|
+
}, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
1735
|
+
closePicker();
|
|
1736
|
+
addMessage("assistant", summary);
|
|
1737
|
+
}, onCancel: () => {
|
|
1738
|
+
closePicker();
|
|
1739
|
+
addMessage("assistant", "已取消 Feishu setup。");
|
|
1740
|
+
} }) })), statsPanel && !pickerMode && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(StatsPanel, { panel: statsPanel, terminalColumns: mainWidth, terminalRows: terminalRows, onRangeChange: (range) => setStatsPanel((current) => current ? { ...current, range } : current), onCancel: closeStatsPanel }) })), todos.length > 0 && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(TodosPanel, { todos: todos, terminalColumns: terminalColumns }) })), pendingPlan && !pickerMode && !statsPanel && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(PlanConfirm, { initialPlan: pendingPlan.plan, onApprove: (finalPlan) => {
|
|
1741
|
+
const resolve = pendingPlan.resolve;
|
|
1742
|
+
setPendingPlan(null);
|
|
1743
|
+
resolve({ action: "approve", plan: finalPlan });
|
|
1744
|
+
}, onReject: (reason) => {
|
|
1745
|
+
const resolve = pendingPlan.resolve;
|
|
1746
|
+
setPendingPlan(null);
|
|
1747
|
+
resolve({ action: "reject", reason });
|
|
1748
|
+
} }) })), pendingApproval && !pickerMode && !statsPanel && !pendingPlan && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(ApprovalDialog, { request: pendingApproval.request, onDecision: (decision) => {
|
|
1749
|
+
const resolve = pendingApproval.resolve;
|
|
1750
|
+
setPendingApproval(null);
|
|
1751
|
+
resolve(decision);
|
|
1752
|
+
}, onAllowBashPrefix: (prefix) => {
|
|
1753
|
+
bashAllowlist?.add(prefix);
|
|
1754
|
+
} }) })), pendingQuestion && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingFeedback && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(QuestionDialog, { request: pendingQuestion, onSubmit: (answers) => {
|
|
1755
|
+
questionController?.reply(pendingQuestion.id, answers);
|
|
1756
|
+
setPendingQuestion(null);
|
|
1757
|
+
}, onCancel: () => {
|
|
1758
|
+
questionController?.reject(pendingQuestion.id);
|
|
1759
|
+
setPendingQuestion(null);
|
|
1760
|
+
} }) })), pendingFeedback && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeedbackDialog, { base: pendingFeedback.base, initialDescription: pendingFeedback.initialDescription, onDismiss: () => setPendingFeedback(null), onResult: (result) => {
|
|
1761
|
+
if (result.kind === "success") {
|
|
1762
|
+
addMessage("assistant", `Feedback submitted: ${result.url}`);
|
|
1763
|
+
}
|
|
1764
|
+
else if (result.kind === "error") {
|
|
1765
|
+
addMessage("error", `Feedback failed: ${result.message}`);
|
|
1766
|
+
}
|
|
1767
|
+
} }) })), !isExiting && compaction && (_jsx(Box, { flexShrink: 0, backgroundColor: palette.background, children: _jsx(CompactionProgressCard, { progress: compaction }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
|
|
1768
|
+
if (subagentMembers.length > 0 && !pickerMode)
|
|
1769
|
+
setSubagentEntryFocused(true);
|
|
1770
|
+
}, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, localSlashCommands: [...INK_LOCAL_SLASH_COMMANDS], terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, backgroundColor: palette.background, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({ mode: permissionMode, goalLine }) }) }))] }) }));
|
|
1825
1771
|
}
|
|
1826
1772
|
function buildCommandPaletteItems(skillRegistry) {
|
|
1827
1773
|
const items = new Map();
|
|
@@ -2109,82 +2055,6 @@ function StatsPanel({ panel, terminalColumns, terminalRows, onRangeChange, onCan
|
|
|
2109
2055
|
return (_jsx(Text, { color: heading ? theme.accent : undefined, bold: heading, children: line || " " }, key));
|
|
2110
2056
|
}) }), maxScroll > 0 && (_jsxs(Text, { color: theme.muted, children: [scroll + 1, "-", Math.min(lines.length, scroll + maxVisible), " of ", lines.length] }))] }));
|
|
2111
2057
|
}
|
|
2112
|
-
function summarizeMcpStates(states) {
|
|
2113
|
-
const summary = { connected: 0, starting: 0, failed: 0, disabled: 0, tools: 0 };
|
|
2114
|
-
for (const state of states) {
|
|
2115
|
-
if (state.status.kind === "connected") {
|
|
2116
|
-
summary.connected += 1;
|
|
2117
|
-
summary.tools += state.status.tools.length;
|
|
2118
|
-
}
|
|
2119
|
-
else if (state.status.kind === "failed") {
|
|
2120
|
-
summary.failed += 1;
|
|
2121
|
-
}
|
|
2122
|
-
else {
|
|
2123
|
-
summary.disabled += 1;
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
return summary;
|
|
2127
|
-
}
|
|
2128
|
-
function summarizeLspStatuses(statuses) {
|
|
2129
|
-
const summary = { connected: 0, starting: 0, failed: 0, disabled: 0 };
|
|
2130
|
-
for (const status of statuses) {
|
|
2131
|
-
if (status.status === "connected")
|
|
2132
|
-
summary.connected += 1;
|
|
2133
|
-
else if (status.status === "starting")
|
|
2134
|
-
summary.starting += 1;
|
|
2135
|
-
else
|
|
2136
|
-
summary.failed += 1;
|
|
2137
|
-
}
|
|
2138
|
-
return summary;
|
|
2139
|
-
}
|
|
2140
|
-
function formatStatusCount(summary) {
|
|
2141
|
-
const parts = [];
|
|
2142
|
-
if (summary.connected > 0)
|
|
2143
|
-
parts.push(`${summary.connected} up`);
|
|
2144
|
-
if (summary.starting > 0)
|
|
2145
|
-
parts.push(`${summary.starting} starting`);
|
|
2146
|
-
if (summary.failed > 0)
|
|
2147
|
-
parts.push(`${summary.failed} failed`);
|
|
2148
|
-
if (summary.disabled > 0)
|
|
2149
|
-
parts.push(`${summary.disabled} disabled`);
|
|
2150
|
-
return parts.join(" · ") || "none";
|
|
2151
|
-
}
|
|
2152
|
-
function SidebarSection({ title, children }) {
|
|
2153
|
-
const theme = useTheme();
|
|
2154
|
-
return (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { color: theme.accent, bold: true, children: title }), children] }));
|
|
2155
|
-
}
|
|
2156
|
-
function SidebarRow({ label, value, color, }) {
|
|
2157
|
-
const theme = useTheme();
|
|
2158
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { color: theme.muted, children: [label, ": "] }), _jsx(Text, { color: color ?? theme.userMessageText, children: value })] }));
|
|
2159
|
-
}
|
|
2160
|
-
function InkSidebar({ width, agent, sessionManager, cwd, mode, goalLine, todos, mcpManager, lspService, }) {
|
|
2161
|
-
const theme = useTheme();
|
|
2162
|
-
const innerWidth = Math.max(12, width - 4);
|
|
2163
|
-
const todoCounts = todos.reduce((acc, todo) => {
|
|
2164
|
-
acc[todo.status] = (acc[todo.status] ?? 0) + 1;
|
|
2165
|
-
return acc;
|
|
2166
|
-
}, {});
|
|
2167
|
-
const todoSummary = todos.length === 0
|
|
2168
|
-
? "none"
|
|
2169
|
-
: [
|
|
2170
|
-
todoCounts.in_progress ? `${todoCounts.in_progress} active` : "",
|
|
2171
|
-
todoCounts.pending ? `${todoCounts.pending} pending` : "",
|
|
2172
|
-
todoCounts.completed ? `${todoCounts.completed} done` : "",
|
|
2173
|
-
].filter(Boolean).join(" · ");
|
|
2174
|
-
const mcpStates = mcpManager?.getStates() ?? [];
|
|
2175
|
-
const mcpSummary = summarizeMcpStates(mcpStates);
|
|
2176
|
-
const lspSummary = lspService?.isDisabled()
|
|
2177
|
-
? { connected: 0, starting: 0, failed: 0, disabled: 1 }
|
|
2178
|
-
: summarizeLspStatuses(lspService?.status() ?? []);
|
|
2179
|
-
const latestMcpFailure = mcpStates.find((state) => state.status.kind === "failed");
|
|
2180
|
-
const latestLspFailure = lspService?.status().find((status) => status.status === "error");
|
|
2181
|
-
const sessionTitle = truncate(sessionDisplayName(sessionManager), innerWidth);
|
|
2182
|
-
const modelLabel = agent.model ? displayModel(agent.model) : "not selected";
|
|
2183
|
-
const route = agent.providerId
|
|
2184
|
-
? `${agent.providerId}/${modelLabel}`
|
|
2185
|
-
: modelLabel;
|
|
2186
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, height: "100%", borderStyle: "single", borderColor: theme.border, paddingX: 1, paddingY: 1, flexShrink: 0, children: [_jsx(Text, { color: theme.borderActive, bold: true, children: "Session" }), _jsx(Text, { color: theme.userMessageText, children: sessionTitle }), _jsx(Text, { color: theme.muted, children: truncate(friendlyCwd(cwd), innerWidth) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(SidebarSection, { title: "Runtime", children: [_jsx(SidebarRow, { label: "model", value: truncate(route, innerWidth - 7) }), _jsx(SidebarRow, { label: "mode", value: mode, color: mode === "bypassPermissions" ? theme.warning : theme.userMessageText }), _jsx(SidebarRow, { label: "thinking", value: agent.thinking || "off" })] }), goalLine && (_jsx(SidebarSection, { title: "Goal", children: _jsx(Text, { color: theme.userMessageText, children: truncate(goalLine, innerWidth) }) })), _jsx(SidebarSection, { title: "Todos", children: _jsx(Text, { color: todos.length > 0 ? theme.userMessageText : theme.muted, children: truncate(todoSummary, innerWidth) }) }), _jsxs(SidebarSection, { title: "MCP", children: [_jsx(Text, { color: mcpSummary.failed > 0 ? theme.warning : theme.userMessageText, children: truncate(`${formatStatusCount(mcpSummary)}${mcpSummary.tools > 0 ? ` · ${mcpSummary.tools} tools` : ""}`, innerWidth) }), latestMcpFailure?.status.kind === "failed" && (_jsx(Text, { color: theme.muted, children: truncate(latestMcpFailure.status.error, innerWidth) }))] }), _jsxs(SidebarSection, { title: "LSP", children: [_jsx(Text, { color: lspSummary.failed > 0 ? theme.warning : theme.userMessageText, children: truncate(formatStatusCount(lspSummary), innerWidth) }), latestLspFailure?.message && (_jsx(Text, { color: theme.muted, children: truncate(latestLspFailure.message, innerWidth) }))] })] })] }));
|
|
2187
|
-
}
|
|
2188
2058
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
2189
2059
|
const GENERIC_PHRASES = [
|
|
2190
2060
|
"mapping the workspace",
|
|
@@ -47,6 +47,9 @@ export declare function resolveCursorRowCompensation(input: {
|
|
|
47
47
|
viewportRows: number;
|
|
48
48
|
previousOutputHeight: number | null;
|
|
49
49
|
}): number;
|
|
50
|
+
export declare function isCtrlLetterInput(input: string, key: {
|
|
51
|
+
ctrl?: boolean;
|
|
52
|
+
}, letter: string): boolean;
|
|
50
53
|
export declare function isCtrlCInput(input: string, key: {
|
|
51
54
|
ctrl?: boolean;
|
|
52
55
|
}): boolean;
|
|
@@ -37,8 +37,15 @@ export function resolveCursorRowCompensation(input) {
|
|
|
37
37
|
return input.previousRowCompensation;
|
|
38
38
|
return needsCursorRowCompensation(input.nextOutputHeight, input.viewportRows, input.previousOutputHeight) ? 1 : 0;
|
|
39
39
|
}
|
|
40
|
+
export function isCtrlLetterInput(input, key, letter) {
|
|
41
|
+
const normalized = letter.toLowerCase();
|
|
42
|
+
if (!/^[a-z]$/.test(normalized))
|
|
43
|
+
return false;
|
|
44
|
+
const rawControlInput = String.fromCharCode(normalized.charCodeAt(0) - 96);
|
|
45
|
+
return input === rawControlInput || (key.ctrl === true && input.toLowerCase() === normalized);
|
|
46
|
+
}
|
|
40
47
|
export function isCtrlCInput(input, key) {
|
|
41
|
-
return input
|
|
48
|
+
return isCtrlLetterInput(input, key, "c");
|
|
42
49
|
}
|
|
43
50
|
export function shouldUseLineComposerFrame(_background) {
|
|
44
51
|
return true;
|
package/dist/tui-ink/markdown.js
CHANGED
|
@@ -5,7 +5,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
5
5
|
*/
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { Box, Text } from "ink";
|
|
8
|
-
import { visualWidth, graphemeWidth } from "./width.js";
|
|
8
|
+
import { ambiguousIsWide, visualWidth, graphemeWidth } from "./width.js";
|
|
9
9
|
import { useTerminalSize } from "./use-terminal-size.js";
|
|
10
10
|
import { useTheme } from "./theme.js";
|
|
11
11
|
import { highlightCode, highlightCodeSync } from "./code-highlight.js";
|
|
@@ -383,6 +383,15 @@ function TableBlock({ headers, rows, maxWidth, }) {
|
|
|
383
383
|
// Reserve a buffer so the table fits even when wrapped inside an indented
|
|
384
384
|
// box (e.g. the timeline gutter contributes marginLeft + "● " = 5 cells).
|
|
385
385
|
const budget = Math.max(20, (maxWidth ?? termWidth) - 8);
|
|
386
|
+
// Box-drawing ─│┌┬┼… are East Asian *Ambiguous*-width: on a terminal that
|
|
387
|
+
// renders them 2 cells wide, border rows would paint at twice the width the
|
|
388
|
+
// cell rows were budgeted for (and twice what Ink itself measures), so the
|
|
389
|
+
// terminal hard-wraps them into scattered fragments. There is no way to hit
|
|
390
|
+
// odd widths with 2-cell dashes, so on such terminals draw ASCII borders —
|
|
391
|
+
// the only glyphs whose width every layer agrees on.
|
|
392
|
+
const g = ambiguousIsWide()
|
|
393
|
+
? { h: "-", v: "|", tl: "+", tm: "+", tr: "+", ml: "+", mm: "+", mr: "+", bl: "+", bm: "+", br: "+" }
|
|
394
|
+
: { h: "─", v: "│", tl: "┌", tm: "┬", tr: "┐", ml: "├", mm: "┼", mr: "┤", bl: "└", bm: "┴", br: "┘" };
|
|
386
395
|
const maxWidths = headers.map((h, i) => {
|
|
387
396
|
let max = visualWidth(inlinePlainText(h));
|
|
388
397
|
for (const row of rows) {
|
|
@@ -399,11 +408,26 @@ function TableBlock({ headers, rows, maxWidth, }) {
|
|
|
399
408
|
const available = Math.max(budget - separatorsWidth, colCount * 4);
|
|
400
409
|
const ratio = totalInnerWidth > 0 ? available / totalInnerWidth : 1;
|
|
401
410
|
widths = maxWidths.map((w) => Math.max(4, Math.floor(w * ratio)));
|
|
411
|
+
// The 4-cell floor can push the sum back above `available`; shave the
|
|
412
|
+
// overshoot off the widest columns so the row never exceeds the budget
|
|
413
|
+
// and gets hard-wrapped by the terminal.
|
|
414
|
+
let excess = widths.reduce((a, b) => a + b, 0) - available;
|
|
415
|
+
while (excess > 0) {
|
|
416
|
+
let widest = -1;
|
|
417
|
+
for (let i = 0; i < widths.length; i++) {
|
|
418
|
+
if (widths[i] > 4 && (widest === -1 || widths[i] > widths[widest]))
|
|
419
|
+
widest = i;
|
|
420
|
+
}
|
|
421
|
+
if (widest === -1)
|
|
422
|
+
break;
|
|
423
|
+
widths[widest] -= 1;
|
|
424
|
+
excess -= 1;
|
|
425
|
+
}
|
|
402
426
|
}
|
|
403
|
-
const top =
|
|
404
|
-
const mid =
|
|
405
|
-
const bot =
|
|
406
|
-
const renderRow = (cells, keyPrefix, isHeader = false) => (_jsxs(Text, { children: [
|
|
427
|
+
const top = g.tl + widths.map((w) => g.h.repeat(w + 2)).join(g.tm) + g.tr;
|
|
428
|
+
const mid = g.ml + widths.map((w) => g.h.repeat(w + 2)).join(g.mm) + g.mr;
|
|
429
|
+
const bot = g.bl + widths.map((w) => g.h.repeat(w + 2)).join(g.bm) + g.br;
|
|
430
|
+
const renderRow = (cells, keyPrefix, isHeader = false) => (_jsxs(Text, { children: [`${g.v} `, cells.map((c, i) => (_jsxs(React.Fragment, { children: [renderTableCell(c, widths[i] ?? 4, isHeader, `${keyPrefix}-cell-${i}`), i < colCount - 1 ? ` ${g.v} ` : ` ${g.v}`] }, i)))] }, keyPrefix));
|
|
407
431
|
return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { children: top }), renderRow(headers, "header", true), _jsx(Text, { children: mid }), rows.map((row, ri) => renderRow(row, `row-${ri}`)), _jsx(Text, { children: bot })] }));
|
|
408
432
|
}
|
|
409
433
|
function renderTableCell(cell, width, isHeader, keyPrefix) {
|
|
@@ -417,9 +441,12 @@ function renderTableCell(cell, width, isHeader, keyPrefix) {
|
|
|
417
441
|
function truncateInlineSegments(segments, width) {
|
|
418
442
|
if (inlineSegmentsWidth(segments) <= width)
|
|
419
443
|
return segments;
|
|
420
|
-
|
|
444
|
+
// The ellipsis is itself ambiguous-width (2 cells on an ambiguous-wide
|
|
445
|
+
// terminal) — reserve its real width or every truncated cell overflows.
|
|
446
|
+
const ellipsisWidth = graphemeWidth("…");
|
|
447
|
+
if (width <= ellipsisWidth)
|
|
421
448
|
return [{ text: "…" }];
|
|
422
|
-
const target = width -
|
|
449
|
+
const target = width - ellipsisWidth;
|
|
423
450
|
const output = [];
|
|
424
451
|
let used = 0;
|
|
425
452
|
for (const segment of segments) {
|
|
@@ -18,7 +18,6 @@ interface MessageListProps {
|
|
|
18
18
|
streamingParts: DisplayMessagePart[];
|
|
19
19
|
terminalColumns: number;
|
|
20
20
|
showThinking?: boolean;
|
|
21
|
-
expandedToolOutput?: boolean;
|
|
22
21
|
verboseTrace: boolean;
|
|
23
22
|
pendingApproval?: PendingApprovalHint | null;
|
|
24
23
|
/** Animation tick used to refresh in-progress elapsed counters. */
|
|
@@ -42,5 +41,5 @@ interface MessageListProps {
|
|
|
42
41
|
*/
|
|
43
42
|
maxStreamRows?: number;
|
|
44
43
|
}
|
|
45
|
-
export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking,
|
|
44
|
+
export declare function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration, paddingX, maxStreamRows, }: MessageListProps): import("react/jsx-runtime").JSX.Element;
|
|
46
45
|
export {};
|
|
@@ -12,7 +12,7 @@ import { latestSubagentNote, sortSubagents, subagentDescriptor, subagentLabel, s
|
|
|
12
12
|
import { sanitizeInternalReminderBlocks } from "../agent/internal-reminder-sanitizer.js";
|
|
13
13
|
import { splitImageDisplayContent } from "../tui/image-display.js";
|
|
14
14
|
const EXECUTE_COMMAND_BLOCK_MAX_LINES = 4;
|
|
15
|
-
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false,
|
|
15
|
+
export function MessageList({ messages, streamingContent, streamingReasoning, streamingTools, streamingParts, terminalColumns, showThinking = false, verboseTrace, pendingApproval, nowTick, welcomeBanner, staticGeneration = 0, paddingX = 1, maxStreamRows, }) {
|
|
16
16
|
const theme = useTheme();
|
|
17
17
|
const hasStreaming = !!(streamingContent ||
|
|
18
18
|
streamingReasoning ||
|
|
@@ -47,8 +47,8 @@ export function MessageList({ messages, streamingContent, streamingReasoning, st
|
|
|
47
47
|
if (item.kind === "welcome") {
|
|
48
48
|
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: welcomeBanner }, item.key));
|
|
49
49
|
}
|
|
50
|
-
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking,
|
|
51
|
-
} }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking,
|
|
50
|
+
return (_jsx(Box, { flexDirection: "column", paddingX: paddingX, children: _jsx(MessageItem, { message: item.message, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, showExpandHint: item.showExpandHint, separateFromPrevious: item.separateFromPrevious }) }, item.key));
|
|
51
|
+
} }, `transcript-${staticGeneration}`), hasDynamic && (_jsxs(DynamicClamp, { maxRows: clampDynamic ? maxStreamRows : undefined, paddingX: paddingX, children: [hasStreaming && (_jsx(StreamingMessage, { content: streamingContent, reasoning: streamingReasoning, tools: streamingTools, parts: streamingParts, terminalColumns: terminalColumns, showThinking: showThinking, verboseTrace: verboseTrace, pendingApproval: pendingApproval, nowTick: nowTick })), pendingSteerMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: pendingSteerMessages, terminalColumns: terminalColumns, title: "Messages to steer at next model call", hint: "applies before the next provider request", bulletColor: theme.warning })), queuedInputMessages.length > 0 && (_jsx(PendingInputMessagesBlock, { messages: queuedInputMessages, terminalColumns: terminalColumns, title: "Messages queued for next turn", hint: "runs after the current answer", bulletColor: theme.muted }))] }))] }));
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Bounds the live (in-progress turn) region to at most `maxRows` rows, pinned
|
|
@@ -99,7 +99,7 @@ function DynamicClamp({ maxRows, paddingX, children, }) {
|
|
|
99
99
|
// append-only (compaction reuses already-compacted instances), keys are
|
|
100
100
|
// stable, and nowTick is only threaded to the last row, so memo hits for all
|
|
101
101
|
// settled history rows.
|
|
102
|
-
const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking,
|
|
102
|
+
const MessageItem = React.memo(function MessageItem({ message, terminalColumns, showThinking, verboseTrace, showExpandHint, separateFromPrevious, nowTick, }) {
|
|
103
103
|
const theme = useTheme();
|
|
104
104
|
if (message.role === "user") {
|
|
105
105
|
return (_jsx(UserMessageBlock, { content: message.content, terminalColumns: terminalColumns, inputStatus: message.inputStatus, separateFromPrevious: separateFromPrevious }));
|
|
@@ -139,9 +139,9 @@ const MessageItem = React.memo(function MessageItem({ message, terminalColumns,
|
|
|
139
139
|
message.taskElapsedMs !== undefined;
|
|
140
140
|
if (!hasVisibleAssistantContent)
|
|
141
141
|
return null;
|
|
142
|
-
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns,
|
|
142
|
+
return (_jsxs(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: [visibleReasoning && (showThinking || verboseTrace) && _jsx(ReasoningTraceBlock, { reasoning: visibleReasoning }), message.parts && message.parts.length > 0 ? (_jsx(MessageParts, { parts: message.parts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })) : (_jsxs(_Fragment, { children: [message.toolCalls && (_jsx(ToolsPart, { toolCalls: message.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: undefined, showExpandHint: showExpandHint, nowTick: nowTick })), visibleContent.trim() && _jsx(MarkdownContent, { content: visibleContent })] })), verboseTrace && message.toolCalls && message.toolCalls.length > 0 && (_jsx(TurnDigest, { toolCalls: message.toolCalls })), message.taskElapsedMs !== undefined && (_jsx(TaskDurationLine, { elapsedMs: message.taskElapsedMs }))] }));
|
|
143
143
|
});
|
|
144
|
-
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking,
|
|
144
|
+
function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, showThinking, verboseTrace, pendingApproval, nowTick, }) {
|
|
145
145
|
const deferredContent = React.useDeferredValue(content);
|
|
146
146
|
const deferredReasoning = React.useDeferredValue(reasoning);
|
|
147
147
|
const deferredParts = React.useDeferredValue(parts);
|
|
@@ -155,16 +155,16 @@ function StreamingMessage({ content, reasoning, tools, parts, terminalColumns, s
|
|
|
155
155
|
// turn commits — no spacing jump at finalize time. (The old marginTop=0
|
|
156
156
|
// was a flicker mitigation for the main-screen <Static> renderer; the
|
|
157
157
|
// alt-screen viewport repaints frames atomically, so it's obsolete.)
|
|
158
|
-
_jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns,
|
|
158
|
+
_jsx(Box, { marginTop: 1, marginBottom: 1, flexDirection: "column", children: _jsx(MessageParts, { parts: visibleParts, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: true, nowTick: nowTick, showActivity: true, streaming: true }) }))] }));
|
|
159
159
|
}
|
|
160
|
-
function MessageParts({ parts, terminalColumns,
|
|
160
|
+
function MessageParts({ parts, terminalColumns, verboseTrace, pendingApproval, showExpandHint, nowTick, showActivity = false, streaming = false, }) {
|
|
161
161
|
const lastToolsPartIndex = findLastToolsPartIndex(parts);
|
|
162
162
|
const lastTextPartIndex = findLastTextPartIndex(parts);
|
|
163
163
|
return (_jsx(Box, { flexDirection: "column", children: parts.map((part, idx) => {
|
|
164
164
|
if (part.type === "text") {
|
|
165
165
|
return (_jsx(TimelineText, { content: part.content, compactTop: idx === 0, terminalColumns: terminalColumns, streaming: streaming && idx === lastTextPartIndex }, `text-${idx}`));
|
|
166
166
|
}
|
|
167
|
-
return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns,
|
|
167
|
+
return (_jsx(ToolsPart, { toolCalls: part.toolCalls, terminalColumns: terminalColumns, verboseTrace: verboseTrace, pendingApproval: pendingApproval, showExpandHint: showExpandHint && idx === lastToolsPartIndex, compactTop: idx === 0, nowTick: nowTick, showActivity: showActivity && idx === lastToolsPartIndex }, `tools-${idx}`));
|
|
168
168
|
}) }));
|
|
169
169
|
}
|
|
170
170
|
function findLastTextPartIndex(parts) {
|
|
@@ -195,17 +195,16 @@ function TimelineText({ content, compactTop, terminalColumns, streaming = false,
|
|
|
195
195
|
const trimmed = visible.trim();
|
|
196
196
|
return (_jsxs(Box, { marginLeft: 2, marginTop: compactTop ? 0 : 1, children: [_jsx(Text, { color: theme.agent, children: "\u25CF " }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: streaming ? (_jsx(StreamingMarkdown, { content: trimmed, maxWidth: available })) : (_jsx(MarkdownContent, { content: trimmed, maxWidth: available })) })] }));
|
|
197
197
|
}
|
|
198
|
-
function ToolsPart({ toolCalls, terminalColumns,
|
|
198
|
+
function ToolsPart({ toolCalls, terminalColumns, verboseTrace, pendingApproval, showExpandHint, compactTop = false, nowTick, showActivity = false, }) {
|
|
199
199
|
if (toolCalls.length === 0)
|
|
200
200
|
return null;
|
|
201
|
-
|
|
202
|
-
if (!expandTools) {
|
|
201
|
+
if (!verboseTrace) {
|
|
203
202
|
return (_jsx(TraceGroupList, { toolCalls: toolCalls, terminalColumns: terminalColumns, pendingApproval: pendingApproval, nowTick: nowTick, compactTop: compactTop, showActivity: showActivity }));
|
|
204
203
|
}
|
|
205
204
|
const lastIdx = toolCalls.length - 1;
|
|
206
205
|
return (_jsx(Box, { flexDirection: "column", children: toolCalls.map((tc, idx) => {
|
|
207
206
|
const isWaitingApproval = isToolPending(tc) && !!pendingApproval && approvalMatchesTool(pendingApproval, tc);
|
|
208
|
-
return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose:
|
|
207
|
+
return (_jsx(ToolCallDisplay, { toolCall: tc, isStreaming: isToolPending(tc), verbose: verboseTrace, terminalColumns: terminalColumns, showExpandHint: showExpandHint && idx === lastIdx, waitingApproval: isWaitingApproval, compactTop: idx === 0 && compactTop, nowTick: nowTick }, tc.id));
|
|
209
208
|
}) }));
|
|
210
209
|
}
|
|
211
210
|
function fallbackStreamingParts(content, tools) {
|
|
@@ -553,7 +552,7 @@ function SubagentToolDisplay({ toolCall, verbose, terminalColumns, compactTop, }
|
|
|
553
552
|
const descriptor = padVisual(truncateVisual(subagentDescriptor(subagent), descriptorWidth), descriptorWidth);
|
|
554
553
|
const note = truncateVisual(latestSubagentNote(subagent), Math.max(12, detailWidth - 16 - descriptorWidth - 10));
|
|
555
554
|
return (_jsxs(Box, { children: [_jsx(Text, { color: subagentStatusColor(status, theme), children: label }), _jsxs(Text, { color: theme.traceAction, children: [" ", descriptor] }), _jsxs(Text, { color: subagentStatusColor(status, theme), children: [" ", padVisual(status, 9)] }), note && _jsxs(Text, { color: subagent.error ? theme.error : theme.traceDetail, children: [" ", note] })] }, subagent.subAgentId ?? `${subagentLabel(subagent)}-${index}`));
|
|
556
|
-
}), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more \u00B7 Ctrl+O to expand \u00B7
|
|
555
|
+
}), omitted > 0 && (_jsxs(Text, { color: theme.muted, children: ["... ", omitted, " more \u00B7 Ctrl+O to expand \u00B7 \u2193 then Enter to inspect traces"] }))] })), subagents.length === 0 && toolCall.result && (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: hasError ? theme.error : theme.muted, children: summarizeToolResult(toolCall) }) }))] }));
|
|
557
556
|
}
|
|
558
557
|
function TruncationHint({ remaining, verbose, showExpandHint, }) {
|
|
559
558
|
const theme = useTheme();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Full-screen subagent inspector
|
|
2
|
+
* Full-screen subagent inspector opened from the subagent entry line.
|
|
3
3
|
*
|
|
4
4
|
* Two-level drill-in modeled on Claude Code's workflow view: a grouped list of
|
|
5
5
|
* subagents (each spawn_agent is one member; each agent_team/agent_batch is a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* Full-screen subagent inspector
|
|
3
|
+
* Full-screen subagent inspector opened from the subagent entry line.
|
|
4
4
|
*
|
|
5
5
|
* Two-level drill-in modeled on Claude Code's workflow view: a grouped list of
|
|
6
6
|
* subagents (each spawn_agent is one member; each agent_team/agent_batch is a
|