zidane 5.0.4 → 5.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.js CHANGED
@@ -2,7 +2,7 @@ import { d as createAgent } from "./tools-CLazLRb4.js";
2
2
  import { n as formatTokenUsage } from "./stats-DZIsGqzu.js";
3
3
  import { n as loadSession, t as createSession } from "./session-791hhrFa.js";
4
4
  import { createTuiStore } from "./session/sqlite.js";
5
- import { $t as piIdOf, C as useSafeModeQueue, Ct as findGitRoot, D as isOnSafelist, E as getSafelist, Et as uniqueSkillNamesFromReferences, F as supportsOAuth, Ft as detectAuth, G as shortId, H as ageString, I as buildMcpServers, J as DEFAULT_SETTINGS, K as listProjectFiles, N as splitPromptSegments, Ot as createFilesCompletionProvider, P as runOAuthLogin, Pt as useCompletion, Q as useSettings, R as discoverProjectMcps, S as useSafeModeActions, St as toolResultText, T as addToSafelist, Tt as createSkillsCompletionProvider, U as compactPath, V as generateSessionTitle, Vt as setProviderCredential, W as fmtTokens, X as SETTINGS_TOGGLES, Y as SETTINGS_CHOICES, Yt as modelSupportsReasoning, Z as SettingsProvider, _ as discoverProjectSkills, a as ThemeProvider, b as writeSessionExport, c as useSurfaces, ct as ConfigProvider, d as finalizeStreamingMarkdown, f as finalizeStreamingMarkdownForOwner, ft as deriveSessionTitle, g as defaultSkillScanPaths, h as buildSkillsConfig, ht as listSessionMeta, i as turnAsText, j as suggestSafelistEntry, lt as useConfig, m as useStreamBuffer, mt as lastContextSizeFromTurns, n as deleteTurnSafely, nt as resolveTheme, o as useColors, p as turnContextSize, pt as eventsFromTurns, q as useEnabledToggleSet, qt as getContextWindow, r as truncateTurnsAt, s as useSelectStyle, tt as resolveChipColor, u as useTheme, ut as resolveConfig, vt as selectableTurnIds, x as SafeModeProvider, xt as toolCallPreview, yt as stripSpawnTokensLine } from "./turn-operations-BF3hMNgo.js";
5
+ import { $ as SETTINGS_TOGGLES, C as useSafeModeQueue, D as isOnSafelist, E as getSafelist, Et as findGitRoot, F as supportsOAuth, G as ageString, I as buildModelCatalog, J as shortId, K as compactPath, L as filterModelCatalog, Lt as useCompletion, N as splitPromptSegments, Ot as createSkillsCompletionProvider, P as runOAuthLogin, Q as SETTINGS_CHOICES, Qt as modelSupportsReasoning, R as indexOfEntry, Rt as detectAuth, S as useSafeModeActions, St as stripSpawnTokensLine, T as addToSafelist, Tt as toolResultText, V as discoverProjectMcps, W as generateSessionTitle, Wt as setProviderCredential, X as useEnabledToggleSet, Xt as getContextWindow, Y as listProjectFiles, Z as DEFAULT_SETTINGS, _ as discoverProjectSkills, _t as lastContextSizeFromTurns, a as ThemeProvider, at as resolveTheme, b as writeSessionExport, c as useSurfaces, d as finalizeStreamingMarkdown, dt as ConfigProvider, et as SettingsProvider, f as finalizeStreamingMarkdownForOwner, ft as useConfig, g as defaultSkillScanPaths, gt as eventsFromTurns, h as buildSkillsConfig, ht as deriveSessionTitle, i as turnAsText, it as resolveChipColor, j as suggestSafelistEntry, jt as createFilesCompletionProvider, kt as uniqueSkillNamesFromReferences, m as useStreamBuffer, n as deleteTurnSafely, nn as piIdOf, o as useColors, p as turnContextSize, pt as resolveConfig, q as fmtTokens, r as truncateTurnsAt, s as useSelectStyle, tt as useSettings, u as useTheme, vt as listSessionMeta, wt as toolCallPreview, x as SafeModeProvider, xt as selectableTurnIds, z as buildMcpServers } from "./turn-operations-BfEh-GER.js";
6
6
  import { Buffer } from "node:buffer";
