This commit is contained in:
2026-07-26 14:02:37 +07:00
parent bc56546b49
commit 367ebc1c7f
171 changed files with 4617 additions and 10402 deletions
@@ -16,16 +16,19 @@ export class CustomMessageComponent extends Container {
private customComponent?: Component;
private markdownTheme: MarkdownTheme;
private _expanded = false;
private outputPad: number;
constructor(
message: CustomMessage<unknown>,
customRenderer?: MessageRenderer,
markdownTheme: MarkdownTheme = getMarkdownTheme(),
outputPad = 1,
) {
super();
this.message = message;
this.customRenderer = customRenderer;
this.markdownTheme = markdownTheme;
this.outputPad = outputPad;
this.addChild(new Spacer(1));
@@ -42,6 +45,13 @@ export class CustomMessageComponent extends Container {
}
}
setOutputPad(outputPad: number): void {
if (this.outputPad !== outputPad) {
this.outputPad = outputPad;
this.rebuild();
}
}
override invalidate(): void {
super.invalidate();
this.rebuild();
@@ -58,7 +68,11 @@ export class CustomMessageComponent extends Container {
// Try custom renderer first - it handles its own styling
if (this.customRenderer) {
try {
const component = this.customRenderer(this.message, { expanded: this._expanded }, theme);
const component = this.customRenderer(
this.message,
{ expanded: this._expanded, outputPad: this.outputPad },
theme,
);
if (component) {
// Custom renderer provides its own styled component
this.customComponent = component;
@@ -36,7 +36,7 @@ function enableAll(enabledIds: EnabledIds, allIds: string[], targetIds?: string[
for (const id of targets) {
if (!result.includes(id)) result.push(id);
}
return result.length === allIds.length ? null : result;
return result.length === allIds.length && result.every((id) => allIds.includes(id)) ? null : result;
}
function clearAll(enabledIds: EnabledIds, allIds: string[], targetIds?: string[]): EnabledIds {
@@ -67,7 +67,7 @@ function getSortedIds(enabledIds: EnabledIds, allIds: string[]): string[] {
interface ModelItem {
fullId: string;
model: Model<any>;
model: Model<any> | undefined;
enabled: boolean;
}
@@ -152,20 +152,20 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
}
private buildItems(): ModelItem[] {
// Filter out IDs that no longer have a corresponding model (e.g., after logout)
return getSortedIds(this.enabledIds, this.allIds)
.filter((id) => this.modelsById.has(id))
.map((id) => ({
fullId: id,
model: this.modelsById.get(id)!,
enabled: isEnabled(this.enabledIds, id),
}));
return getSortedIds(this.enabledIds, this.allIds).map((id) => ({
fullId: id,
model: this.modelsById.get(id),
enabled: isEnabled(this.enabledIds, id),
}));
}
private getFooterText(): string {
const enabledCount = this.enabledIds?.length ?? this.allIds.length;
const enabledCount = this.enabledIds?.filter((id) => this.modelsById.has(id)).length ?? this.allIds.length;
const unavailableCount = this.enabledIds?.filter((id) => !this.modelsById.has(id)).length ?? 0;
const allEnabled = this.enabledIds === null;
const countText = allEnabled ? "all enabled" : `${enabledCount}/${this.allIds.length} enabled`;
const countText = allEnabled
? "all enabled"
: `${enabledCount}/${this.allIds.length} enabled${unavailableCount ? ` · ${unavailableCount} unavailable` : ""}`;
const parts = [
`${keyText("tui.select.confirm")} toggle`,
`${keyText("app.models.enableAll")} all`,
@@ -184,8 +184,10 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
const query = this.searchInput.getValue();
const items = this.buildItems();
this.filteredItems = query
? fuzzyFilter(items, query, (i) =>
getModelSearchText({ id: i.model.id, provider: i.model.provider, name: i.model.name }),
? fuzzyFilter(items, query, (item) =>
item.model
? getModelSearchText({ id: item.model.id, provider: item.model.provider, name: item.model.name })
: item.fullId,
)
: items;
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));
@@ -216,9 +218,16 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
const item = this.filteredItems[i]!;
const isSelected = i === this.selectedIndex;
const prefix = isSelected ? theme.fg("accent", "→ ") : " ";
const modelText = isSelected ? theme.fg("accent", item.model.id) : item.model.id;
const providerBadge = theme.fg("muted", ` [${item.model.provider}]`);
const status = allEnabled ? "" : item.enabled ? theme.fg("success", " ✓") : theme.fg("dim", " ✗");
const id = item.model?.id ?? item.fullId;
const modelText = isSelected ? theme.fg("accent", id) : id;
const providerBadge = theme.fg("muted", item.model ? ` [${item.model.provider}]` : " [unavailable]");
const status = item.model
? allEnabled
? ""
: item.enabled
? theme.fg("success", " ✓")
: theme.fg("dim", " ✗")
: theme.fg("dim", " ✗");
this.listContainer.addChild(new Text(`${prefix}${modelText}${providerBadge}${status}`, 0, 0));
}
@@ -232,7 +241,13 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
if (this.filteredItems.length > 0) {
const selected = this.filteredItems[this.selectedIndex];
this.listContainer.addChild(new Spacer(1));
this.listContainer.addChild(new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`), 0, 0));
this.listContainer.addChild(
new Text(
theme.fg("muted", ` ${selected.model ? `Model Name: ${selected.model.name}` : "Model unavailable"}`),
0,
0,
),
);
}
}
@@ -310,7 +325,7 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl
// Toggle provider of current item
if (kb.matches(data, "app.models.toggleProvider")) {
const item = this.filteredItems[this.selectedIndex];
if (item) {
if (item?.model) {
const provider = item.model.provider;
const providerIds = this.allIds.filter((id) => this.modelsById.get(id)!.provider === provider);
const allEnabled = providerIds.every((id) => isEnabled(this.enabledIds, id));
@@ -76,7 +76,12 @@ import { FooterDataProvider, type ReadonlyFooterDataProvider } from "../../core/
import { configureHttpDispatcher, formatHttpIdleTimeoutMs } from "../../core/http-dispatcher.ts";
import { type AppKeybinding, KeybindingsManager } from "../../core/keybindings.ts";
import { createCompactionSummaryMessage } from "../../core/messages.ts";
import { defaultModelPerProvider, findExactModelReferenceMatch, resolveModelScope } from "../../core/model-resolver.ts";
import {
defaultModelPerProvider,
findExactModelReferenceMatch,
resolveModelScope,
resolveModelScopeWithDiagnostics,
} from "../../core/model-resolver.ts";
import { DefaultPackageManager } from "../../core/package-manager.ts";
import type { ResourceDiagnostic } from "../../core/resource-loader.ts";
import { formatMissingSessionCwdPrompt, MissingSessionCwdError } from "../../core/session-cwd.ts";
@@ -2985,6 +2990,10 @@ export class InteractiveMode {
this.ui.requestRender();
break;
case "bash_execution_update":
// The bash execution callback handles TUI output rendering.
break;
case "tool_execution_start": {
let component = this.pendingTools.get(event.toolCallId);
if (!component) {
@@ -3233,7 +3242,12 @@ export class InteractiveMode {
case "custom": {
if (message.display) {
const renderer = this.session.extensionRunner.getMessageRenderer(message.customType);
const component = new CustomMessageComponent(message, renderer, this.getMarkdownThemeWithSettings());
const component = new CustomMessageComponent(
message,
renderer,
this.getMarkdownThemeWithSettings(),
this.outputPad,
);
component.setExpanded(this.toolOutputExpanded);
this.chatContainer.addChild(component);
}
@@ -4248,7 +4262,11 @@ export class InteractiveMode {
this.outputPad = padding;
if (this.streamingComponent || this.session.isStreaming) {
for (const child of this.chatContainer.children) {
if (child instanceof AssistantMessageComponent || child instanceof UserMessageComponent) {
if (
child instanceof AssistantMessageComponent ||
child instanceof CustomMessageComponent ||
child instanceof UserMessageComponent
) {
child.setOutputPad(padding);
}
}
@@ -4459,14 +4477,20 @@ export class InteractiveMode {
// Get all available models
await this.session.modelRuntime.refresh();
const allModels = [...(await this.session.modelRuntime.getAvailable())];
const allModelIds = new Set(allModels.map((model) => `${model.provider}/${model.id}`));
const configuredPatterns = this.settingsManager.getEnabledModels();
const sessionScopedModels = this.session.scopedModels;
if (allModels.length === 0) {
if (allModels.length === 0 && !configuredPatterns?.length && sessionScopedModels.length === 0) {
this.showStatus("No models available");
return;
}
const configuredScope = configuredPatterns?.length
? await resolveModelScopeWithDiagnostics(configuredPatterns, this.session.modelRuntime)
: undefined;
// Check if session has scoped models (from previous session-only changes or CLI --models)
const sessionScopedModels = this.session.scopedModels;
const hasSessionScope = sessionScopedModels.length > 0;
// Build enabled model IDs from session state or settings
@@ -4475,19 +4499,25 @@ export class InteractiveMode {
if (hasSessionScope) {
// Use current session's scoped models
currentEnabledIds = sessionScopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
} else {
// Fall back to settings
const patterns = this.settingsManager.getEnabledModels();
if (patterns !== undefined && patterns.length > 0) {
const scopedModels = await resolveModelScope(patterns, this.session.modelRuntime);
currentEnabledIds = scopedModels.map((scoped) => `${scoped.model.provider}/${scoped.model.id}`);
}
} else if (configuredScope) {
currentEnabledIds = configuredScope.scopedModels.map(
(scoped) => `${scoped.model.provider}/${scoped.model.id}`,
);
}
for (const diagnostic of configuredScope?.diagnostics ?? []) {
if (diagnostic.code !== "no-match") continue;
currentEnabledIds ??= [];
if (!currentEnabledIds.includes(diagnostic.pattern)) currentEnabledIds.push(diagnostic.pattern);
}
// Helper to update session's scoped models (session-only, no persist)
const updateSessionModels = async (enabledIds: string[] | null) => {
currentEnabledIds = enabledIds === null ? null : [...enabledIds];
if (enabledIds && enabledIds.length > 0 && enabledIds.length < allModels.length) {
const hasEnabledAvailableModel = enabledIds?.some((id) => allModelIds.has(id)) ?? false;
const allAvailableModelsEnabled =
enabledIds !== null && [...allModelIds].every((id) => enabledIds.includes(id));
if (enabledIds && hasEnabledAvailableModel && !allAvailableModelsEnabled) {
const newScopedModels = await resolveModelScope(enabledIds, this.session.modelRuntime);
this.session.setScopedModels(
newScopedModels.map((sm) => ({
@@ -4515,10 +4545,11 @@ export class InteractiveMode {
},
onPersist: (enabledIds) => {
// Persist to settings
const newPatterns =
enabledIds === null || enabledIds.length === allModels.length
? undefined // All enabled = clear filter
: enabledIds;
const allEnabled =
enabledIds !== null &&
enabledIds.length === allModels.length &&
enabledIds.every((id) => allModelIds.has(id));
const newPatterns = enabledIds === null || allEnabled ? undefined : enabledIds;
this.settingsManager.setEnabledModels(newPatterns ? [...newPatterns] : undefined);
this.showStatus("Model selection saved to settings");
},