feat(coding-agent): add configurable working indicator closes #3413

This commit is contained in:
Mario Zechner
2026-04-20 16:28:39 +02:00
parent 61579214c8
commit 74139c3f66
15 changed files with 248 additions and 65 deletions

View File

@@ -26,6 +26,7 @@ import {
Container,
fuzzyFilter,
Loader,
type LoaderIndicatorOptions,
Markdown,
matchesKey,
ProcessTerminal,
@@ -201,6 +202,7 @@ export class InteractiveMode {
private onInputCallback?: (text: string) => void;
private loadingAnimation: Loader | undefined = undefined;
private pendingWorkingMessage: string | undefined = undefined;
private workingIndicatorOptions: LoaderIndicatorOptions | undefined = undefined;
private readonly defaultWorkingMessage = "Working...";
private readonly defaultHiddenThinkingLabel = "Thinking...";
private hiddenThinkingLabel = this.defaultHiddenThinkingLabel;
@@ -370,11 +372,7 @@ export class InteractiveMode {
return description ? `[${sourceTag}] ${description}` : `[${sourceTag}]`;
}
private getBuiltInCommandConflictDiagnostics(extensionRunner: ExtensionRunner | undefined): ResourceDiagnostic[] {
if (!extensionRunner) {
return [];
}
private getBuiltInCommandConflictDiagnostics(extensionRunner: ExtensionRunner): ResourceDiagnostic[] {
const builtinNames = new Set(BUILTIN_SLASH_COMMANDS.map((command) => command.name));
return extensionRunner
.getRegisteredCommands()
@@ -436,13 +434,14 @@ export class InteractiveMode {
// Convert extension commands to SlashCommand format
const builtinCommandNames = new Set(slashCommands.map((c) => c.name));
const extensionCommands: SlashCommand[] = (
this.session.extensionRunner?.getRegisteredCommands().filter((cmd) => !builtinCommandNames.has(cmd.name)) ?? []
).map((cmd) => ({
name: cmd.invocationName,
description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
getArgumentCompletions: cmd.getArgumentCompletions,
}));
const extensionCommands: SlashCommand[] = this.session.extensionRunner
.getRegisteredCommands()
.filter((cmd) => !builtinCommandNames.has(cmd.name))
.map((cmd) => ({
name: cmd.invocationName,
description: this.prefixAutocompleteDescription(cmd.description, cmd.sourceInfo),
getArgumentCompletions: cmd.getArgumentCompletions,
}));
// Build skill commands from session.skills (if enabled)
this.skillCommands.clear();
@@ -1393,11 +1392,11 @@ export class InteractiveMode {
}
}
const commandDiagnostics = this.session.extensionRunner?.getCommandDiagnostics() ?? [];
const commandDiagnostics = this.session.extensionRunner.getCommandDiagnostics();
extensionDiagnostics.push(...commandDiagnostics);
extensionDiagnostics.push(...this.getBuiltInCommandConflictDiagnostics(this.session.extensionRunner));
const shortcutDiagnostics = this.session.extensionRunner?.getShortcutDiagnostics() ?? [];
const shortcutDiagnostics = this.session.extensionRunner.getShortcutDiagnostics();
extensionDiagnostics.push(...shortcutDiagnostics);
if (extensionDiagnostics.length > 0) {
@@ -1501,12 +1500,6 @@ export class InteractiveMode {
this.setupAutocomplete(this.fdPath);
const extensionRunner = this.session.extensionRunner;
if (!extensionRunner) {
this.showLoadedResources({ extensions: [], force: false });
this.showStartupNoticesIfNeeded();
return;
}
this.setupExtensionShortcuts(extensionRunner);
this.showLoadedResources({ force: false });
this.showStartupNoticesIfNeeded();
@@ -1627,6 +1620,12 @@ export class InteractiveMode {
this.ui.requestRender();
}
private setWorkingIndicator(options?: LoaderIndicatorOptions): void {
this.workingIndicatorOptions = options;
this.loadingAnimation?.setIndicator(options);
this.ui.requestRender();
}
private setHiddenThinkingLabel(label?: string): void {
this.hiddenThinkingLabel = label ?? this.defaultHiddenThinkingLabel;
for (const child of this.chatContainer.children) {
@@ -1717,6 +1716,8 @@ export class InteractiveMode {
this.setCustomEditorComponent(undefined);
this.defaultEditor.onExtensionShortcut = undefined;
this.updateTerminalTitle();
this.pendingWorkingMessage = undefined;
this.setWorkingIndicator();
if (this.loadingAnimation) {
this.loadingAnimation.setMessage(`${this.defaultWorkingMessage} (${keyText("app.interrupt")} to interrupt)`);
}
@@ -1879,9 +1880,7 @@ export class InteractiveMode {
this.pendingWorkingMessage = message;
}
},
setWorkingIndicator: () => {
// Working indicator customization not implemented in interactive mode yet.
},
setWorkingIndicator: (options) => this.setWorkingIndicator(options),
setHiddenThinkingLabel: (label) => this.setHiddenThinkingLabel(label),
setWidget: (key, content, options) => this.setExtensionWidget(key, content, options),
setFooter: (factory) => this.setExtensionFooter(factory),
@@ -2580,6 +2579,7 @@ export class InteractiveMode {
(spinner) => theme.fg("accent", spinner),
(text) => theme.fg("muted", text),
this.defaultWorkingMessage,
this.workingIndicatorOptions,
);
this.statusContainer.addChild(this.loadingAnimation);
// Apply any pending working message queued before loader existed
@@ -2927,7 +2927,7 @@ export class InteractiveMode {
}
case "custom": {
if (message.display) {
const renderer = this.session.extensionRunner?.getMessageRenderer(message.customType);
const renderer = this.session.extensionRunner.getMessageRenderer(message.customType);
const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
component.setExpanded(this.toolOutputExpanded);
this.chatContainer.addChild(component);
@@ -3531,7 +3531,6 @@ export class InteractiveMode {
if (!text.startsWith("/")) return false;
const extensionRunner = this.session.extensionRunner;
if (!extensionRunner) return false;
const spaceIndex = text.indexOf(" ");
const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
@@ -4478,9 +4477,7 @@ export class InteractiveMode {
this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
this.setupAutocomplete(this.fdPath);
const runner = this.session.extensionRunner;
if (runner) {
this.setupExtensionShortcuts(runner);
}
this.setupExtensionShortcuts(runner);
this.rebuildChatFromMessages();
dismissReloadBox(this.editor as Component);
this.showLoadedResources({
@@ -4904,19 +4901,17 @@ export class InteractiveMode {
// Add extension-registered shortcuts
const extensionRunner = this.session.extensionRunner;
if (extensionRunner) {
const shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());
if (shortcuts.size > 0) {
hotkeys += `
const shortcuts = extensionRunner.getShortcuts(this.keybindings.getEffectiveConfig());
if (shortcuts.size > 0) {
hotkeys += `
**Extensions**
| Key | Action |
|-----|--------|
`;
for (const [key, shortcut] of shortcuts) {
const description = shortcut.description ?? shortcut.extensionPath;
const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase());
hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
}
for (const [key, shortcut] of shortcuts) {
const description = shortcut.description ?? shortcut.extensionPath;
const keyDisplay = key.replace(/\b\w/g, (c) => c.toUpperCase());
hotkeys += `| \`${keyDisplay}\` | ${description} |\n`;
}
}
@@ -5011,14 +5006,12 @@ export class InteractiveMode {
const extensionRunner = this.session.extensionRunner;
// Emit user_bash event to let extensions intercept
const eventResult = extensionRunner
? await extensionRunner.emitUserBash({
type: "user_bash",
command,
excludeFromContext,
cwd: this.sessionManager.getCwd(),
})
: undefined;
const eventResult = await extensionRunner.emitUserBash({
type: "user_bash",
command,
excludeFromContext,
cwd: this.sessionManager.getCwd(),
});
// If extension returned a full result, use it directly
if (eventResult?.result) {