feat(coding-agent): added temp model controls and auto-chdir
This commit is contained in:
+33
-19
@@ -38,30 +38,40 @@ export class ExtensionDashboard extends Container {
|
||||
private inspector: InspectorPanel;
|
||||
private settingsManager: SettingsManager | null;
|
||||
private cwd: string;
|
||||
private terminalHeight: number;
|
||||
|
||||
public onClose?: () => void;
|
||||
|
||||
constructor(cwd: string, settingsManager: SettingsManager | null = null) {
|
||||
constructor(cwd: string, settingsManager: SettingsManager | null = null, terminalHeight?: number) {
|
||||
super();
|
||||
this.cwd = cwd;
|
||||
this.settingsManager = settingsManager;
|
||||
this.terminalHeight = terminalHeight ?? process.stdout.rows ?? 24;
|
||||
const disabledIds = settingsManager?.getDisabledExtensions() ?? [];
|
||||
this.state = createInitialState(cwd, disabledIds);
|
||||
|
||||
// Calculate max visible items based on terminal height
|
||||
// Reserve ~10 lines for header, tabs, help text, borders
|
||||
const maxVisible = Math.max(5, Math.floor((this.terminalHeight - 10) / 2));
|
||||
|
||||
// Create main list - always focused
|
||||
this.mainList = new ExtensionList(this.state.searchFiltered, {
|
||||
onSelectionChange: (ext) => {
|
||||
this.state.selected = ext;
|
||||
this.inspector.setExtension(ext);
|
||||
this.mainList = new ExtensionList(
|
||||
this.state.searchFiltered,
|
||||
{
|
||||
onSelectionChange: (ext) => {
|
||||
this.state.selected = ext;
|
||||
this.inspector.setExtension(ext);
|
||||
},
|
||||
onToggle: (extensionId, enabled) => {
|
||||
this.handleExtensionToggle(extensionId, enabled);
|
||||
},
|
||||
onMasterToggle: (providerId) => {
|
||||
this.handleProviderToggle(providerId);
|
||||
},
|
||||
masterSwitchProvider: this.getActiveProviderId(),
|
||||
},
|
||||
onToggle: (extensionId, enabled) => {
|
||||
this.handleExtensionToggle(extensionId, enabled);
|
||||
},
|
||||
onMasterToggle: (providerId) => {
|
||||
this.handleProviderToggle(providerId);
|
||||
},
|
||||
masterSwitchProvider: this.getActiveProviderId(),
|
||||
});
|
||||
maxVisible,
|
||||
);
|
||||
this.mainList.setFocused(true);
|
||||
|
||||
// Create inspector
|
||||
@@ -91,9 +101,10 @@ export class ExtensionDashboard extends Container {
|
||||
this.addChild(new Text(this.renderTabBar(), 0, 0));
|
||||
this.addChild(new Spacer(1));
|
||||
|
||||
// Help text
|
||||
// 2-column body
|
||||
this.addChild(new TwoColumnBody(this.mainList, this.inspector));
|
||||
// 2-column body with height limit
|
||||
// Reserve ~8 lines for header, tabs, help text, borders
|
||||
const bodyMaxHeight = Math.max(5, this.terminalHeight - 8);
|
||||
this.addChild(new TwoColumnBody(this.mainList, this.inspector, bodyMaxHeight));
|
||||
|
||||
this.addChild(new Spacer(1));
|
||||
this.addChild(new Text(theme.fg("dim", " ↑/↓: navigate Space: toggle Tab: next provider Esc: close"), 0, 0));
|
||||
@@ -262,10 +273,12 @@ export class ExtensionDashboard extends Container {
|
||||
class TwoColumnBody implements Component {
|
||||
private leftPane: ExtensionList;
|
||||
private rightPane: InspectorPanel;
|
||||
private maxHeight: number;
|
||||
|
||||
constructor(left: ExtensionList, right: InspectorPanel) {
|
||||
constructor(left: ExtensionList, right: InspectorPanel, maxHeight: number) {
|
||||
this.leftPane = left;
|
||||
this.rightPane = right;
|
||||
this.maxHeight = maxHeight;
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
@@ -275,11 +288,12 @@ class TwoColumnBody implements Component {
|
||||
const leftLines = this.leftPane.render(leftWidth);
|
||||
const rightLines = this.rightPane.render(rightWidth);
|
||||
|
||||
const maxLines = Math.max(leftLines.length, rightLines.length);
|
||||
// Limit to maxHeight lines
|
||||
const numLines = Math.min(this.maxHeight, Math.max(leftLines.length, rightLines.length));
|
||||
const combined: string[] = [];
|
||||
const separator = theme.fg("dim", ` ${theme.boxSharp.vertical} `);
|
||||
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
for (let i = 0; i < numLines; i++) {
|
||||
const left = truncateToWidth(leftLines[i] ?? "", leftWidth);
|
||||
const leftPadded = left + " ".repeat(Math.max(0, leftWidth - visibleWidth(left)));
|
||||
const right = truncateToWidth(rightLines[i] ?? "", rightWidth);
|
||||
|
||||
+15
-8
@@ -31,7 +31,7 @@ export interface ExtensionListCallbacks {
|
||||
masterSwitchProvider?: string | null;
|
||||
}
|
||||
|
||||
const MAX_VISIBLE = 25;
|
||||
const DEFAULT_MAX_VISIBLE = 15;
|
||||
|
||||
/** Flattened list item for rendering */
|
||||
type ListItem =
|
||||
@@ -48,14 +48,21 @@ export class ExtensionList implements Component {
|
||||
private focused = false;
|
||||
private callbacks: ExtensionListCallbacks;
|
||||
private masterSwitchProvider: string | null = null;
|
||||
private maxVisible: number;
|
||||
|
||||
constructor(extensions: Extension[], callbacks: ExtensionListCallbacks = {}) {
|
||||
constructor(extensions: Extension[], callbacks: ExtensionListCallbacks = {}, maxVisible?: number) {
|
||||
this.extensions = extensions;
|
||||
this.callbacks = callbacks;
|
||||
this.masterSwitchProvider = callbacks.masterSwitchProvider ?? null;
|
||||
this.maxVisible = maxVisible ?? DEFAULT_MAX_VISIBLE;
|
||||
this.rebuildList();
|
||||
}
|
||||
|
||||
setMaxVisible(maxVisible: number): void {
|
||||
this.maxVisible = maxVisible;
|
||||
this.clampSelection();
|
||||
}
|
||||
|
||||
setExtensions(extensions: Extension[]): void {
|
||||
this.extensions = extensions;
|
||||
this.rebuildList();
|
||||
@@ -126,7 +133,7 @@ export class ExtensionList implements Component {
|
||||
|
||||
// Calculate visible range
|
||||
const startIdx = this.scrollOffset;
|
||||
const endIdx = Math.min(startIdx + MAX_VISIBLE, this.listItems.length);
|
||||
const endIdx = Math.min(startIdx + this.maxVisible, this.listItems.length);
|
||||
|
||||
// Render visible items
|
||||
for (let i = startIdx; i < endIdx; i++) {
|
||||
@@ -143,7 +150,7 @@ export class ExtensionList implements Component {
|
||||
}
|
||||
|
||||
// Scroll indicator
|
||||
if (this.listItems.length > MAX_VISIBLE) {
|
||||
if (this.listItems.length > this.maxVisible) {
|
||||
const indicator = theme.fg("muted", ` (${this.selectedIndex + 1}/${this.listItems.length})`);
|
||||
lines.push(indicator);
|
||||
}
|
||||
@@ -388,8 +395,8 @@ export class ExtensionList implements Component {
|
||||
// Adjust scroll offset
|
||||
if (this.selectedIndex < this.scrollOffset) {
|
||||
this.scrollOffset = this.selectedIndex;
|
||||
} else if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
|
||||
this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
|
||||
} else if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
|
||||
this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,8 +477,8 @@ export class ExtensionList implements Component {
|
||||
private moveSelectionDown(): void {
|
||||
if (this.selectedIndex < this.listItems.length - 1) {
|
||||
this.selectedIndex++;
|
||||
if (this.selectedIndex >= this.scrollOffset + MAX_VISIBLE) {
|
||||
this.scrollOffset = this.selectedIndex - MAX_VISIBLE + 1;
|
||||
if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
|
||||
this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
|
||||
}
|
||||
this.notifySelectionChange();
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ interface ScopedModelItem {
|
||||
thinkingLevel: string;
|
||||
}
|
||||
|
||||
type ModelRole = "default" | "smol" | "slow";
|
||||
type ModelRole = "default" | "smol" | "slow" | "temporary";
|
||||
|
||||
interface MenuAction {
|
||||
label: string;
|
||||
@@ -75,6 +75,7 @@ export class ModelSelectorComponent extends Container {
|
||||
private errorMessage?: string;
|
||||
private tui: TUI;
|
||||
private scopedModels: ReadonlyArray<ScopedModelItem>;
|
||||
private temporaryOnly: boolean;
|
||||
|
||||
// Tab state
|
||||
private providers: string[] = [ALL_TAB];
|
||||
@@ -92,6 +93,7 @@ export class ModelSelectorComponent extends Container {
|
||||
scopedModels: ReadonlyArray<ScopedModelItem>,
|
||||
onSelect: (model: Model<any>, role: string) => void,
|
||||
onCancel: () => void,
|
||||
options?: { temporaryOnly?: boolean },
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -102,6 +104,7 @@ export class ModelSelectorComponent extends Container {
|
||||
this.scopedModels = scopedModels;
|
||||
this.onSelectCallback = onSelect;
|
||||
this.onCancelCallback = onCancel;
|
||||
this.temporaryOnly = options?.temporaryOnly ?? false;
|
||||
|
||||
// Load current role assignments from settings
|
||||
this._loadRoleModels();
|
||||
@@ -483,10 +486,16 @@ export class ModelSelectorComponent extends Container {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter - open context menu
|
||||
// Enter - open context menu or select directly in temporary mode
|
||||
if (isEnter(keyData)) {
|
||||
if (this.filteredModels[this.selectedIndex]) {
|
||||
this.openMenu();
|
||||
const selectedModel = this.filteredModels[this.selectedIndex];
|
||||
if (selectedModel) {
|
||||
if (this.temporaryOnly) {
|
||||
// In temporary mode, skip menu and select directly
|
||||
this.handleSelect(selectedModel.model, "temporary");
|
||||
} else {
|
||||
this.openMenu();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -536,6 +545,12 @@ export class ModelSelectorComponent extends Container {
|
||||
}
|
||||
|
||||
private handleSelect(model: Model<any>, role: ModelRole): void {
|
||||
// For temporary role, don't save to settings - just notify caller
|
||||
if (role === "temporary") {
|
||||
this.onSelectCallback(model, role);
|
||||
return;
|
||||
}
|
||||
|
||||
// Save to settings
|
||||
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user