7
7
  import { createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
8
8
  import { RGBA, SyntaxStyle, addDefaultParsers, createCliRenderer, defaultTextareaKeyBindings, getTreeSitterClient } from "@opentui/core";
@@ -108,7 +108,7 @@ function Modal({ title, bottomTitle, onClose, disableEscape = false, children, m
108
108
  //#endregion
109
109
  //#region src/tui/agent-picker.tsx
110
110
  /** Cap the scroll window — a long custom registry shouldn't push the modal off-screen. */
111
- const VISIBLE_ROW_CAP$1 = 10;
111
+ const VISIBLE_ROW_CAP = 10;
112
112
  /**
113
113
  * Modal that lists the registered {@link AgentProfile}s and lets the user
114
114
  * pick one. Rows show: `● selected · label description`.
@@ -130,8 +130,8 @@ function AgentPickerModal({ agents, currentAgentId, onPick }) {
130
130
  description: p.description,
131
131
  value: p.id
132
132
  })), [profiles, currentAgentId]);
133
- if (profiles.length === 0) return /* @__PURE__ */ jsx(EmptyState$2, {});
134
- const visibleRows = Math.min(options.length, VISIBLE_ROW_CAP$1);
133
+ if (profiles.length === 0) return /* @__PURE__ */ jsx(EmptyState$1, {});
134
+ const visibleRows = Math.min(options.length, VISIBLE_ROW_CAP);
135
135
  const currentMissing = initialIndex < 0;
136
136
  const safeIndex = currentMissing ? 0 : initialIndex;
137
137
  return /* @__PURE__ */ jsxs(Modal, {
@@ -176,7 +176,7 @@ function AgentPickerModal({ agents, currentAgentId, onPick }) {
176
176
  ]
177
177
  });
178
178
  }
179
- function EmptyState$2() {
179
+ function EmptyState$1() {
180
180
  const COLOR = useColors();
181
181
  return /* @__PURE__ */ jsxs(Modal, {
182
182
  title: "select agent",
@@ -733,7 +733,7 @@ function Transcript({ events, settings, selectedTurnId = null }) {
733
733
  });
734
734
  return () => cancelAnimationFrame(handle);
735
735
  }, [selectedTurnId, anchors]);
736
- if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState$1, {});
736
+ if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
737
737
  return /* @__PURE__ */ jsx("scrollbox", {
738
738
  ref: scrollboxRef,
739
739
  focusable: false,
@@ -905,7 +905,7 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds }) {
905
905
  }, i))
906
906
  });
907
907
  }
