Merge remote-tracking branch 'can1357/main' into feat/advisor-per-agent-toggle

This commit is contained in:
DarkPhilosophy
2026-07-13 23:16:26 +03:00
279 changed files with 31051 additions and 2804 deletions
@@ -67,10 +67,12 @@ import { ExtensionDashboard } from "../components/extensions";
import { HistorySearchComponent } from "../components/history-search";
import { LoginDialogComponent } from "../components/login-dialog";
import { LogoutAccountSelectorComponent } from "../components/logout-account-selector";
import { ModelHubComponent, type ModelHubMode } from "../components/model-hub";
import { ModelHubComponent } from "../components/model-hub";
import { ModelPickerComponent } from "../components/model-picker";
import { OAuthSelectorComponent } from "../components/oauth-selector";
import { PluginSelectorComponent } from "../components/plugin-selector";
import { ResetUsageSelectorComponent } from "../components/reset-usage-selector";
import { renderSegmentTrack } from "../components/segment-track";
import { SessionSelectorComponent } from "../components/session-selector";
import { SettingsSelectorComponent } from "../components/settings-selector";
import { ToolExecutionComponent } from "../components/tool-execution";
@@ -458,6 +460,13 @@ export class SelectorController {
this.ctx.rebuildChatFromMessages();
this.ctx.ui.resetDisplay();
break;
case "display.collapseCompacted":
// Rebuild swaps between the collapsed tail and the full inline
// history; full reset retires blocks already committed to native
// scrollback (mirrors cacheMissMarker).
this.ctx.rebuildChatFromMessages();
this.ctx.ui.resetDisplay();
break;
case "tui.tight":
setTuiTight(value as boolean);
this.ctx.ui.invalidate();
@@ -593,7 +602,85 @@ export class SelectorController {
}
showModelSelector(options?: { temporaryOnly?: boolean }): void {
this.#showModelHub({ mode: options?.temporaryOnly ? "pick" : "roles" });
if (options?.temporaryOnly) {
this.#showModelPicker();
return;
}
this.#showModelHub({});
}
/**
* Compact session-only model picker (alt+p / `/switch`): a floating
* bottom-anchored overlay over the transcript. The current model is
* highlighted and preselected; a leading `@` searches ctrl+p quick roles.
*/
#showModelPicker(): void {
const currentContextTokens = this.ctx.session.getContextUsage()?.tokens ?? 0;
const current = this.ctx.session.model;
const quickRoleOrder = this.ctx.settings.get("cycleOrder");
const quickRoleCycle = this.ctx.session.getRoleModelCycle(quickRoleOrder);
let overlayHandle: OverlayHandle | undefined;
let closed = false;
const done = () => {
if (closed) return;
closed = true;
overlayHandle?.hide();
this.focusActiveEditorArea();
this.ctx.ui.requestRender();
};
const picker = new ModelPickerComponent(
this.ctx.ui,
this.ctx.settings,
this.ctx.session.modelRegistry,
this.ctx.session.scopedModels,
{
onPick: async (model, selector) => {
try {
// Session-only: update agent state but don't persist the model to settings.
await this.ctx.session.setModelTemporary(model);
this.ctx.statusLine.invalidate();
this.ctx.updateEditorBorderColor();
const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
this.ctx.showStatus(`Session-only model: ${selector}. Use ${roleSelectorHint} or /model for roles.`);
done();
} catch (error) {
this.ctx.showError(error instanceof Error ? error.message : String(error));
}
},
onPickRole: async entry => {
try {
await this.ctx.session.applyRoleModel(entry);
this.ctx.statusLine.invalidate();
this.ctx.updateEditorBorderColor();
this.ctx.showModelCycleTrack(
renderSegmentTrack(
quickRoleOrder.map(role => ({ label: role })),
quickRoleOrder.indexOf(entry.role),
),
);
done();
} catch (error) {
this.ctx.showError(error instanceof Error ? error.message : String(error));
}
},
onCancel: done,
},
{
currentContextTokens,
currentSelector: current ? `${current.provider}/${current.id}` : undefined,
quickRoles: quickRoleCycle?.models,
quickRoleOrder,
currentQuickRole: quickRoleCycle?.models[quickRoleCycle.currentIndex]?.role,
},
);
overlayHandle = this.ctx.ui.showOverlay(picker, {
anchor: "bottom-center",
width: "100%",
maxHeight: "100%",
margin: 0,
});
this.ctx.ui.setFocus(picker);
this.ctx.ui.requestRender();
}
/**
@@ -602,13 +689,13 @@ export class SelectorController {
* untouched underneath. `initialProviderId` preselects a provider's sidebar
* entry — used when reopening the hub after a /login round-trip.
*/
#showModelHub(hubOptions: { mode: ModelHubMode; initialProviderId?: string }): void {
#showModelHub(hubOptions: { initialProviderId?: string }): void {
const currentContextTokens = this.ctx.session.getContextUsage()?.tokens ?? 0;
let overlayHandle: OverlayHandle | undefined;
let hub: ModelHubComponent | undefined;
let closed = false;
const done = () => {
// Re-entrant guard: cancel paths (Esc, pick, login forward) may race;
// Re-entrant guard: cancel paths (Esc, login forward) may race;
// the overlay must hide exactly once.
if (closed) return;
closed = true;
@@ -623,7 +710,7 @@ export class SelectorController {
this.ctx.session.modelRegistry,
this.ctx.session.scopedModels,
{
onAssign: async (model, role, thinkingLevel, selector, action) => {
onAssign: async (model, role, thinkingLevel, selector) => {
// `auto` is session-global: never baked into a per-role model value
// (it can't round-trip through `model:<level>`). Apply it to the session
// separately and persist via `defaultThinkingLevel`.
@@ -631,18 +718,6 @@ export class SelectorController {
const concreteThinking = isAuto || thinkingLevel === undefined ? undefined : thinkingLevel;
const selectorValue = selector ?? `${model.provider}/${model.id}`;
try {
if (action === "retryFallback") {
const fallbackSelector = formatModelSelectorValue(selectorValue, concreteThinking);
const fallbackChains = this.ctx.settings.get("retry.fallbackChains");
const chain = Array.isArray(fallbackChains[role]) ? fallbackChains[role] : [];
this.ctx.settings.set("retry.fallbackChains", {
...fallbackChains,
[role]: [fallbackSelector, ...chain.filter(existing => existing !== fallbackSelector)],
});
const roleInfo = getRoleInfo(role, settings);
this.ctx.showStatus(`${roleInfo?.name ?? role} fallback model: ${fallbackSelector}`);
return;
}
if (role === "default") {
const { switched } = await this.ctx.session.setModel(model, role, {
selector,
@@ -686,17 +761,21 @@ export class SelectorController {
this.ctx.showError(error instanceof Error ? error.message : String(error));
}
},
onPick: async (model, selector) => {
onFallbackChainChange: (role, chain) => {
try {
// Session-only: update agent state but don't persist the model to settings.
await this.ctx.session.setModelTemporary(model);
this.ctx.statusLine.invalidate();
this.ctx.updateEditorBorderColor();
const roleSelectorHint = this.ctx.keybindings.getKeys("app.model.select")[0] ?? "Alt+M";
const chains = { ...this.ctx.settings.get("retry.fallbackChains") };
if (chain.length === 0) {
delete chains[role];
} else {
chains[role] = chain;
}
this.ctx.settings.set("retry.fallbackChains", chains);
const roleInfo = getRoleInfo(role, settings);
this.ctx.showStatus(
`Session-only model: ${selector ?? model.id}. Use ${roleSelectorHint} or /model for roles.`,
chain.length > 0
? `${roleInfo?.name ?? role} fallbacks: ${chain.join(" → ")}`
: `${roleInfo?.name ?? role} fallbacks cleared`,
);
done();
} catch (error) {
this.ctx.showError(error instanceof Error ? error.message : String(error));
}
@@ -718,8 +797,6 @@ export class SelectorController {
onCancel: () => done(),
},
{
mode: hubOptions.mode,
currentContextTokens,
initialProviderId: hubOptions.initialProviderId,
},
);
@@ -738,7 +815,7 @@ export class SelectorController {
async #loginThenReopenModelHub(providerId: string): Promise<void> {
const succeeded = await this.#handleOAuthLogin(providerId);
if (succeeded) {
this.#showModelHub({ mode: "roles", initialProviderId: providerId });
this.#showModelHub({ initialProviderId: providerId });
}
}