zidane 5.0.3 → 5.0.5

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 { 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, 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-CHS2Prne.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-DZ3TrljX.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",
@@ -474,9 +474,17 @@ function renderHintSpans(hints, COLOR) {
474
474
  fg: h.keyColor ?? COLOR.warn,
475
475
  children: h.key
476
476
  }),
477
+ h.extra && /* @__PURE__ */ jsx("span", {
478
+ fg: h.extra.keyColor ?? COLOR.mute,
479
+ children: h.extra.key
480
+ }),
477
481
  /* @__PURE__ */ jsx("span", {
478
482
  fg: h.labelColor ?? COLOR.dim,
479
483
  children: ` ${h.label}`
484
+ }),
485
+ h.extra && /* @__PURE__ */ jsx("span", {
486
+ fg: h.extra.labelColor ?? COLOR.dim,
487
+ children: ` ${h.extra.label}`
480
488
  })
481
489
  ] }, i));
482
490
  }
@@ -645,7 +653,11 @@ function truncateTrailing(text, max) {
645
653
  */
646
654
  function hintsLength(hints) {
647
655
  if (hints.length === 0) return 0;
648
- return hints.reduce((sum, h, i) => sum + h.key.length + 1 + h.label.length + (i > 0 ? 3 : 0), 0);
656
+ return hints.reduce((sum, h, i) => {
657
+ const base = h.key.length + 1 + h.label.length;
658
+ const extra = h.extra ? h.extra.key.length + 1 + h.extra.label.length : 0;
659
+ return sum + base + extra + (i > 0 ? 3 : 0);
660
+ }, 0);
649
661
  }
650
662
  function contextIndicatorLength(context) {
651
663
  const ratio = context.max > 0 ? context.used / context.max : 0;
@@ -721,7 +733,7 @@ function Transcript({ events, settings, selectedTurnId = null }) {
721
733
  });
722
734
  return () => cancelAnimationFrame(handle);
723
735
  }, [selectedTurnId, anchors]);
724
- if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState$1, {});
736
+ if (items.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
725
737
  return /* @__PURE__ */ jsx("scrollbox", {
726
738
  ref: scrollboxRef,
727
739
  focusable: false,
@@ -893,7 +905,7 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds }) {
893
905
  }, i))
894
906
  });
895
907
  }