908
- function EmptyState$1() {
908
+ function EmptyState() {
909
909
  return /* @__PURE__ */ jsx("box", {
910
910
  style: {
911
911
  flexGrow: 1,
@@ -1216,12 +1216,6 @@ function ToolResultBlock({ text, indent }) {
1216
1216
  }
1217
1217
  //#endregion
1218
1218
  //#region src/tui/effort-picker.tsx
1219
- /**
1220
- * Reasoning effort options surfaced in the picker. Mirrors {@link ThinkingLevel}
1221
- * minus `'adaptive'` (Anthropic-only — opt in via {@link EffortPickerModal}'s
1222
- * `supportsAdaptive` flag rather than confusing OpenAI / OpenRouter users with
1223
- * a level their model silently treats as `'off'`).
1224
- */
1225
1219
  const BASE_LEVELS = [
1226
1220
  {
1227
1221
  id: "off",
@@ -1248,58 +1242,162 @@ const ADAPTIVE_LEVEL = {
1248
1242
  id: "adaptive",
1249
1243
  description: "model decides per-turn (Anthropic)"
1250
1244
  };
1251
- /**
1252
- * Modal that lets the user pick a reasoning effort for the active model.
1253
- * Only surfaced for models whose registry entry reports
1254
- * `reasoning: true` — see `modelSupportsReasoning`.
1255
- *
1256
- * `'adaptive'` is Anthropic-only; pass `supportsAdaptive` to surface it.
1257
- */
1258
1245
  function EffortPickerModal({ current, supportsAdaptive, onPick }) {
1259
1246
  const COLOR = useColors();
1260
- const SELECT_THEME = useSelectStyle();
1261
- const levels = useMemo(() => supportsAdaptive ? [...BASE_LEVELS, ADAPTIVE_LEVEL] : BASE_LEVELS, [supportsAdaptive]);
1262
- const options = useMemo(() => levels.map((l) => ({
1263
- name: `${l.id === current ? "● " : " "}${l.id}`,
1264
- description: l.description,
1265
- value: l.id
1266
- })), [levels, current]);
1267
- const initialIndex = useMemo(() => {
1247
+ const SURFACE = useSurfaces();
1248
+ const inputRef = useRef(null);
1249
+ const [query, setQuery] = useState("");
1250
+ const levels = useMemo(() => {
1251
+ return (supportsAdaptive ? [...BASE_LEVELS, ADAPTIVE_LEVEL] : BASE_LEVELS).map((l) => ({
1252
+ ...l,
1253
+ searchCorpus: `${l.id} ${l.description}`.toLowerCase()
1254
+ }));
1255
+ }, [supportsAdaptive]);
1256
+ const filtered = useMemo(() => {
1257
+ const trimmed = query.trim().toLowerCase();
1258
+ if (!trimmed) return levels;
1259
+ const terms = trimmed.split(/\s+/);
1260
+ return levels.filter((l) => terms.every((t) => l.searchCorpus.includes(t)));
1261
+ }, [levels, query]);
1262
+ const [selectedIdx, setSelectedIdx] = useState(() => {
1268
1263
  const idx = levels.findIndex((l) => l.id === current);
1269
- return idx < 0 ? levels.findIndex((l) => l.id === "medium") : idx;
1270
- }, [levels, current]);
1264
+ if (idx >= 0) return idx;
1265
+ const fallback = levels.findIndex((l) => l.id === "medium");
1266
+ return fallback < 0 ? 0 : fallback;
1267
+ });
1268
+ const handleQueryChange = useCallback((next) => {
1269
+ setQuery(next);
1270
+ setSelectedIdx(0);
1271
+ }, []);
1272
+ const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
1273
+ const commit = () => {
1274
+ const row = filtered[safeIndex];
1275
+ if (row) onPick(row.id);
1276
+ };
1277
+ useEffect(() => {
1278
+ inputRef.current?.focus();
1279
+ }, []);
1280
+ useKeyboard((key) => {
1281
+ if (key.name === "up") {
1282
+ setSelectedIdx((i) => Math.max(0, i - 1));
1283
+ return;
1284
+ }
1285
+ if (key.name === "down") {
1286
+ setSelectedIdx((i) => Math.min(filtered.length - 1, i + 1));
1287
+ return;
1288
+ }
1289
+ if (key.name === "return") commit();
1290
+ });
1271
1291
  return /* @__PURE__ */ jsxs(Modal, {
1272
1292
  title: "select reasoning effort",
1273
- children: [/* @__PURE__ */ jsx("select", {
1274
- ...SELECT_THEME,
1275
- focused: true,
1276
- options,
1277
- wrapSelection: true,
1278
- selectedIndex: Math.max(0, initialIndex),
1279
- style: { height: levels.length },
1280
- onSelect: (_idx, option) => {
1281
- if (option) onPick(option.value);
1282
- }
1283
- }), /* @__PURE__ */ jsxs("text", {
1284
- fg: COLOR.mute,
1293
+ maxWidth: 80,
1294
+ children: [
1295
+ /* @__PURE__ */ jsx("box", {
1296
+ style: {
1297
+ border: true,
1298
+ borderColor: COLOR.borderActive,
1299
+ paddingLeft: 1,
1300
+ paddingRight: 1,
1301
+ height: 3
1302
+ },
1303
+ children: /* @__PURE__ */ jsx("input", {
1304
+ ref: inputRef,
1305
+ focused: true,
1306
+ placeholder: "search effort levels…",
1307
+ onInput: handleQueryChange,
1308
+ onSubmit: () => {},
1309
+ style: { flexGrow: 1 }
1310
+ })
1311
+ }),
1312
+ /* @__PURE__ */ jsx("box", {
1313
+ style: {
1314
+ flexDirection: "column",
1315
+ flexShrink: 0
1316
+ },
1317
+ children: filtered.length === 0 ? /* @__PURE__ */ jsxs("text", {
1318
+ fg: COLOR.dim,
1319
+ children: [/* @__PURE__ */ jsx("span", {
1320
+ fg: COLOR.mute,
1321
+ children: "no levels match "
1322
+ }), /* @__PURE__ */ jsx("span", {
1323
+ fg: COLOR.warn,
1324
+ children: query.trim()
1325
+ })]
1326
+ }) : filtered.map((level, i) => /* @__PURE__ */ jsx(EffortRow, {
1327
+ level,
1328
+ isCurrent: level.id === current,
1329
+ isFocused: i === safeIndex,
1330
+ highlightBg: SURFACE.selection
1331
+ }, level.id))
1332
+ }),
1333
+ /* @__PURE__ */ jsxs("text", {
1334
+ fg: COLOR.dim,
1335
+ children: [
1336
+ /* @__PURE__ */ jsx("span", {
1337
+ fg: COLOR.warn,
1338
+ children: "↑↓"
1339
+ }),
1340
+ " navigate · ",
1341
+ /* @__PURE__ */ jsx("span", {
1342
+ fg: COLOR.warn,
1343
+ children: "↵"
1344
+ }),
1345
+ " select · ",
1346
+ /* @__PURE__ */ jsx("span", {
1347
+ fg: COLOR.warn,
1348
+ children: "esc"
1349
+ }),
1350
+ " close · ",
1351
+ /* @__PURE__ */ jsx("span", {
1352
+ fg: COLOR.mute,
1353
+ children: `${filtered.length} / ${levels.length} level${levels.length === 1 ? "" : "s"}`
1354
+ })
1355
+ ]
1356
+ })
1357
+ ]
1358
+ });
1359
+ }
1360
+ /**
1361
+ * Single row in the picker. Mirrors `ModelRow` in `model-picker.tsx`:
1362
+ * `●` marker for the current pick, single-space middle-dot separators,
1363
+ * focused row gets the `surfaces.selection` background lift.
1364
+ */
1365
+ function EffortRow({ level, isCurrent, isFocused, highlightBg }) {
1366
+ const COLOR = useColors();
1367
+ const marker = isCurrent ? "●" : " ";
1368
+ return /* @__PURE__ */ jsx("box", {
1369
+ style: {
1370
+ height: 1,
1371
+ paddingLeft: 1,
1372
+ paddingRight: 1,
1373
+ flexShrink: 0,
1374
+ backgroundColor: isFocused ? highlightBg : void 0
1375
+ },
1376
+ children: /* @__PURE__ */ jsxs("text", {
1377
+ wrapMode: "none",
1285
1378
  children: [
1286
1379
  /* @__PURE__ */ jsx("span", {
1287
- fg: COLOR.warn,
1288
- children: "↑↓"
1380
+ fg: isCurrent ? COLOR.brand : COLOR.mute,
1381
+ children: marker
1289
1382
  }),
1290
- " navigate · ",
1291
1383
  /* @__PURE__ */ jsx("span", {
1292
- fg: COLOR.warn,
1293
- children: ""
1384
+ fg: COLOR.mute,
1385
+ children: " "
1294
1386
  }),
1295
- " select · ",
1296
1387
  /* @__PURE__ */ jsx("span", {
1297
- fg: COLOR.warn,
1298
- children: "esc"
1388
+ fg: isFocused ? COLOR.brand : COLOR.dim,
1389
+ children: level.id
1299
1390
  }),
1300
- " close"
1391
+ /* @__PURE__ */ jsx("span", {
1392
+ fg: COLOR.mute,
1393
+ children: " · "
1394
+ }),
1395
+ /* @__PURE__ */ jsx("span", {
1396
+ fg: COLOR.mute,
1397
+ children: level.description
1398
+ })
1301
1399
  ]
1302
- })]
1400
+ })
1303
1401
  });
1304
1402
  }
1305
1403
  //#endregion
@@ -1465,50 +1563,134 @@ function McpsSettingsModal({ catalog }) {
1465
1563
  }
1466
1564
  //#endregion
1467
1565
  //#region src/tui/model-picker.tsx
1468
- /** Cap the visible scroll window so a 30-model list doesn't push the modal off-screen. */
1469
- const VISIBLE_ROW_CAP = 12;
1470
1566
  /**
1471
- * Modal that lists the available models for the current provider and lets
1472
- * the user pick one. Options come from the active `ProviderDescriptor` —
1473
- * either its declared `models` list or, when absent, pi-ai's built-in
1474
- * registry looked up via `piProviderId`.
1567
+ * Cross-provider, searchable model picker.
1568
+ *
1569
+ * The picker unions every available provider's models into one flat
1570
+ * catalog, lets the user filter with a typed query, and returns both
1571
+ * the chosen provider and model id on commit. Pick a model from a
1572
+ * different provider than the current one and the caller is expected
1573
+ * to swap the active `ProviderAuth` + rebuild the agent — see
1574
+ * `app.tsx`'s `onPickModel` for the canonical wiring.
1475
1575
  *
1476
- * Each row shows: `● selected · name (ctx N · reasoning · vision)`.
1576
+ * Geometry:
1577
+ * - Top: single-row search input. The input owns focus so the user
1578
+ * can type immediately; ↑/↓/↵/⎋ are intercepted before the input's
1579
+ * text handler sees them so navigation works without losing focus.
1580
+ * - Middle: windowed list of catalog rows around the current
1581
+ * selection. Each row shows the model name (or id) in brand color,
1582
+ * the provider label in dim, and a compact capability suffix
1583
+ * (`ctx 200k · reasoning · vision`).
1584
+ * - Bottom: shortcut hint row.
1585
+ *
1586
+ * Empty states:
1587
+ * - No available providers → "no providers configured" notice.
1588
+ * - Query has no matches → "no matches" row, keep the search input
1589
+ * live so the user can backspace out.
1477
1590
  */
1478
- function ModelPickerModal({ models, currentModelId, onPick }) {
1591
+ /** Visible rows in the windowed list. Keeps the modal a stable size on long catalogs. */
1592
+ const VISIBLE_ROWS = 10;
1593
+ function ModelPickerModal({ providers, modelsFor, current, onPick }) {
1479
1594
  const COLOR = useColors();
1480
- const SELECT_THEME = useSelectStyle();
1481
- const initialIndex = useMemo(() => models.findIndex((m) => m.id === currentModelId), [models, currentModelId]);
1482
- const options = useMemo(() => models.map((m) => ({
1483
- name: `${m.id === currentModelId ? "● " : " "}${m.name ?? m.id}`,
1484
- description: describeModel(m),
1485
- value: m.id
1486
- })), [models, currentModelId]);
1487
- if (models.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
1488
- const visibleRows = Math.min(options.length, VISIBLE_ROW_CAP);
1489
- const currentMissing = initialIndex < 0;
1490
- const safeIndex = currentMissing ? 0 : initialIndex;
1595
+ const SURFACE = useSurfaces();
1596
+ const inputRef = useRef(null);
1597
+ const [query, setQuery] = useState("");
1598
+ useEffect(() => {
1599
+ inputRef.current?.focus();
1600
+ }, []);
1601
+ const catalog = useMemo(() => buildModelCatalog({
1602
+ providers,
1603
+ modelsFor,
1604
+ current
1605
+ }), [
1606
+ providers,
1607
+ modelsFor,
1608
+ current
1609
+ ]);
1610
+ const filtered = useMemo(() => filterModelCatalog(catalog, query), [catalog, query]);
1611
+ const [selectedIdx, setSelectedIdx] = useState(() => Math.max(0, indexOfEntry(catalog, current)));
1612
+ const handleQueryChange = useCallback((next) => {
1613
+ setQuery(next);
1614
+ setSelectedIdx(0);
1615
+ }, []);
1616
+ const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
1617
+ const commit = () => {
1618
+ const row = filtered[safeIndex];
1619
+ if (row) onPick({
1620
+ providerKey: row.providerKey,
1621
+ modelId: row.model.id
1622
+ });
1623
+ };
1624
+ const viewport = useMemo(() => {
1625
+ if (filtered.length <= VISIBLE_ROWS) return {
1626
+ start: 0,
1627
+ slice: filtered
1628
+ };
1629
+ const half = Math.floor(VISIBLE_ROWS / 2);
1630
+ let start = Math.max(0, safeIndex - half);
1631
+ if (start + VISIBLE_ROWS > filtered.length) start = filtered.length - VISIBLE_ROWS;
1632
+ return {
1633
+ start,
1634
+ slice: filtered.slice(start, start + VISIBLE_ROWS)
1635
+ };
1636
+ }, [filtered, safeIndex]);
1637
+ useKeyboard((key) => {
1638
+ if (key.name === "up") {
1639
+ setSelectedIdx((i) => Math.max(0, i - 1));
1640
+ return;
1641
+ }
1642
+ if (key.name === "down") {
1643
+ setSelectedIdx((i) => Math.min(filtered.length - 1, i + 1));
1644
+ return;
1645
+ }
1646
+ if (key.name === "return") commit();
1647
+ });
1648
+ if (providers.length === 0) return /* @__PURE__ */ jsx(EmptyProvidersState, {});
1491
1649
  return /* @__PURE__ */ jsxs(Modal, {
1492
1650
  title: "select model",
1651
+ maxWidth: 100,
1493
1652
  children: [
1494
- currentMissing && /* @__PURE__ */ jsx("text", {
1495
- fg: COLOR.warn,
1496
- children: `Current model "${currentModelId}" is not in this registry — pick one below to switch.`
1653
+ /* @__PURE__ */ jsx("box", {
1654
+ style: {
1655
+ border: true,
1656
+ borderColor: COLOR.borderActive,
1657
+ paddingLeft: 1,
1658
+ paddingRight: 1,
1659
+ height: 3
1660
+ },
1661
+ children: /* @__PURE__ */ jsx("input", {
1662
+ ref: inputRef,
1663
+ focused: true,
1664
+ placeholder: "search models — provider, name, capability…",
1665
+ onInput: handleQueryChange,
1666
+ onSubmit: () => {},
1667
+ style: { flexGrow: 1 }
1668
+ })
1497
1669
  }),
1498
- /* @__PURE__ */ jsx("select", {
1499
- ...SELECT_THEME,
1500
- focused: true,
1501
- options,
1502
- wrapSelection: true,
1503
- selectedIndex: safeIndex,
1504
- showScrollIndicator: options.length > visibleRows,
1505
- style: { height: visibleRows },
1506
- onSelect: (_idx, option) => {
1507
- if (option) onPick(option.value);
1508
- }
1670
+ /* @__PURE__ */ jsx("box", {
1671
+ style: {
1672
+ flexDirection: "column",
1673
+ height: VISIBLE_ROWS,
1674
+ flexShrink: 0
1675
+ },
1676
+ children: filtered.length === 0 ? /* @__PURE__ */ jsxs("text", {
1677
+ fg: COLOR.dim,
1678
+ children: [/* @__PURE__ */ jsx("span", {
1679
+ fg: COLOR.mute,
1680
+ children: "no models match "
1681
+ }), /* @__PURE__ */ jsx("span", {
1682
+ fg: COLOR.warn,
1683
+ children: query.trim()
1684
+ })]
1685
+ }) : viewport.slice.map((entry, i) => /* @__PURE__ */ jsx(ModelRow, {
1686
+ entry,
1687
+ isCurrent: entry.providerKey === current.providerKey && entry.model.id === current.modelId,
1688
+ isFocused: viewport.start + i === safeIndex,
1689
+ highlightBg: SURFACE.selection
1690
+ }, `${entry.providerKey}:${entry.model.id}`))
1509
1691
  }),
1510
1692
  /* @__PURE__ */ jsxs("text", {
1511
- fg: COLOR.mute,
1693
+ fg: COLOR.dim,
1512
1694
  children: [
1513
1695
  /* @__PURE__ */ jsx("span", {
1514
1696
  fg: COLOR.warn,
@@ -1524,38 +1706,94 @@ function ModelPickerModal({ models, currentModelId, onPick }) {
1524
1706
  fg: COLOR.warn,
1525
1707
  children: "esc"
1526
1708
  }),
1527
- " close"
1709
+ " close · ",
1710
+ /* @__PURE__ */ jsx("span", {
1711
+ fg: COLOR.mute,
1712
+ children: `${filtered.length} / ${catalog.length} model${catalog.length === 1 ? "" : "s"}`
1713
+ })
1528
1714
  ]
1529
1715
  })
1530
1716
  ]
1531
1717
  });
1532
1718
  }
1533
- function EmptyState() {
1719
+ /**
1720
+ * Single row in the picker. Renders the model name in `brand`, the
1721
+ * provider tag in `dim`, and the capability suffix in `mute`. The
1722
+ * focused row gets a subtle selection background (same surface as
1723
+ * select-turn mode in the transcript) so it pops without a separate
1724
+ * marker glyph competing with the `●` "current" indicator.
1725
+ */
1726
+ function ModelRow({ entry, isCurrent, isFocused, highlightBg }) {
1727
+ const COLOR = useColors();
1728
+ const marker = isCurrent ? "●" : " ";
1729
+ return /* @__PURE__ */ jsx("box", {
1730
+ style: {
1731
+ height: 1,
1732
+ paddingLeft: 1,
1733
+ paddingRight: 1,
1734
+ flexShrink: 0,
1735
+ backgroundColor: isFocused ? highlightBg : void 0
1736
+ },
1737
+ children: /* @__PURE__ */ jsxs("text", {
1738
+ wrapMode: "none",
1739
+ children: [
1740
+ /* @__PURE__ */ jsx("span", {
1741
+ fg: isCurrent ? COLOR.brand : COLOR.mute,
1742
+ children: marker
1743
+ }),
1744
+ /* @__PURE__ */ jsx("span", {
1745
+ fg: COLOR.mute,
1746
+ children: " "
1747
+ }),
1748
+ /* @__PURE__ */ jsx("span", {
1749
+ fg: isFocused ? COLOR.brand : COLOR.dim,
1750
+ children: entry.model.name ?? entry.model.id
1751
+ }),
1752
+ /* @__PURE__ */ jsx("span", {
1753
+ fg: COLOR.mute,
1754
+ children: " · "
1755
+ }),
1756
+ /* @__PURE__ */ jsx("span", {
1757
+ fg: COLOR.model,
1758
+ children: entry.providerLabel
1759
+ }),
1760
+ /* @__PURE__ */ jsx("span", {
1761
+ fg: COLOR.mute,
1762
+ children: " · "
1763
+ }),
1764
+ /* @__PURE__ */ jsx("span", {
1765
+ fg: COLOR.mute,
1766
+ children: describeModel(entry.model)
1767
+ })
1768
+ ]
1769
+ })
1770
+ });
1771
+ }
1772
+ function EmptyProvidersState() {
1534
1773
  const COLOR = useColors();
1535
1774
  return /* @__PURE__ */ jsxs(Modal, {
1536
1775
  title: "select model",
1537
1776
  children: [/* @__PURE__ */ jsx("text", {
1538
1777
  fg: COLOR.dim,
1539
- children: "No models available for this provider."
1778
+ children: "No authed providers configure one via"
1540
1779
  }), /* @__PURE__ */ jsxs("text", {
1541
- fg: COLOR.mute,
1780
+ fg: COLOR.dim,
1542
1781
  children: [
1543
- "Set",
1544
1782
  /* @__PURE__ */ jsx("span", {
1545
1783
  fg: COLOR.model,
1546
- children: " models "
1784
+ children: " settings → re-configure providers"
1547
1785
  }),
1548
- "on the provider descriptor (or a",
1786
+ " or ",
1549
1787
  /* @__PURE__ */ jsx("span", {
1550
1788
  fg: COLOR.model,
1551
- children: " piProviderId "
1789
+ children: "esc sessions → settings"
1552
1790
  }),
1553
- "that pi-ai recognizes) to populate this list."
1791
+ " first."
1554
1792
  ]
1555
1793
  })]
1556
1794
  });
1557
1795
  }
1558
- /** "ctx 200k · reasoning · vision" — compact per-model description. */
1796
+ /** "ctx 200k · reasoning · vision" — compact capability blurb. */
1559
1797
  function describeModel(m) {
1560
1798
  const parts = [`ctx ${fmtTokens(m.contextWindow)}`];
1561
1799
  if (m.reasoning) parts.push("reasoning");
@@ -4131,6 +4369,7 @@ function AppShell() {
4131
4369
  const SURFACE = useSurfaces();
4132
4370
  const queue = useSafeModeQueue();
4133
4371
  const { requestApproval, resolveHead, denyAll } = useSafeModeActions();
4372
+ const pendingApproval = queue[0] ?? null;
4134
4373
  const { providers: providerRegistry, agents: agentRegistry, initialAgentId, store, stateStore, modelsFor, resumeProvider, initialPicked, initialState } = config;
4135
4374
  const lastResumedSessionId = initialState.lastSessionId;
4136
4375
  const dataDir = config.paths.userDir;
@@ -4140,8 +4379,7 @@ function AppShell() {
4140
4379
  useEffect(() => {
4141
4380
  safeModeEnabledRef.current = settings.safeMode;
4142
4381
  }, [settings.safeMode]);
4143
- const [projectDir] = useState(() => process.cwd());
4144
- const [sessionProjectRoot] = useState(() => findGitRoot(process.cwd()) ?? process.cwd());
4382
+ const [projectDir] = useState(() => findGitRoot(process.cwd()) ?? process.cwd());
4145
4383
  const safelistRef = useRef(null);
4146
4384
  const readSafelist = useCallback(() => {
4147
4385
  if (safelistRef.current === null) safelistRef.current = getSafelist(dataDir, projectDir);
@@ -4411,13 +4649,13 @@ function AppShell() {
4411
4649
  config.prefix
4412
4650
  ]);
4413
4651
  const refreshSessions = useCallback(async () => {
4414
- const list = await listSessionMeta(store, settings.showAllProjects ? void 0 : { projectRoot: sessionProjectRoot });
4652
+ const list = await listSessionMeta(store, settings.showAllProjects ? void 0 : { projectRoot: projectDir });
4415
4653
  setSessions(list);
4416
4654
  return list;
4417
4655
  }, [
4418
4656
  store,
4419
4657
  settings.showAllProjects,
4420
- sessionProjectRoot
4658
+ projectDir
4421
4659
  ]);
4422
4660
  const teardown = useCallback(async () => {
4423
4661
  try {
@@ -4439,7 +4677,7 @@ function AppShell() {
4439
4677
  await teardown();
4440
4678
  const session = (id ? await loadSession(store, id) : null) ?? await createSession({
4441
4679
  store,
4442
- projectRoot: sessionProjectRoot,
4680
+ projectRoot: projectDir,
4443
4681
  ...id ? { id } : {}
4444
4682
  });
4445
4683
  sessionRef.current = session;
@@ -4465,7 +4703,7 @@ function AppShell() {
4465
4703
  buildAgent,
4466
4704
  store,
4467
4705
  stateStore,
4468
- sessionProjectRoot
4706
+ projectDir
4469
4707
  ]);
4470
4708
  useEffect(() => {
4471
4709
  if (!resumeProvider) return;
@@ -4474,7 +4712,7 @@ function AppShell() {
4474
4712
  if (lastResumedSessionId) {
4475
4713
  const data = await store.load(lastResumedSessionId);
4476
4714
  if (cancelled) return;
4477
- const sessionMatchesProject = settings.showAllProjects || data?.projectRoot != null && data.projectRoot === sessionProjectRoot;
4715
+ const sessionMatchesProject = settings.showAllProjects || data?.projectRoot != null && data.projectRoot === projectDir;
4478
4716
  if (data && sessionMatchesProject) {
4479
4717
  await activateSession(lastResumedSessionId, resumeProvider.key);
4480
4718
  return;
@@ -4495,8 +4733,15 @@ function AppShell() {
4495
4733
  lastResumedSessionId,
4496
4734
  store,
4497
4735
  settings.showAllProjects,
4498
- sessionProjectRoot
4736
+ projectDir
4499
4737
  ]);
4738
+ const [availableProviders, setAvailableProviders] = useState([]);
4739
+ const refreshAvailableProviders = useCallback(() => {
4740
+ setAvailableProviders(detectAuth(config.paths.userDir, providerRegistry).filter((p) => p.available));
4741
+ }, [config.paths.userDir, providerRegistry]);
4742
+ useEffect(() => {
4743
+ refreshAvailableProviders();
4744
+ }, [refreshAvailableProviders]);
4500
4745
  const onPickProvider = useCallback(async (p) => {
4501
4746
  const next = makePicked(p);
4502
4747
  if (!next) return;
@@ -4505,13 +4750,15 @@ function AppShell() {
4505
4750
  ...stateStore.load(),
4506
4751
  lastProvider: p.key
4507
4752
  });
4753
+ refreshAvailableProviders();
4508
4754
  if ((await refreshSessions()).length === 0) await activateSession(null, p.key);
4509
4755
  else setScreen("sessions");
4510
4756
  }, [
4511
4757
  refreshSessions,
4512
4758
  activateSession,
4513
4759
  makePicked,
4514
- stateStore
4760
+ stateStore,
4761
+ refreshAvailableProviders
4515
4762
  ]);
4516
4763
  const onCreateSession = useCallback(async () => {
4517
4764
  if (picked) await activateSession(null, picked.provider.key);
@@ -4532,33 +4779,63 @@ function AppShell() {
4532
4779
  denyAll();
4533
4780
  agentRef.current?.abort();
4534
4781
  }, [denyAll]);
4535
- const onPickModel = useCallback((modelId) => {
4536
- setPicked((prev) => {
4537
- if (!prev) return prev;
4538
- const descriptor = providerRegistry[prev.provider.key];
4539
- const prior = stateStore.load();
4540
- stateStore.save({
4541
- ...prior,
4542
- lastModelByProvider: {
4543
- ...prior.lastModelByProvider,
4544
- [prev.provider.key]: modelId
4545
- }
4546
- });
4547
- const nextEffort = descriptor ? effortForModel(descriptor, modelId, prior.lastEffortByModel) : void 0;
4548
- return nextEffort ? {
4549
- ...prev,
4550
- model: modelId,
4551
- effort: nextEffort
4552
- } : {
4553
- provider: prev.provider,
4554
- model: modelId
4555
- };
4782
+ /**
4783
+ * Pick a `{ providerKey, modelId }` tuple from the cross-provider
4784
+ * model picker. Two branches:
4785
+ *
4786
+ * - **Same provider, different model** — update `picked.model` +
4787
+ * remember it in `lastModelByProvider`. The active agent keeps
4788
+ * running; `agent.run({ model })` accepts the model per-call so
4789
+ * no session rebuild is needed.
4790
+ * - **Different provider** — swap the active `ProviderAuth`,
4791
+ * persist BOTH `lastProvider` + `lastModelByProvider`, then
4792
+ * re-activate the current session against the new provider's
4793
+ * factory. The session id (and conversation history) is
4794
+ * preserved; only the bound agent + provider instance change.
4795
+ *
4796
+ * Either branch re-resolves the reasoning effort: a non-reasoning
4797
+ * model loses its `effort`, a reasoning model gets the remembered
4798
+ * per-model value (falling back to a sensible default).
4799
+ */
4800
+ const onPickModel = useCallback(async (next) => {
4801
+ const nextProvider = availableProviders.find((p) => p.key === next.providerKey);
4802
+ if (!nextProvider) {
4803
+ debugLog("onPickModel: unknown provider key", next.providerKey);
4804
+ modal.close();
4805
+ return;
4806
+ }
4807
+ const descriptor = providerRegistry[nextProvider.key];
4808
+ const prior = stateStore.load();
4809
+ const providerChanged = picked?.provider.key !== nextProvider.key;
4810
+ stateStore.save({
4811
+ ...prior,
4812
+ ...providerChanged ? { lastProvider: nextProvider.key } : {},
4813
+ lastModelByProvider: {
4814
+ ...prior.lastModelByProvider,
4815
+ [nextProvider.key]: next.modelId
4816
+ }
4817
+ });
4818
+ const nextEffort = descriptor ? effortForModel(descriptor, next.modelId, prior.lastEffortByModel) : void 0;
4819
+ setPicked(nextEffort ? {
4820
+ provider: nextProvider,
4821
+ model: next.modelId,
4822
+ effort: nextEffort
4823
+ } : {
4824
+ provider: nextProvider,
4825
+ model: next.modelId
4556
4826
  });
4557
4827
  modal.close();
4828
+ if (providerChanged && currentSession && !busy && !pendingApproval) await activateSession(currentSession.id, nextProvider.key);
4558
4829
  }, [
4559
- modal,
4830
+ availableProviders,
4831
+ providerRegistry,
4560
4832
  stateStore,
4561
- providerRegistry
4833
+ modal,
4834
+ picked,
4835
+ currentSession,
4836
+ busy,
4837
+ pendingApproval,
4838
+ activateSession
4562
4839
  ]);
4563
4840
  const onPickEffort = useCallback((effort) => {
4564
4841
  setPicked((prev) => {
@@ -4661,7 +4938,6 @@ function AppShell() {
4661
4938
  setBusy(false);
4662
4939
  }
4663
4940
  }, [picked, stream]);
4664
- const pendingApproval = queue[0] ?? null;
4665
4941
  const onReauth = useMemo(() => {
4666
4942
  if (busy || pendingApproval) return void 0;
4667
4943
  return () => {
@@ -4932,7 +5208,7 @@ function AppShell() {
4932
5208
  }
4933
5209
  return;
4934
5210
  }
4935
- if (key.ctrl && key.name === "," && screen !== "auth") {
5211
+ if (key.ctrl && key.name === "o" && screen !== "auth") {
4936
5212
  modal.open(/* @__PURE__ */ jsx(SettingsModal, { actions: {
4937
5213
  onReauth,
4938
5214
  onOpenSkills: onOpenSkillsSettings,
@@ -4952,8 +5228,12 @@ function AppShell() {
4952
5228
  }
4953
5229
  if (key.ctrl && key.name === "m" && screen === "chat" && picked && !busy) {
4954
5230
  modal.open(/* @__PURE__ */ jsx(ModelPickerModal, {
4955
- models: modelsFor(picked.provider.key),
4956
- currentModelId: picked.model,
5231
+ providers: availableProviders,
5232
+ modelsFor,
5233
+ current: {
5234
+ providerKey: picked.provider.key,
5235
+ modelId: picked.model
5236
+ },
4957
5237
  onPick: onPickModel
4958
5238
  }));
4959
5239
  return;
@@ -5071,7 +5351,7 @@ function AppShell() {
5071
5351
  onCreate: onCreateSession,
5072
5352
  onFocusChange: setFocusedSessionId,
5073
5353
  showAllProjects: settings.showAllProjects,
5074
- currentProjectRoot: sessionProjectRoot
5354
+ currentProjectRoot: projectDir
5075
5355
  }),
5076
5356
  screen === "chat" && /* @__PURE__ */ jsx(ChatScreen, {
5077
5357
  events,
@@ -5159,7 +5439,7 @@ function buildHints({ screen, busy, pending, currentSession, hasMultipleAgents,
5159
5439
  label: "session"
5160
5440
  },
5161
5441
  {
5162
- key: "ctrl+,",
5442
+ key: "ctrl+o",
5163
5443
  label: "settings"
5164
5444
  },
5165
5445
  {
@@ -5190,7 +5470,7 @@ function buildHints({ screen, busy, pending, currentSession, hasMultipleAgents,
5190
5470
  label: "session"
5191
5471
  }] : [],
5192
5472
  {
5193
- key: "ctrl+,",
5473
+ key: "ctrl+o",
5194
5474
  label: "settings"
5195
5475
  },
5196
5476
  {