Merge branch 'main' into fix-ask-option-descriptions
This commit is contained in:
@@ -141,6 +141,14 @@ export interface ToolExecutionHandle {
|
||||
setExpanded(expanded: boolean): void;
|
||||
}
|
||||
|
||||
/** Drive pending-tool redraws at ~60fps so the animated border sweep is smooth.
|
||||
* The TUI already throttles at its 16ms `MIN_RENDER_INTERVAL_MS`, so this is the
|
||||
* natural upper bound and static frames diff to a no-op redraw at ~zero cost. */
|
||||
const SPINNER_RENDER_INTERVAL_MS = 16;
|
||||
/** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
|
||||
* 60fps render cadence (mirrors `Loader`). */
|
||||
const SPINNER_GLYPH_ADVANCE_MS = 80;
|
||||
|
||||
/**
|
||||
* Component that renders a tool call with its result (updateable)
|
||||
*/
|
||||
@@ -177,6 +185,7 @@ export class ToolExecutionComponent extends Container {
|
||||
// Spinner animation for partial task results
|
||||
#spinnerFrame?: number;
|
||||
#spinnerInterval?: NodeJS.Timeout;
|
||||
#lastSpinnerAdvanceAt = 0;
|
||||
// Todo write completion strikethrough reveal animation
|
||||
#todoStrikeInterval?: NodeJS.Timeout;
|
||||
// Track if args are still being streamed (for edit/write spinner)
|
||||
@@ -404,13 +413,20 @@ export class ToolExecutionComponent extends Container {
|
||||
this.#isPartial && shimmerEnabled() && (this.#toolName === "bash" || this.#toolName === "eval");
|
||||
const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
|
||||
if (needsSpinner && !this.#spinnerInterval) {
|
||||
this.#lastSpinnerAdvanceAt = performance.now();
|
||||
this.#spinnerInterval = setInterval(() => {
|
||||
const now = performance.now();
|
||||
const frameCount = theme.spinnerFrames.length;
|
||||
if (frameCount === 0) return;
|
||||
this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
|
||||
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
||||
// Redraw at ~60fps for a smooth border sweep, but only step the spinner
|
||||
// glyph at its classic ~12.5fps cadence. The TUI throttles renders at
|
||||
// 16ms and the differ drops no-op redraws, so the extra ticks are free.
|
||||
if (frameCount > 0 && now - this.#lastSpinnerAdvanceAt >= SPINNER_GLYPH_ADVANCE_MS) {
|
||||
this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
|
||||
this.#renderState.spinnerFrame = this.#spinnerFrame;
|
||||
this.#lastSpinnerAdvanceAt = now;
|
||||
}
|
||||
this.#ui.requestRender();
|
||||
}, 80);
|
||||
}, SPINNER_RENDER_INTERVAL_MS);
|
||||
} else if (!needsSpinner && this.#spinnerInterval) {
|
||||
clearInterval(this.#spinnerInterval);
|
||||
this.#spinnerInterval = undefined;
|
||||
|
||||
@@ -25,12 +25,14 @@ type AgentSessionEventKind = AgentSessionEvent["type"];
|
||||
|
||||
const IRC_MESSAGE_VISIBLE_TTL_MS = 10_000;
|
||||
|
||||
// Events that change which foreground tools are executing, or that reset a turn.
|
||||
// The eager native-scrollback rebuild mode is recomputed only on these — other
|
||||
// events (assistant text streaming, IRC, notices) leave it untouched so plain
|
||||
// streaming keeps the no-yank deferral.
|
||||
const TOOL_RENDER_MODE_EVENTS: Record<string, true> = {
|
||||
// Events that change foreground streaming state, or that reset a turn. The TUI
|
||||
// eager native-scrollback rebuild mode is recomputed only on these so unrelated
|
||||
// IRC/notices/status refreshes do not toggle scrollback replay policy.
|
||||
const STREAM_RENDER_MODE_EVENTS: Record<string, true> = {
|
||||
agent_start: true,
|
||||
agent_end: true,
|
||||
message_start: true,
|
||||
message_end: true,
|
||||
tool_execution_start: true,
|
||||
tool_execution_update: true,
|
||||
tool_execution_end: true,
|
||||
@@ -46,6 +48,7 @@ export class EventController {
|
||||
#renderedCustomMessages = new Set<string>();
|
||||
#lastIntent: string | undefined = undefined;
|
||||
#backgroundToolCallIds = new Set<string>();
|
||||
#assistantMessageStreaming = false;
|
||||
#readToolCallArgs = new Map<string, Record<string, unknown>>();
|
||||
#readToolCallAssistantComponents = new Map<string, AssistantMessageComponent>();
|
||||
#lastAssistantComponent: AssistantMessageComponent | undefined = undefined;
|
||||
@@ -169,24 +172,27 @@ export class EventController {
|
||||
|
||||
const run = this.#handlers[event.type] as (e: AgentSessionEvent) => Promise<void>;
|
||||
await run(event);
|
||||
// While a foreground tool is executing, its streaming result re-renders and can
|
||||
// re-lay-out rows that already scrolled into native scrollback. Let the TUI
|
||||
// rebuild history on those offscreen edits (a snap to the tail is acceptable
|
||||
// mid-tool) instead of deferring, which would leave stale/duplicated rows.
|
||||
// Background-running tools are excluded so their late async updates — and the
|
||||
// assistant text that streams alongside them — keep the no-yank deferral;
|
||||
// agent_start resets the mode at every turn boundary.
|
||||
if (TOOL_RENDER_MODE_EVENTS[event.type]) {
|
||||
// While assistant text or a foreground tool is streaming, rows above the
|
||||
// viewport can re-layout after they have already entered native scrollback
|
||||
// (Markdown fences, wrapping, previews). Let the TUI rebuild history on
|
||||
// those offscreen edits instead of deferring, which otherwise leaves stale
|
||||
// tail rows duplicated above the live viewport.
|
||||
// Background-running tools are excluded so late async updates outside the
|
||||
// active foreground stream keep the no-yank deferral; agent_start resets
|
||||
// the mode at every turn boundary.
|
||||
if (STREAM_RENDER_MODE_EVENTS[event.type]) {
|
||||
this.#refreshToolRenderMode();
|
||||
}
|
||||
}
|
||||
|
||||
#refreshToolRenderMode(): void {
|
||||
let foregroundToolActive = false;
|
||||
for (const toolCallId of this.ctx.pendingTools.keys()) {
|
||||
if (!this.#backgroundToolCallIds.has(toolCallId)) {
|
||||
foregroundToolActive = true;
|
||||
break;
|
||||
let foregroundToolActive = this.#assistantMessageStreaming;
|
||||
if (!foregroundToolActive) {
|
||||
for (const toolCallId of this.ctx.pendingTools.keys()) {
|
||||
if (!this.#backgroundToolCallIds.has(toolCallId)) {
|
||||
foregroundToolActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.ctx.ui.setEagerNativeScrollbackRebuild(foregroundToolActive);
|
||||
@@ -196,6 +202,7 @@ export class EventController {
|
||||
this.#lastIntent = undefined;
|
||||
this.#readToolCallArgs.clear();
|
||||
this.#readToolCallAssistantComponents.clear();
|
||||
this.#assistantMessageStreaming = false;
|
||||
this.#lastAssistantComponent = undefined;
|
||||
if (this.ctx.retryEscapeHandler) {
|
||||
this.ctx.editor.onEscape = this.ctx.retryEscapeHandler;
|
||||
@@ -268,6 +275,7 @@ export class EventController {
|
||||
this.ctx.ui.requestRender();
|
||||
} else if (event.message.role === "assistant") {
|
||||
this.#lastThinkingCount = 0;
|
||||
this.#assistantMessageStreaming = true;
|
||||
this.#resetReadGroup();
|
||||
this.ctx.streamingComponent = new AssistantMessageComponent(undefined, this.ctx.hideThinkingBlock, () =>
|
||||
this.ctx.ui.requestRender(),
|
||||
@@ -414,6 +422,9 @@ export class EventController {
|
||||
|
||||
async #handleMessageEnd(event: Extract<AgentSessionEvent, { type: "message_end" }>): Promise<void> {
|
||||
if (event.message.role === "user") return;
|
||||
if (event.message.role === "assistant") {
|
||||
this.#assistantMessageStreaming = false;
|
||||
}
|
||||
if (this.ctx.streamingComponent && event.message.role === "assistant") {
|
||||
this.ctx.streamingMessage = event.message;
|
||||
let errorMessage: string | undefined;
|
||||
@@ -596,8 +607,8 @@ export class EventController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async #handleAgentEnd(_event: Extract<AgentSessionEvent, { type: "agent_end" }>): Promise<void> {
|
||||
this.#assistantMessageStreaming = false;
|
||||
if (this.ctx.loadingAnimation) {
|
||||
this.ctx.loadingAnimation.stop();
|
||||
this.ctx.loadingAnimation = undefined;
|
||||
|
||||
@@ -1425,7 +1425,7 @@ export class MCPCommandController {
|
||||
this.#showMessage(["", theme.fg("muted", `Reconnecting to "${name}"...`), ""].join("\n"));
|
||||
|
||||
try {
|
||||
const connection = await this.ctx.mcpManager.reconnectServer(name);
|
||||
const connection = await this.ctx.mcpManager.reconnectServer(name, { manual: true });
|
||||
if (connection) {
|
||||
// refreshMCPTools re-registers tools and preserves the user's prior
|
||||
// MCP tool selection. No need to call activateDiscoveredMCPTools —
|
||||
|
||||
Reference in New Issue
Block a user