896
- function EmptyState$1() {
908
+ function EmptyState() {
897
909
  return /* @__PURE__ */ jsx("box", {
898
910
  style: {
899
911
  flexGrow: 1,
@@ -1203,6 +1215,192 @@ function ToolResultBlock({ text, indent }) {
1203
1215
  });
1204
1216
  }
1205
1217
  //#endregion
1218
+ //#region src/tui/effort-picker.tsx
1219
+ const BASE_LEVELS = [
1220
+ {
1221
+ id: "off",
1222
+ description: "no reasoning — fastest, smallest output"
1223
+ },
1224
+ {
1225
+ id: "minimal",
1226
+ description: "tiny reasoning budget (gpt-5 family)"
1227
+ },
1228
+ {
1229
+ id: "low",
1230
+ description: "short reasoning pass"
1231
+ },
1232
+ {
1233
+ id: "medium",
1234
+ description: "balanced — sensible default"
1235
+ },
1236
+ {
1237
+ id: "high",
1238
+ description: "deep reasoning — slowest, longest"
1239
+ }
1240
+ ];
1241
+ const ADAPTIVE_LEVEL = {
1242
+ id: "adaptive",
1243
+ description: "model decides per-turn (Anthropic)"
1244
+ };
1245
+ function EffortPickerModal({ current, supportsAdaptive, onPick }) {
1246
+ const COLOR = useColors();
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(() => {
1263
+ const idx = levels.findIndex((l) => l.id === 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
+ });
1291
+ return /* @__PURE__ */ jsxs(Modal, {
1292
+ title: "select reasoning effort",
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",
1378
+ children: [
1379
+ /* @__PURE__ */ jsx("span", {
1380
+ fg: isCurrent ? COLOR.brand : COLOR.mute,
1381
+ children: marker
1382
+ }),
1383
+ /* @__PURE__ */ jsx("span", {
1384
+ fg: COLOR.mute,
1385
+ children: " "
1386
+ }),
1387
+ /* @__PURE__ */ jsx("span", {
1388
+ fg: isFocused ? COLOR.brand : COLOR.dim,
1389
+ children: level.id
1390
+ }),
1391
+ /* @__PURE__ */ jsx("span", {
1392
+ fg: COLOR.mute,
1393
+ children: " · "
1394
+ }),
1395
+ /* @__PURE__ */ jsx("span", {
1396
+ fg: COLOR.mute,
1397
+ children: level.description
1398
+ })
1399
+ ]
1400
+ })
1401
+ });
1402
+ }
1403
+ //#endregion
1206
1404
  //#region src/tui/toggle-list-modal.tsx
1207
1405
  /**
1208
1406
  * Generic list-with-checkboxes modal. Powers both the Skills and MCP
@@ -1365,50 +1563,134 @@ function McpsSettingsModal({ catalog }) {
1365
1563
  }
1366
1564
  //#endregion
1367
1565
  //#region src/tui/model-picker.tsx
1368
- /** Cap the visible scroll window so a 30-model list doesn't push the modal off-screen. */
1369
- const VISIBLE_ROW_CAP = 12;
1370
1566
  /**
1371
- * Modal that lists the available models for the current provider and lets
1372
- * the user pick one. Options come from the active `ProviderDescriptor` —
1373
- * either its declared `models` list or, when absent, pi-ai's built-in
1374
- * 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.
1375
1575
  *
1376
- * 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.
1377
1590
  */
1378
- 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 }) {
1379
1594
  const COLOR = useColors();
1380
- const SELECT_THEME = useSelectStyle();
1381
- const initialIndex = useMemo(() => models.findIndex((m) => m.id === currentModelId), [models, currentModelId]);
1382
- const options = useMemo(() => models.map((m) => ({
1383
- name: `${m.id === currentModelId ? "● " : " "}${m.name ?? m.id}`,
1384
- description: describeModel(m),
1385
- value: m.id
1386
- })), [models, currentModelId]);
1387
- if (models.length === 0) return /* @__PURE__ */ jsx(EmptyState, {});
1388
- const visibleRows = Math.min(options.length, VISIBLE_ROW_CAP);
1389
- const currentMissing = initialIndex < 0;
1390
- 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, {});
1391
1649
  return /* @__PURE__ */ jsxs(Modal, {
1392
1650
  title: "select model",
1651
+ maxWidth: 100,
1393
1652
  children: [
1394
- currentMissing && /* @__PURE__ */ jsx("text", {
1395
- fg: COLOR.warn,
1396
- 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
+ })
1397
1669
  }),
1398
- /* @__PURE__ */ jsx("select", {
1399
- ...SELECT_THEME,
1400
- focused: true,
1401
- options,
1402
- wrapSelection: true,
1403
- selectedIndex: safeIndex,
1404
- showScrollIndicator: options.length > visibleRows,
1405
- style: { height: visibleRows },
1406
- onSelect: (_idx, option) => {
1407
- if (option) onPick(option.value);
1408
- }
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}`))
1409
1691
  }),
1410
1692
  /* @__PURE__ */ jsxs("text", {
1411
- fg: COLOR.mute,
1693
+ fg: COLOR.dim,
1412
1694
  children: [
1413
1695
  /* @__PURE__ */ jsx("span", {
1414
1696
  fg: COLOR.warn,
@@ -1424,38 +1706,94 @@ function ModelPickerModal({ models, currentModelId, onPick }) {
1424
1706
  fg: COLOR.warn,
1425
1707
  children: "esc"
1426
1708
  }),
1427
- " close"
1709
+ " close · ",
1710
+ /* @__PURE__ */ jsx("span", {
1711
+ fg: COLOR.mute,
1712
+ children: `${filtered.length} / ${catalog.length} model${catalog.length === 1 ? "" : "s"}`
1713
+ })
1428
1714
  ]
1429
1715
  })
1430
1716
  ]
1431
1717
  });
1432
1718
  }
1433
- 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() {
1434
1773
  const COLOR = useColors();
1435
1774
  return /* @__PURE__ */ jsxs(Modal, {
1436
1775
  title: "select model",
1437
1776
  children: [/* @__PURE__ */ jsx("text", {
1438
1777
  fg: COLOR.dim,
1439
- children: "No models available for this provider."
1778
+ children: "No authed providers configure one via"
1440
1779
  }), /* @__PURE__ */ jsxs("text", {
1441
- fg: COLOR.mute,
1780
+ fg: COLOR.dim,
1442
1781
  children: [
1443
- "Set",
1444
1782
  /* @__PURE__ */ jsx("span", {
1445
1783
  fg: COLOR.model,
1446
- children: " models "
1784
+ children: " settings → re-configure providers"
1447
1785
  }),
1448
- "on the provider descriptor (or a",
1786
+ " or ",
1449
1787
  /* @__PURE__ */ jsx("span", {
1450
1788
  fg: COLOR.model,
1451
- children: " piProviderId "
1789
+ children: "esc sessions → settings"
1452
1790
  }),
1453
- "that pi-ai recognizes) to populate this list."
1791
+ " first."
1454
1792
  ]
1455
1793
  })]
1456
1794
  });
1457
1795
  }
1458
- /** "ctx 200k · reasoning · vision" — compact per-model description. */
1796
+ /** "ctx 200k · reasoning · vision" — compact capability blurb. */
1459
1797
  function describeModel(m) {
1460
1798
  const parts = [`ctx ${fmtTokens(m.contextWindow)}`];
1461
1799
  if (m.reasoning) parts.push("reasoning");
@@ -4031,6 +4369,7 @@ function AppShell() {
4031
4369
  const SURFACE = useSurfaces();
4032
4370
  const queue = useSafeModeQueue();
4033
4371
  const { requestApproval, resolveHead, denyAll } = useSafeModeActions();
4372
+ const pendingApproval = queue[0] ?? null;
4034
4373
  const { providers: providerRegistry, agents: agentRegistry, initialAgentId, store, stateStore, modelsFor, resumeProvider, initialPicked, initialState } = config;
4035
4374
  const lastResumedSessionId = initialState.lastSessionId;
4036
4375
  const dataDir = config.paths.userDir;
@@ -4160,9 +4499,15 @@ function AppShell() {
4160
4499
  const descriptor = providerRegistry[provider.key];
4161
4500
  if (!descriptor) return null;
4162
4501
  const remembered = initialState.lastModelByProvider?.[provider.key];
4163
- return {
4502
+ const model = modelId ?? remembered ?? descriptor.defaultModel ?? descriptor.factory().meta.defaultModel;
4503
+ const effort = effortForModel(descriptor, model, initialState.lastEffortByModel);
4504
+ return effort ? {
4505
+ provider,
4506
+ model,
4507
+ effort
4508
+ } : {
4164
4509
  provider,
4165
- model: modelId ?? remembered ?? descriptor.defaultModel ?? descriptor.factory().meta.defaultModel
4510
+ model
4166
4511
  };
4167
4512
  }, [providerRegistry, initialState]);
4168
4513
  const buildAgent = useCallback((session, key) => {
@@ -4391,6 +4736,13 @@ function AppShell() {
4391
4736
  settings.showAllProjects,
4392
4737
  sessionProjectRoot
4393
4738
  ]);
4739
+ const [availableProviders, setAvailableProviders] = useState([]);
4740
+ const refreshAvailableProviders = useCallback(() => {
4741
+ setAvailableProviders(detectAuth(config.paths.userDir, providerRegistry).filter((p) => p.available));
4742
+ }, [config.paths.userDir, providerRegistry]);
4743
+ useEffect(() => {
4744
+ refreshAvailableProviders();
4745
+ }, [refreshAvailableProviders]);
4394
4746
  const onPickProvider = useCallback(async (p) => {
4395
4747
  const next = makePicked(p);
4396
4748
  if (!next) return;
@@ -4399,13 +4751,15 @@ function AppShell() {
4399
4751
  ...stateStore.load(),
4400
4752
  lastProvider: p.key
4401
4753
  });
4754
+ refreshAvailableProviders();
4402
4755
  if ((await refreshSessions()).length === 0) await activateSession(null, p.key);
4403
4756
  else setScreen("sessions");
4404
4757
  }, [
4405
4758
  refreshSessions,
4406
4759
  activateSession,
4407
4760
  makePicked,
4408
- stateStore
4761
+ stateStore,
4762
+ refreshAvailableProviders
4409
4763
  ]);
4410
4764
  const onCreateSession = useCallback(async () => {
4411
4765
  if (picked) await activateSession(null, picked.provider.key);
@@ -4426,24 +4780,87 @@ function AppShell() {
4426
4780
  denyAll();
4427
4781
  agentRef.current?.abort();
4428
4782
  }, [denyAll]);
4429
- const onPickModel = useCallback((modelId) => {
4783
+ /**
4784
+ * Pick a `{ providerKey, modelId }` tuple from the cross-provider
4785
+ * model picker. Two branches:
4786
+ *
4787
+ * - **Same provider, different model** — update `picked.model` +
4788
+ * remember it in `lastModelByProvider`. The active agent keeps
4789
+ * running; `agent.run({ model })` accepts the model per-call so
4790
+ * no session rebuild is needed.
4791
+ * - **Different provider** — swap the active `ProviderAuth`,
4792
+ * persist BOTH `lastProvider` + `lastModelByProvider`, then
4793
+ * re-activate the current session against the new provider's
4794
+ * factory. The session id (and conversation history) is
4795
+ * preserved; only the bound agent + provider instance change.
4796
+ *
4797
+ * Either branch re-resolves the reasoning effort: a non-reasoning
4798
+ * model loses its `effort`, a reasoning model gets the remembered
4799
+ * per-model value (falling back to a sensible default).
4800
+ */
4801
+ const onPickModel = useCallback(async (next) => {
4802
+ const nextProvider = availableProviders.find((p) => p.key === next.providerKey);
4803
+ if (!nextProvider) {
4804
+ debugLog("onPickModel: unknown provider key", next.providerKey);
4805
+ modal.close();
4806
+ return;
4807
+ }
4808
+ const descriptor = providerRegistry[nextProvider.key];
4809
+ const prior = stateStore.load();
4810
+ const providerChanged = picked?.provider.key !== nextProvider.key;
4811
+ stateStore.save({
4812
+ ...prior,
4813
+ ...providerChanged ? { lastProvider: nextProvider.key } : {},
4814
+ lastModelByProvider: {
4815
+ ...prior.lastModelByProvider,
4816
+ [nextProvider.key]: next.modelId
4817
+ }
4818
+ });
4819
+ const nextEffort = descriptor ? effortForModel(descriptor, next.modelId, prior.lastEffortByModel) : void 0;
4820
+ setPicked(nextEffort ? {
4821
+ provider: nextProvider,
4822
+ model: next.modelId,
4823
+ effort: nextEffort
4824
+ } : {
4825
+ provider: nextProvider,
4826
+ model: next.modelId
4827
+ });
4828
+ modal.close();
4829
+ if (providerChanged && currentSession && !busy && !pendingApproval) await activateSession(currentSession.id, nextProvider.key);
4830
+ }, [
4831
+ availableProviders,
4832
+ providerRegistry,
4833
+ stateStore,
4834
+ modal,
4835
+ picked,
4836
+ currentSession,
4837
+ busy,
4838
+ pendingApproval,
4839
+ activateSession
4840
+ ]);
4841
+ const onPickEffort = useCallback((effort) => {
4430
4842
  setPicked((prev) => {
4431
4843
  if (!prev) return prev;
4432
4844
  const prior = stateStore.load();
4433
4845
  stateStore.save({
4434
4846
  ...prior,
4435
- lastModelByProvider: {
4436
- ...prior.lastModelByProvider,
4437
- [prev.provider.key]: modelId
4847
+ lastEffortByModel: {
4848
+ ...prior.lastEffortByModel,
4849
+ [prev.model]: effort
4438
4850
  }
4439
4851
  });
4440
4852
  return {
4441
4853
  ...prev,
4442
- model: modelId
4854
+ effort
4443
4855
  };
4444
4856
  });
4445
4857
  modal.close();
4446
4858
  }, [modal, stateStore]);
4859
+ const modelHasReasoning = useMemo(() => {
4860
+ if (!picked) return false;
4861
+ const descriptor = providerRegistry[picked.provider.key];
4862
+ return !!descriptor && modelSupportsReasoning(descriptor, picked.model);
4863
+ }, [picked, providerRegistry]);
4447
4864
  const onPickAgent = useCallback(async (id) => {
4448
4865
  const profile = agentRegistry[id];
4449
4866
  if (!profile) return;
@@ -4500,7 +4917,8 @@ function AppShell() {
4500
4917
  try {
4501
4918
  await agent.run({
4502
4919
  model: picked.model,
4503
- prompt
4920
+ prompt,
4921
+ ...picked.effort ? { thinking: picked.effort } : {}
4504
4922
  });
4505
4923
  await session.save().catch((err) => debugLog("session.save failed", err));
4506
4924
  setCurrentSession((prev) => prev ? {
@@ -4521,7 +4939,6 @@ function AppShell() {
4521
4939
  setBusy(false);
4522
4940
  }
4523
4941
  }, [picked, stream]);
4524
- const pendingApproval = queue[0] ?? null;
4525
4942
  const onReauth = useMemo(() => {
4526
4943
  if (busy || pendingApproval) return void 0;
4527
4944
  return () => {
@@ -4792,7 +5209,7 @@ function AppShell() {
4792
5209
  }
4793
5210
  return;
4794
5211
  }
4795
- if (key.ctrl && key.name === "," && screen !== "auth") {
5212
+ if (key.ctrl && key.name === "o" && screen !== "auth") {
4796
5213
  modal.open(/* @__PURE__ */ jsx(SettingsModal, { actions: {
4797
5214
  onReauth,
4798
5215
  onOpenSkills: onOpenSkillsSettings,
@@ -4812,12 +5229,25 @@ function AppShell() {
4812
5229
  }
4813
5230
  if (key.ctrl && key.name === "m" && screen === "chat" && picked && !busy) {
4814
5231
  modal.open(/* @__PURE__ */ jsx(ModelPickerModal, {
4815
- models: modelsFor(picked.provider.key),
4816
- currentModelId: picked.model,
5232
+ providers: availableProviders,
5233
+ modelsFor,
5234
+ current: {
5235
+ providerKey: picked.provider.key,
5236
+ modelId: picked.model
5237
+ },
4817
5238
  onPick: onPickModel
4818
5239
  }));
4819
5240
  return;
4820
5241
  }
5242
+ if (key.ctrl && key.name === "n" && screen === "chat" && picked && !busy && modelHasReasoning) {
5243
+ const descriptor = providerRegistry[picked.provider.key];
5244
+ modal.open(/* @__PURE__ */ jsx(EffortPickerModal, {
5245
+ current: picked.effort,
5246
+ supportsAdaptive: !!descriptor && piIdOf(descriptor) === "anthropic",
5247
+ onPick: onPickEffort
5248
+ }));
5249
+ return;
5250
+ }
4821
5251
  if (key.ctrl && key.name === "s" && screen === "chat" && !busy && !pendingApproval) {
4822
5252
  enterSelectMode();
4823
5253
  return;
@@ -4848,6 +5278,9 @@ function AppShell() {
4848
5278
  hasMultipleAgents,
4849
5279
  modelLabel: picked?.model ?? null,
4850
5280
  modelColor: COLOR.model,
5281
+ effortLabel: modelHasReasoning ? picked?.effort ?? "medium" : null,
5282
+ effortColor: COLOR.warn,
5283
+ effortKeyColor: COLOR.warn,
4851
5284
  agentLabel: pickedAgent.label,
4852
5285
  agentColor: accentColor(pickedAgent.accent, COLOR)
4853
5286
  }), [
@@ -4858,7 +5291,8 @@ function AppShell() {
4858
5291
  hasMultipleAgents,
4859
5292
  picked,
4860
5293
  pickedAgent,
4861
- COLOR
5294
+ COLOR,
5295
+ modelHasReasoning
4862
5296
  ]);
4863
5297
  const promptTriggerHints = useMemo(() => {
4864
5298
  const out = [];
@@ -4941,12 +5375,25 @@ function AppShell() {
4941
5375
  });
4942
5376
  }
4943
5377
  /**
5378
+ * Resolve the reasoning effort to seed `Picked.effort` for the given model.
5379
+ * Returns `undefined` when the model has no reasoning knob; otherwise the
5380
+ * per-model remembered value, defaulting to `'medium'` (sensible middle
5381
+ * ground when the user has never picked an explicit effort for this model).
5382
+ */
5383
+ function effortForModel(descriptor, modelId, remembered) {
5384
+ if (!modelSupportsReasoning(descriptor, modelId)) return void 0;
5385
+ return remembered?.[modelId] ?? "medium";
5386
+ }
5387
+ /**
4944
5388
  * Build the footer's shortcut hints for the current screen. On the chat
4945
5389
  * screen the model id rides next to its `ctrl+m` shortcut and the agent
4946
5390
  * label rides next to `shift+tab`, each in its accent color — the bar
4947
- * doubles as the status display without needing separate badges.
5391
+ * doubles as the status display without needing separate badges. When
5392
+ * the active model exposes reasoning, the `ctrl+m` hint grows a
5393
+ * secondary `/n` chord with the current effort label, surfacing the
5394
+ * effort picker as a discoverable, in-place affordance.
4948
5395
  */
4949
- function buildHints({ screen, busy, pending, currentSession, hasMultipleAgents, modelLabel, modelColor, agentLabel, agentColor }) {
5396
+ function buildHints({ screen, busy, pending, currentSession, hasMultipleAgents, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor }) {
4950
5397
  if (pending) return [
4951
5398
  {
4952
5399
  key: "↑↓",
@@ -4993,7 +5440,7 @@ function buildHints({ screen, busy, pending, currentSession, hasMultipleAgents,
4993
5440
  label: "session"
4994
5441
  },
4995
5442
  {
4996
- key: "ctrl+,",
5443
+ key: "ctrl+o",
4997
5444
  label: "settings"
4998
5445
  },
4999
5446
  {
@@ -5001,23 +5448,30 @@ function buildHints({ screen, busy, pending, currentSession, hasMultipleAgents,
5001
5448
  label: currentSession ? "back" : "exit"
5002
5449
  }
5003
5450
  ];
5451
+ const modelHint = modelLabel ? {
5452
+ key: "ctrl+m",
5453
+ label: modelLabel,
5454
+ labelColor: modelColor,
5455
+ ...effortLabel ? { extra: {
5456
+ key: "/n",
5457
+ keyColor: effortKeyColor,
5458
+ label: effortLabel,
5459
+ labelColor: effortColor
5460
+ } } : {}
5461
+ } : null;
5004
5462
  return [
5005
5463
  ...hasMultipleAgents ? [{
5006
5464
  key: "shift+tab",
5007
5465
  label: agentLabel,
5008
5466
  labelColor: agentColor
5009
5467
  }] : [],
5010
- ...modelLabel ? [{
5011
- key: "ctrl+m",
5012
- label: modelLabel,
5013
- labelColor: modelColor
5014
- }] : [],
5468
+ ...modelHint ? [modelHint] : [],
5015
5469
  ...currentSession ? [{
5016
5470
  key: "ctrl+x",
5017
5471
  label: "session"
5018
5472
  }] : [],
5019
5473
  {
5020
- key: "ctrl+,",
5474
+ key: "ctrl+o",
5021
5475
  label: "settings"
5022
5476
  },
5023
5477
  {