Mike0021 commited on
Commit
9dda3c7
·
verified ·
1 Parent(s): 44728ba

Selects: our chevron in every engine

Browse files
Files changed (1) hide show
  1. piweb/hf-design.patch +331 -67
piweb/hf-design.patch CHANGED
@@ -6571,7 +6571,7 @@ index 79c8282..97731f1 100644
6571
  lang: "en",
6572
  icons: [
6573
  diff --git a/app/settings.css b/app/settings.css
6574
- index f0e2e95..52a7d18 100644
6575
  --- a/app/settings.css
6576
  +++ b/app/settings.css
6577
  @@ -35,14 +35,15 @@
@@ -8365,7 +8365,7 @@ index f0e2e95..52a7d18 100644
8365
  background: var(--bg-panel);
8366
  color: var(--text-muted);
8367
  font-family: var(--font-mono);
8368
- @@ -2256,9 +2461,8 @@
8369
  min-width: 0;
8370
  height: 32px;
8371
  padding: 0 10px;
@@ -8377,7 +8377,25 @@ index f0e2e95..52a7d18 100644
8377
  color: var(--text);
8378
  font-family: var(--font-mono);
8379
  font-size: 12px;
8380
- @@ -2350,10 +2554,9 @@ select.mcp-add-input {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8381
  max-width: 100%;
8382
  max-height: 100%;
8383
  overflow: hidden;
@@ -8391,7 +8409,7 @@ index f0e2e95..52a7d18 100644
8391
  }
8392
 
8393
  /* Focus moves to the dialog itself when it opens (tabIndex -1); it is not a control, so no ring. */
8394
- @@ -2366,7 +2569,7 @@ select.mcp-add-input {
8395
  flex: 1 1 auto;
8396
  gap: 12px;
8397
  min-height: 0;
@@ -8400,7 +8418,7 @@ index f0e2e95..52a7d18 100644
8400
  overflow: auto;
8401
  overscroll-behavior: contain;
8402
  }
8403
- @@ -2399,7 +2602,7 @@ select.mcp-add-input {
8404
  margin-top: 10px;
8405
  padding: 8px 10px;
8406
  border: 1px solid var(--border);
@@ -8409,7 +8427,7 @@ index f0e2e95..52a7d18 100644
8409
  background: var(--bg);
8410
  color: var(--text);
8411
  font-family: var(--font-mono);
8412
- @@ -2417,7 +2620,7 @@ select.mcp-add-input {
8413
  .project-trust-error {
8414
  flex-shrink: 0;
8415
  max-height: 30vh;
@@ -8418,7 +8436,7 @@ index f0e2e95..52a7d18 100644
8418
  overflow: auto;
8419
  color: #ef4444;
8420
  font-size: 12px;
8421
- @@ -2437,19 +2640,20 @@ select.mcp-add-input {
8422
  flex-shrink: 0;
8423
  justify-content: flex-end;
8424
  gap: 8px;
@@ -8446,7 +8464,7 @@ index f0e2e95..52a7d18 100644
8446
  }
8447
 
8448
  .project-trust-button:disabled {
8449
- @@ -2457,10 +2661,8 @@ select.mcp-add-input {
8450
  }
8451
 
8452
  .project-trust-button.is-primary {
@@ -8459,7 +8477,7 @@ index f0e2e95..52a7d18 100644
8459
  }
8460
 
8461
  .project-trust-button.is-primary:disabled {
8462
- @@ -2534,7 +2736,7 @@ select.mcp-add-input {
8463
  min-width: 0;
8464
  padding: 8px 10px;
8465
  border: 1px solid var(--border);
@@ -8468,7 +8486,7 @@ index f0e2e95..52a7d18 100644
8468
  background: var(--bg);
8469
  }
8470
 
8471
- @@ -2556,7 +2758,7 @@ select.mcp-add-input {
8472
  .project-trust-mcp-tag {
8473
  flex-shrink: 0;
8474
  padding: 1px 5px;
@@ -8477,7 +8495,7 @@ index f0e2e95..52a7d18 100644
8477
  background: rgba(120, 120, 120, 0.12);
8478
  color: var(--text-dim);
8479
  font-size: 10px;
8480
- @@ -2577,7 +2779,7 @@ select.mcp-add-input {
8481
  display: inline-block;
8482
  margin: 2px 4px 0 0;
8483
  padding: 0 4px;
@@ -10769,10 +10787,10 @@ index 0b8f66a..9a60b64 100644
10769
  .sidebar-tool-button.is-active,
10770
  diff --git a/app/ui.css b/app/ui.css
10771
  new file mode 100644
10772
- index 0000000..bf28dc3
10773
  --- /dev/null
10774
  +++ b/app/ui.css
10775
- @@ -0,0 +1,901 @@
10776
  +/* The shared components on Base UI (components/ui/*): menus, tooltips,
10777
  + popovers and the composer's suggestion list. Popups are portaled to <body>,
10778
  + so they stack by the positioner's z-index, above every pane. */
@@ -11674,6 +11692,40 @@ index 0000000..bf28dc3
11674
  +}
11675
  +/* Quiet, on the left, its icon on the folders' column. */
11676
  +.directory-picker-new { margin-right: auto; padding-left: var(--popup-row-pad); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11677
  diff --git a/components/AgentSessionPanel.tsx b/components/AgentSessionPanel.tsx
11678
  index 8d51f3c..61fcf9e 100644
11679
  --- a/components/AgentSessionPanel.tsx
@@ -11793,19 +11845,20 @@ index 8d51f3c..61fcf9e 100644
11793
  }}
11794
  />
11795
  diff --git a/components/AgentsConfig.tsx b/components/AgentsConfig.tsx
11796
- index 861a1c6..d5d466e 100644
11797
  --- a/components/AgentsConfig.tsx
11798
  +++ b/components/AgentsConfig.tsx
11799
- @@ -38,6 +38,8 @@ import {
11800
  } from "./SettingsUi";
11801
  import { ModelSelector } from "./ModelSelector";
11802
  import { projectTrustReloadKey } from "./settings-ui-helpers";
11803
  +import { CarbonIcon } from "./CarbonIcon";
11804
  +import * as Carbon from "./carbon/icons";
 
11805
 
11806
  const TOOL_OPTIONS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
11807
  const THINKING_OPTIONS = ["", "off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
11808
- @@ -67,7 +69,7 @@ const inputStyle: CSSProperties = {
11809
  height: 34,
11810
  padding: "0 9px",
11811
  border: "1px solid var(--border)",
@@ -11814,7 +11867,43 @@ index 861a1c6..d5d466e 100644
11814
  background: "var(--bg)",
11815
  color: "var(--text)",
11816
  fontSize: 12,
11817
- @@ -727,9 +729,7 @@ export function AgentsConfig({
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11818
  className={savedOk ? "is-success" : undefined}
11819
  >
11820
  {savedOk && (
@@ -22587,8 +22676,64 @@ index e04cdc1..3bedf1f 100644
22587
  const filePath = onOpenFile ? resolveLocalFileHref(href, cwd) : null;
22588
  const openFile = onOpenFile;
22589
  if (!filePath || !openFile) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22590
  diff --git a/components/McpConfig.tsx b/components/McpConfig.tsx
22591
- index 97e86ee..faf8050 100644
22592
  --- a/components/McpConfig.tsx
22593
  +++ b/components/McpConfig.tsx
22594
  @@ -1,5 +1,6 @@
@@ -22598,7 +22743,15 @@ index 97e86ee..faf8050 100644
22598
  import { useCallback, useEffect, useId, useRef, useState, type Ref } from "react";
22599
  import type { McpExposure } from "@earendil-works/pi-coding-agent";
22600
  import type {
22601
- @@ -795,6 +796,7 @@ export function McpConfig({
 
 
 
 
 
 
 
 
22602
  setAddFailure(null);
22603
  }}
22604
  onAddSubmit={(request) => void submitAdd(request)}
@@ -22606,7 +22759,7 @@ index 97e86ee..faf8050 100644
22606
  onTest={(server) => void testServer(server)}
22607
  onSignIn={(server) => void startSignIn(server)}
22608
  onSignOut={(server) => void signOut(server)}
22609
- @@ -850,6 +852,7 @@ export function McpConfigView({
22610
  onAddOpen = () => {},
22611
  onAddDraftChange = () => {},
22612
  onAddSubmit = () => {},
@@ -22614,7 +22767,7 @@ index 97e86ee..faf8050 100644
22614
  onSelect,
22615
  onRefresh,
22616
  onCodemodeChange,
22617
- @@ -896,6 +899,8 @@ export function McpConfigView({
22618
  onAddOpen?: () => void;
22619
  onAddDraftChange?: (draft: McpAddDraft) => void;
22620
  onAddSubmit?: (request: McpAddActionRequest) => void;
@@ -22623,7 +22776,7 @@ index 97e86ee..faf8050 100644
22623
  onSelect: (key: string) => void;
22624
  onRefresh: () => void;
22625
  onCodemodeChange: (preference: McpCodemodePreference) => void;
22626
- @@ -1099,6 +1104,15 @@ export function McpConfigView({
22627
  </>
22628
  ) : null}
22629
  </ConfigSidebarList>
@@ -22639,6 +22792,24 @@ index 97e86ee..faf8050 100644
22639
  {data && (
22640
  <ConfigListAction ref={addActionRef} active={adding} onClick={onAddOpen}>
22641
  {t("mcp.add.action")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22642
  diff --git a/components/MermaidBlock.test.mjs b/components/MermaidBlock.test.mjs
22643
  index 45088d3..cf1df98 100644
22644
  --- a/components/MermaidBlock.test.mjs
@@ -25027,7 +25198,7 @@ index bc635a4..5872f05 100644
25027
  + assert.equal(providerMatchesSearch(" ", oauth.id, oauth.name), true);
25028
  +});
25029
  diff --git a/components/ModelsConfig.tsx b/components/ModelsConfig.tsx
25030
- index 3cfaabb..019cb48 100644
25031
  --- a/components/ModelsConfig.tsx
25032
  +++ b/components/ModelsConfig.tsx
25033
  @@ -14,9 +14,11 @@ import {
@@ -25042,16 +25213,17 @@ index 3cfaabb..019cb48 100644
25042
  trackAddedModels,
25043
  collectModelRenames,
25044
  renameProviderEntry,
25045
- @@ -56,6 +58,8 @@ import { providerBadgeLabel } from "./enabled-models-helpers";
25046
  import { OAuthPastePanel } from "./OAuthPastePanel";
25047
  import { ProviderIcon } from "./ProviderIcon";
25048
  import { ProviderUsageSummary } from "./ProviderUsageSummary";
25049
  +import { CarbonIcon } from "./CarbonIcon";
25050
  +import * as Carbon from "./carbon/icons";
 
25051
 
25052
  // ── Types ─────────────────────────────────────────────────────────────────────
25053
 
25054
- @@ -93,6 +97,7 @@ interface ModelEntry {
25055
  id: string;
25056
  name?: string;
25057
  api?: string;
@@ -25059,7 +25231,7 @@ index 3cfaabb..019cb48 100644
25059
  reasoning?: boolean;
25060
  thinkingLevelMap?: Record<string, string | null>;
25061
  input?: string[];
25062
- @@ -186,7 +191,7 @@ const inputStyle = {
25063
  padding: "6px 9px",
25064
  background: "var(--bg-panel)",
25065
  border: "1px solid var(--border)",
@@ -25068,7 +25240,7 @@ index 3cfaabb..019cb48 100644
25068
  color: "var(--text)",
25069
  fontSize: 12,
25070
  outline: "none",
25071
- @@ -260,17 +265,9 @@ function SecretTextInput({
25072
  }}
25073
  >
25074
  {visible ? (
@@ -25088,7 +25260,22 @@ index 3cfaabb..019cb48 100644
25088
  )}
25089
  </button>
25090
  </div>
25091
- @@ -415,7 +412,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25092
  <TextInput value={editingName} onChange={onEditingNameChange} placeholder="provider-name" mono />
25093
  {editingName !== name && editingName.trim() && (
25094
  <button onClick={() => onRename(editingName.trim())}
@@ -25097,7 +25284,7 @@ index 3cfaabb..019cb48 100644
25097
  {t("i18n.rename")}
25098
  </button>
25099
  )}
25100
- @@ -424,17 +421,11 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25101
  <Field label="Base URL">
25102
  <TextInput value={provider.baseUrl ?? ""} onChange={(v) => set("baseUrl", v || undefined)}
25103
  placeholder="https://api.example.com/v1" mono />
@@ -25116,7 +25303,7 @@ index 3cfaabb..019cb48 100644
25116
  </Field>
25117
 
25118
  <Field label="API">
25119
- @@ -446,9 +437,6 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25120
  headers={provider.headers}
25121
  onChange={(headers) => set("headers", headers)}
25122
  />
@@ -25126,7 +25313,7 @@ index 3cfaabb..019cb48 100644
25126
  </Field>
25127
 
25128
  <div style={{ borderTop: "1px solid var(--border)", paddingTop: 14, display: "flex", flexDirection: "column", gap: 10 }}>
25129
- @@ -457,7 +445,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25130
  onClick={handleDiscoverModels}
25131
  disabled={discoveryState.phase === "loading"}
25132
  style={{
@@ -25135,7 +25322,7 @@ index 3cfaabb..019cb48 100644
25135
  background: "var(--bg-panel)", color: discoveryState.phase === "loading" ? "var(--text-dim)" : "var(--text-muted)",
25136
  cursor: discoveryState.phase === "loading" ? "not-allowed" : "pointer", fontSize: 11,
25137
  }}
25138
- @@ -467,7 +455,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25139
  )}
25140
 
25141
  {discoveryState.phase === "error" && (
@@ -25144,7 +25331,7 @@ index 3cfaabb..019cb48 100644
25144
  {discoveryState.message}
25145
  </div>
25146
  )}
25147
- @@ -482,7 +470,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25148
  style={{ ...inputStyle, width: "100%", minWidth: 0 }}
25149
  />
25150
 
@@ -25153,7 +25340,7 @@ index 3cfaabb..019cb48 100644
25153
  <label
25154
  style={{
25155
  minHeight: 32, padding: "5px 9px", display: "flex", alignItems: "center", gap: 8,
25156
- @@ -541,7 +529,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25157
  <button
25158
  onClick={addSelectedModels}
25159
  disabled={selectedCount === 0}
@@ -25162,7 +25349,7 @@ index 3cfaabb..019cb48 100644
25162
  >
25163
  {selectedCount
25164
  ? t("models.discoveryAddSelectedCount", { count: selectedCount })
25165
- @@ -563,10 +551,11 @@ type ThinkingLevel = typeof THINKING_LEVELS[number];
25166
 
25167
  const LEVEL_COLORS: Record<ThinkingLevel, string> = {
25168
  off: "var(--text-dim)",
@@ -25178,7 +25365,7 @@ index 3cfaabb..019cb48 100644
25178
  xhigh: "#fb923c",
25179
  max: "#ef4444",
25180
  };
25181
- @@ -629,7 +618,7 @@ function ThinkingLevelMapEditor({
25182
  alignItems: "center",
25183
  gap: 8,
25184
  padding: "5px 4px",
@@ -25187,7 +25374,7 @@ index 3cfaabb..019cb48 100644
25187
  background: "transparent",
25188
  border: "1px solid transparent",
25189
  }}
25190
- @@ -646,7 +635,7 @@ function ThinkingLevelMapEditor({
25191
  </span>
25192
  </div>
25193
 
@@ -25196,7 +25383,7 @@ index 3cfaabb..019cb48 100644
25196
  <button
25197
  onClick={() => setLevel(level, "omit")}
25198
  style={{ ...btnBase, ...(state === "omit" ? btnActive : {}) }}
25199
- @@ -661,7 +650,7 @@ function ThinkingLevelMapEditor({
25200
  </button>
25201
  </div>
25202
 
@@ -25205,7 +25392,7 @@ index 3cfaabb..019cb48 100644
25205
  <button
25206
  onClick={() => setLevel(level, strVal || level)}
25207
  style={{ ...btnBase, ...(state === "string" ? btnActive : {}), borderRight: "1px solid var(--border)", flexShrink: 0 }}
25208
- @@ -749,7 +738,7 @@ function HeaderListEditor({ headers, onChange }: {
25209
  padding: "6px 9px",
25210
  background: "none",
25211
  border: "1px solid rgba(239,68,68,0.3)",
@@ -25214,7 +25401,7 @@ index 3cfaabb..019cb48 100644
25214
  color: "#ef4444",
25215
  cursor: "pointer",
25216
  fontSize: 11,
25217
- @@ -770,7 +759,7 @@ function HeaderListEditor({ headers, onChange }: {
25218
  ...current,
25219
  { id: nextRowIdRef.current++, name: "", value: "" },
25220
  ])}
@@ -25223,7 +25410,7 @@ index 3cfaabb..019cb48 100644
25223
  + Add header
25224
  </button>
25225
  </div>
25226
- @@ -842,6 +831,8 @@ function ModelDetail({
25227
  const [costDraft, setCostDraft] = useState<ModelCostDraft>(() => modelCostToDraft(model.cost));
25228
  const costDraftRef = useRef(costDraft);
25229
  const [advancedOpen, setAdvancedOpen] = useState(false);
@@ -25232,7 +25419,7 @@ index 3cfaabb..019cb48 100644
25232
  const catalogRequestIdRef = useRef(0);
25233
  const catalogUndoRef = useRef<ModelEntry | null>(null);
25234
  const costTemplateRef = useRef(model.cost);
25235
- @@ -885,17 +876,21 @@ function ModelDetail({
25236
  })();
25237
 
25238
  useEffect(() => {
@@ -25256,7 +25443,7 @@ index 3cfaabb..019cb48 100644
25256
  setTestState({ phase: "testing" });
25257
  try {
25258
  const res = await fetch("/api/models-config/test", {
25259
- @@ -910,6 +905,7 @@ function ModelDetail({
25260
  status?: number;
25261
  responseText?: string;
25262
  };
@@ -25264,7 +25451,7 @@ index 3cfaabb..019cb48 100644
25264
  if (!res.ok || !d.ok) {
25265
  setTestState({
25266
  phase: "error",
25267
- @@ -926,6 +922,7 @@ function ModelDetail({
25268
  responseText: d.responseText,
25269
  });
25270
  } catch (e) {
@@ -25272,7 +25459,7 @@ index 3cfaabb..019cb48 100644
25272
  setTestState({ phase: "error", message: e instanceof Error ? e.message : String(e) });
25273
  }
25274
  }, [model, provider, providerName, testState.phase]);
25275
- @@ -937,7 +934,7 @@ function ModelDetail({
25276
  setCatalogState({ phase: "loading" });
25277
  try {
25278
  const params = new URLSearchParams({ q: query, provider: providerName, limit: "50" });
@@ -25281,7 +25468,7 @@ index 3cfaabb..019cb48 100644
25281
  const res = await fetch(`/api/models-config/catalog?${params}`);
25282
  const data = await res.json() as { recommendation?: ModelCatalogRecommendation; error?: string };
25283
  if (requestId !== catalogRequestIdRef.current) return;
25284
- @@ -960,7 +957,7 @@ function ModelDetail({
25285
  if (requestId !== catalogRequestIdRef.current) return;
25286
  setCatalogState({ phase: "error", message: error instanceof Error ? error.message : String(error) });
25287
  }
@@ -25290,7 +25477,7 @@ index 3cfaabb..019cb48 100644
25290
 
25291
  const undoCatalogFill = () => {
25292
  const previous = catalogUndoRef.current;
25293
- @@ -1023,6 +1020,7 @@ function ModelDetail({
25294
  }
25295
  compatibilityOverrideCount += remainingCompatKeys.size;
25296
  const advancedSummaryParts = [
@@ -25298,7 +25485,7 @@ index 3cfaabb..019cb48 100644
25298
  model.api ? `API: ${model.api}` : null,
25299
  Object.keys(model.headers ?? {}).length
25300
  ? t("models.headersSummary", { count: Object.keys(model.headers ?? {}).length })
25301
- @@ -1053,9 +1051,9 @@ function ModelDetail({
25302
  height: 28,
25303
  padding: "0 8px",
25304
  border: `1px solid ${testState.phase === "error" ? "#fecaca" : testState.phase === "success" ? "#bbf7d0" : "var(--border)"}`,
@@ -25311,7 +25498,7 @@ index 3cfaabb..019cb48 100644
25311
  fontSize: 11,
25312
  display: "inline-flex",
25313
  alignItems: "center",
25314
- @@ -1077,9 +1075,7 @@ function ModelDetail({
25315
  className={testState.phase === "success" ? "is-success" : undefined}
25316
  >
25317
  {testState.phase === "success" && (
@@ -25322,7 +25509,7 @@ index 3cfaabb..019cb48 100644
25322
  )}
25323
  {testState.phase === "testing" ? t("i18n.checking") : testState.phase === "success" ? t("common.ok") : t("i18n.test")}
25324
  </ConfigButton>
25325
- @@ -1098,7 +1094,7 @@ function ModelDetail({
25326
  onClick={() => void handleCatalogFill()}
25327
  disabled={!model.id.trim() || catalogState.phase === "loading"}
25328
  style={{
@@ -25331,7 +25518,7 @@ index 3cfaabb..019cb48 100644
25331
  background: "var(--bg-panel)",
25332
  color: !model.id.trim() || catalogState.phase === "loading" ? "var(--text-dim)" : "var(--text-muted)",
25333
  cursor: !model.id.trim() || catalogState.phase === "loading" ? "not-allowed" : "pointer",
25334
- @@ -1177,7 +1173,7 @@ function ModelDetail({
25335
  </div>
25336
 
25337
  <div style={{ marginTop: 16 }}>
@@ -25340,7 +25527,7 @@ index 3cfaabb..019cb48 100644
25340
  {t("models.costPerMillion")}
25341
  </div>
25342
  {costEditing ? (
25343
- @@ -1229,24 +1225,16 @@ function ModelDetail({
25344
  {advancedSummary}
25345
  </span>
25346
  </span>
@@ -25371,7 +25558,7 @@ index 3cfaabb..019cb48 100644
25371
  <Field label={t("models.apiOverride")}>
25372
  <Select value={model.api ?? ""} onChange={(v) => set("api", v || undefined)} options={API_OPTIONS} />
25373
  </Field>
25374
- @@ -1274,6 +1262,18 @@ function ModelDetail({
25375
  checked={effectiveCompat(provider, model)["supportsDeveloperRole"] !== false}
25376
  onChange={(v) => onChange(setCompatBool(model, "supportsDeveloperRole", v))}
25377
  />
@@ -25390,7 +25577,7 @@ index 3cfaabb..019cb48 100644
25390
  <div style={{ marginTop: 4 }}>
25391
  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, marginBottom: 8 }}>
25392
  <SectionTitle>{t("models.thinkingLevelMap")}</SectionTitle>
25393
- @@ -1495,7 +1495,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25394
  <button
25395
  key={option.id}
25396
  onClick={() => submitSelection(loginState.token, option.id)}
@@ -25399,7 +25586,7 @@ index 3cfaabb..019cb48 100644
25399
  >
25400
  {option.label}
25401
  </button>
25402
- @@ -1530,7 +1530,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25403
  <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
25404
  Open the verification page and enter this code:
25405
  </p>
@@ -25408,7 +25595,7 @@ index 3cfaabb..019cb48 100644
25408
  {loginState.userCode}
25409
  </div>
25410
  <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
25411
- @@ -1670,7 +1670,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25412
  style={{
25413
  padding: "6px 12px",
25414
  background: savedOk ? "#16a34a" : apiKey.trim() ? "var(--accent)" : "var(--bg-panel)",
@@ -25417,7 +25604,7 @@ index 3cfaabb..019cb48 100644
25417
  color: savedOk ? "#fff" : apiKey.trim() ? "var(--accent-contrast)" : "var(--text-dim)",
25418
  cursor: (saving || !apiKey.trim() || savedOk) ? "not-allowed" : "pointer",
25419
  fontSize: 12, fontWeight: 600, flexShrink: 0,
25420
- @@ -1678,9 +1678,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25421
  }}
25422
  >
25423
  {savedOk && (
@@ -25428,7 +25615,7 @@ index 3cfaabb..019cb48 100644
25428
  )}
25429
  {savedOk ? t("i18n.saved") : saving ? t("i18n.saving") : t("i18n.save")}
25430
  </button>
25431
- @@ -1718,8 +1716,8 @@ function AddProviderPicker({
25432
 
25433
  const q = search.trim().toLowerCase();
25434
 
@@ -25439,7 +25626,7 @@ index 3cfaabb..019cb48 100644
25439
  const showCustom = !q || "custom".includes(q) || "openai-compatible".includes(q) || "anthropic-compatible".includes(q);
25440
 
25441
  const totalCount = availableOAuth.length + availableApiKey.length + (showCustom ? 1 : 0);
25442
- @@ -1729,7 +1727,7 @@ function AddProviderPicker({
25443
  padding: "10px 12px",
25444
  background: "var(--bg-panel)",
25445
  border: "1px solid var(--border)",
@@ -25448,7 +25635,7 @@ index 3cfaabb..019cb48 100644
25448
  boxSizing: "border-box",
25449
  cursor: "pointer",
25450
  minWidth: 0,
25451
- @@ -1751,12 +1749,10 @@ function AddProviderPicker({
25452
  onClose();
25453
  }}
25454
  >
@@ -25463,7 +25650,7 @@ index 3cfaabb..019cb48 100644
25463
  <input
25464
  ref={inputRef}
25465
  value={search}
25466
- @@ -1773,7 +1769,7 @@ function AddProviderPicker({
25467
  ) : (
25468
  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(240px, 100%), 1fr))", gap: 8 }}>
25469
  {showCustom && (
@@ -25472,7 +25659,7 @@ index 3cfaabb..019cb48 100644
25472
  )}
25473
  {showCustom && (
25474
  <button
25475
- @@ -1786,16 +1782,14 @@ function AddProviderPicker({
25476
  <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>OpenAI / Anthropic compatible</div>
25477
  <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>{t("i18n.customEndpoint")}</div>
25478
  </div>
@@ -25492,7 +25679,7 @@ index 3cfaabb..019cb48 100644
25492
  )}
25493
  {availableOAuth.map((p) => (
25494
  <button key={p.id} onClick={() => { onSelectOAuth(p.id); onClose(); }}
25495
- @@ -1812,7 +1806,7 @@ function AddProviderPicker({
25496
  ))}
25497
 
25498
  {availableApiKey.length > 0 && (
@@ -25501,7 +25688,7 @@ index 3cfaabb..019cb48 100644
25501
  )}
25502
  {availableApiKey.map((p) => (
25503
  <button key={p.id} onClick={() => { onSelectApiKey(p.id); onClose(); }}
25504
- @@ -2216,13 +2210,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25505
  onClick={() => setSelection({ type: "provider", name: pName })}
25506
  active={isProviderSelected}
25507
  >
@@ -25516,7 +25703,7 @@ index 3cfaabb..019cb48 100644
25516
  <ConfigSidebarText className="is-grow">
25517
  {pName}
25518
  </ConfigSidebarText>
25519
- @@ -2243,7 +2231,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25520
  {m.id || t("i18n.newModel")}
25521
  </ConfigSidebarText>
25522
  {m.reasoning && (
@@ -25525,7 +25712,7 @@ index 3cfaabb..019cb48 100644
25525
  )}
25526
  </ConfigSidebarItem>
25527
  );
25528
- @@ -2276,11 +2264,11 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25529
  </ConfigSplitView>
25530
 
25531
  {/* Footer */}
@@ -25539,7 +25726,7 @@ index 3cfaabb..019cb48 100644
25539
  {!embedded && <ConfigButton onClick={onClose}>{t("i18n.cancel")}</ConfigButton>}
25540
  <ConfigButton
25541
  variant="primary"
25542
- @@ -2289,10 +2277,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25543
  className={savedOk ? "is-success" : undefined}
25544
  >
25545
  {savedOk && (
@@ -32534,6 +32721,83 @@ index 0000000..5d2e784
32534
  + else if (also) (also as { current: T | null }).current = node;
32535
  + }, [also]);
32536
  +}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32537
  diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
32538
  new file mode 100644
32539
  index 0000000..019f84d
 
6571
  lang: "en",
6572
  icons: [
6573
  diff --git a/app/settings.css b/app/settings.css
6574
+ index f0e2e95..4d4f447 100644
6575
  --- a/app/settings.css
6576
  +++ b/app/settings.css
6577
  @@ -35,14 +35,15 @@
 
8365
  background: var(--bg-panel);
8366
  color: var(--text-muted);
8367
  font-family: var(--font-mono);
8368
+ @@ -2256,27 +2461,14 @@
8369
  min-width: 0;
8370
  height: 32px;
8371
  padding: 0 10px;
 
8377
  color: var(--text);
8378
  font-family: var(--font-mono);
8379
  font-size: 12px;
8380
+ outline: none;
8381
+ }
8382
+
8383
+ -/* macOS draws the native arrow against the border, ignoring the padding: a chevron of
8384
+ - our own, in the theme's dim text color, as far from the right edge as the text is from the left. */
8385
+ -select.mcp-add-input {
8386
+ - appearance: none;
8387
+ - padding-right: 30px;
8388
+ - background-image:
8389
+ - linear-gradient(45deg, transparent 40%, var(--text-dim) 40%, var(--text-dim) 60%, transparent 60%),
8390
+ - linear-gradient(-45deg, transparent 40%, var(--text-dim) 40%, var(--text-dim) 60%, transparent 60%);
8391
+ - background-size: 5px 5px;
8392
+ - background-position: right 15px center, right 10px center;
8393
+ - background-repeat: no-repeat;
8394
+ -}
8395
+
8396
+ .mcp-add-toggle {
8397
+ display: flex;
8398
+ @@ -2350,10 +2542,9 @@ select.mcp-add-input {
8399
  max-width: 100%;
8400
  max-height: 100%;
8401
  overflow: hidden;
 
8409
  }
8410
 
8411
  /* Focus moves to the dialog itself when it opens (tabIndex -1); it is not a control, so no ring. */
8412
+ @@ -2366,7 +2557,7 @@ select.mcp-add-input {
8413
  flex: 1 1 auto;
8414
  gap: 12px;
8415
  min-height: 0;
 
8418
  overflow: auto;
8419
  overscroll-behavior: contain;
8420
  }
8421
+ @@ -2399,7 +2590,7 @@ select.mcp-add-input {
8422
  margin-top: 10px;
8423
  padding: 8px 10px;
8424
  border: 1px solid var(--border);
 
8427
  background: var(--bg);
8428
  color: var(--text);
8429
  font-family: var(--font-mono);
8430
+ @@ -2417,7 +2608,7 @@ select.mcp-add-input {
8431
  .project-trust-error {
8432
  flex-shrink: 0;
8433
  max-height: 30vh;
 
8436
  overflow: auto;
8437
  color: #ef4444;
8438
  font-size: 12px;
8439
+ @@ -2437,19 +2628,20 @@ select.mcp-add-input {
8440
  flex-shrink: 0;
8441
  justify-content: flex-end;
8442
  gap: 8px;
 
8464
  }
8465
 
8466
  .project-trust-button:disabled {
8467
+ @@ -2457,10 +2649,8 @@ select.mcp-add-input {
8468
  }
8469
 
8470
  .project-trust-button.is-primary {
 
8477
  }
8478
 
8479
  .project-trust-button.is-primary:disabled {
8480
+ @@ -2534,7 +2724,7 @@ select.mcp-add-input {
8481
  min-width: 0;
8482
  padding: 8px 10px;
8483
  border: 1px solid var(--border);
 
8486
  background: var(--bg);
8487
  }
8488
 
8489
+ @@ -2556,7 +2746,7 @@ select.mcp-add-input {
8490
  .project-trust-mcp-tag {
8491
  flex-shrink: 0;
8492
  padding: 1px 5px;
 
8495
  background: rgba(120, 120, 120, 0.12);
8496
  color: var(--text-dim);
8497
  font-size: 10px;
8498
+ @@ -2577,7 +2767,7 @@ select.mcp-add-input {
8499
  display: inline-block;
8500
  margin: 2px 4px 0 0;
8501
  padding: 0 4px;
 
10787
  .sidebar-tool-button.is-active,
10788
  diff --git a/app/ui.css b/app/ui.css
10789
  new file mode 100644
10790
+ index 0000000..32e2abf
10791
  --- /dev/null
10792
  +++ b/app/ui.css
10793
+ @@ -0,0 +1,935 @@
10794
  +/* The shared components on Base UI (components/ui/*): menus, tooltips,
10795
  + popovers and the composer's suggestion list. Popups are portaled to <body>,
10796
  + so they stack by the positioner's z-index, above every pane. */
 
11692
  +}
11693
  +/* Quiet, on the left, its icon on the folders' column. */
11694
  +.directory-picker-new { margin-right: auto; padding-left: var(--popup-row-pad); }
11695
+ +
11696
+ +/* ---------------------------------------------------------------- selects
11697
+ + components/ui/select.tsx: a native select with Carbon's 12px chevron instead of the
11698
+ + engine's arrow (Chromium's sat 3px from the edge whatever the padding, after 4.5px of
11699
+ + its own before the text; Firefox and Safari placed theirs elsewhere). The chevron's ink
11700
+ + mirrors the text's padding (--select-pad; its ink starts 2.25px inside its box, the
11701
+ + text's 0.75px past the padding) and starts 6.25px after the text, as in the menu triggers.
11702
+ + The select keeps its own colours and border; the padding is set by the component. */
11703
+ +.ui-select {
11704
+ + position: relative;
11705
+ + display: inline-flex;
11706
+ + flex: none;
11707
+ + min-width: 0;
11708
+ + max-width: 100%;
11709
+ + vertical-align: middle;
11710
+ +}
11711
+ +.ui-select.is-fill { display: flex; width: 100%; }
11712
+ +.ui-select > select {
11713
+ + -webkit-appearance: none;
11714
+ + appearance: none;
11715
+ + flex: 1 1 auto;
11716
+ + min-width: 0;
11717
+ + cursor: pointer;
11718
+ +}
11719
+ +.ui-select > select:disabled { cursor: default; }
11720
+ +.ui-select-chevron {
11721
+ + position: absolute;
11722
+ + top: 50%;
11723
+ + right: calc(var(--select-pad, 10px) + var(--select-border, 0px) - 1.5px);
11724
+ + color: var(--text-dim);
11725
+ + transform: translateY(-50%);
11726
+ + pointer-events: none;
11727
+ +}
11728
+ +.ui-select:has(> select:disabled) > .ui-select-chevron { opacity: 0.45; }
11729
  diff --git a/components/AgentSessionPanel.tsx b/components/AgentSessionPanel.tsx
11730
  index 8d51f3c..61fcf9e 100644
11731
  --- a/components/AgentSessionPanel.tsx
 
11845
  }}
11846
  />
11847
  diff --git a/components/AgentsConfig.tsx b/components/AgentsConfig.tsx
11848
+ index 861a1c6..efd7e87 100644
11849
  --- a/components/AgentsConfig.tsx
11850
  +++ b/components/AgentsConfig.tsx
11851
+ @@ -38,6 +38,9 @@ import {
11852
  } from "./SettingsUi";
11853
  import { ModelSelector } from "./ModelSelector";
11854
  import { projectTrustReloadKey } from "./settings-ui-helpers";
11855
  +import { CarbonIcon } from "./CarbonIcon";
11856
  +import * as Carbon from "./carbon/icons";
11857
+ +import { NativeSelect } from "./ui/select";
11858
 
11859
  const TOOL_OPTIONS = ["read", "bash", "edit", "write", "grep", "find", "ls"];
11860
  const THINKING_OPTIONS = ["", "off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
11861
+ @@ -67,7 +70,7 @@ const inputStyle: CSSProperties = {
11862
  height: 34,
11863
  padding: "0 9px",
11864
  border: "1px solid var(--border)",
 
11867
  background: "var(--bg)",
11868
  color: "var(--text)",
11869
  fontSize: 12,
11870
+ @@ -662,17 +665,20 @@ export function AgentsConfig({
11871
+
11872
+ <Field label={t("agents.codemode")}>
11873
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
11874
+ - <select
11875
+ + <NativeSelect
11876
+ + pad={9}
11877
+ + border={1}
11878
+ + wrapperStyle={{ alignSelf: "flex-start" }}
11879
+ aria-label={t("agents.codemode")}
11880
+ value={draft.codemode ?? "off"}
11881
+ disabled={disabled}
11882
+ onChange={(event) => update("codemode", event.target.value as EditableProfile["codemode"])}
11883
+ - style={{ ...controlStyle, width: "auto", minWidth: 180, alignSelf: "flex-start" }}
11884
+ + style={{ ...controlStyle, width: "auto", minWidth: 180 }}
11885
+ >
11886
+ <option value="inherit">{t("agents.codemode.inherit")}</option>
11887
+ <option value="on">{t("agents.codemode.on")}</option>
11888
+ <option value="off">{t("agents.codemode.off")}</option>
11889
+ - </select>
11890
+ + </NativeSelect>
11891
+ <span style={{ color: "var(--text-dim)", fontSize: 11, lineHeight: 1.4 }}>
11892
+ {t("agents.codemodeHint")}
11893
+ </span>
11894
+ @@ -700,9 +706,9 @@ export function AgentsConfig({
11895
+ </div>
11896
+ </Field>
11897
+ <Field label={t("agents.thinking")}>
11898
+ - <select aria-label={t("agents.thinking")} value={draft.thinking ?? ""} disabled={disabled} onChange={(event) => update("thinking", (event.target.value || undefined) as EditableProfile["thinking"])} style={controlStyle}>
11899
+ + <NativeSelect pad={9} border={1} fill aria-label={t("agents.thinking")} value={draft.thinking ?? ""} disabled={disabled} onChange={(event) => update("thinking", (event.target.value || undefined) as EditableProfile["thinking"])} style={controlStyle}>
11900
+ {THINKING_OPTIONS.map((value) => <option key={value || "default"} value={value}>{value || t("agents.inherit")}</option>)}
11901
+ - </select>
11902
+ + </NativeSelect>
11903
+ </Field>
11904
+ <Field label={t("agents.maxTurns")}>
11905
+ <input aria-label={t("agents.maxTurns")} type="number" min={1} value={draft.maxTurns ?? ""} disabled={disabled} onChange={(event) => update("maxTurns", event.target.value ? Number(event.target.value) : undefined)} style={controlStyle} />
11906
+ @@ -727,9 +733,7 @@ export function AgentsConfig({
11907
  className={savedOk ? "is-success" : undefined}
11908
  >
11909
  {savedOk && (
 
22676
  const filePath = onOpenFile ? resolveLocalFileHref(href, cwd) : null;
22677
  const openFile = onOpenFile;
22678
  if (!filePath || !openFile) {
22679
+ diff --git a/components/McpAddServer.tsx b/components/McpAddServer.tsx
22680
+ index 2753f9e..4b272ca 100644
22681
+ --- a/components/McpAddServer.tsx
22682
+ +++ b/components/McpAddServer.tsx
22683
+ @@ -40,6 +40,7 @@ import {
22684
+ type McpAddSubmitBlock,
22685
+ } from "./mcp-add-helpers";
22686
+ import type { McpActionFailure, McpActionRequest } from "./mcp-config-helpers";
22687
+ +import { NativeSelect } from "./ui/select";
22688
+
22689
+ type Translate = ReturnType<typeof useI18n>["t"];
22690
+ export type McpAddActionRequest = Extract<McpActionRequest, { action: "add" }>;
22691
+ @@ -255,7 +256,7 @@ export function McpAddServer({
22692
+
22693
+ {parsed.ok && parsed.servers.length > 1 && (
22694
+ <ConfigField label={t("mcp.add.serverLabel")}>
22695
+ - <select
22696
+ + <NativeSelect fill
22697
+ className="mcp-add-input"
22698
+ aria-label={t("mcp.add.serverLabel")}
22699
+ value={String(parsed.servers.indexOf(server ?? parsed.servers[0]))}
22700
+ @@ -264,7 +265,7 @@ export function McpAddServer({
22701
+ {parsed.servers.map((item, index) => (
22702
+ <option key={`${index}\0${item.name}`} value={index}>{revealHiddenCharacters(item.name)}</option>
22703
+ ))}
22704
+ - </select>
22705
+ + </NativeSelect>
22706
+ <span className="mcp-config-line is-dim">{t("mcp.add.serverCount", { count: parsed.servers.length })}</span>
22707
+ </ConfigField>
22708
+ )}
22709
+ @@ -518,10 +519,10 @@ function McpAddFieldInput({
22710
+ onChange={(event) => setReference(event.target.value)}
22711
+ />
22712
+ ) : field.kind === "select" ? (
22713
+ - <select className="mcp-add-input" aria-label={label} value={value} {...invalid} onChange={(event) => setValue(event.target.value)}>
22714
+ + <NativeSelect fill className="mcp-add-input" aria-label={label} value={value} {...invalid} onChange={(event) => setValue(event.target.value)}>
22715
+ {!field.options?.includes(value) && <option value="">{t("mcp.add.field.choose")}</option>}
22716
+ {(field.options ?? []).map((option) => <option key={option} value={option}>{revealHiddenCharacters(option)}</option>)}
22717
+ - </select>
22718
+ + </NativeSelect>
22719
+ ) : (
22720
+ <input
22721
+ className="mcp-add-input"
22722
+ diff --git a/components/McpConfig.test.mjs b/components/McpConfig.test.mjs
22723
+ index 440f63e..529aea4 100644
22724
+ --- a/components/McpConfig.test.mjs
22725
+ +++ b/components/McpConfig.test.mjs
22726
+ @@ -774,7 +774,7 @@ test("the exposure dropdown waits like a switch, and points at why it cannot be
22727
+ // While any change is on its way it waits; the one for this server says it is saving.
22728
+ const saving = validated({ view: { busy: "exposure:global\0docs" } });
22729
+ assert.match(exposureSelect(saving).tag, /disabled=""/);
22730
+ - assert.match(decode(saving), /<span class="mcp-exposure-choice"><select[^>]*>[\s\S]*?<\/select><span role="status" class="mcp-config-line is-dim">Saving…<\/span><\/span>/);
22731
+ + assert.match(decode(saving), /<span class="mcp-exposure-choice"><span class="ui-select"[^>]*><select[^>]*>[\s\S]*?<\/select><svg[^>]*class="ui-select-chevron"[\s\S]*?<\/svg><\/span><span role="status" class="mcp-config-line is-dim">Saving…<\/span><\/span>/);
22732
+ assert.match(exposureSelect(validated({ view: { busy: "switch:global\0pw" } })).tag, /disabled=""/);
22733
+ assert.doesNotMatch(exposureSelect(validated()).tag, /disabled/);
22734
+ // Where no change may be written, it points at the note that says why.
22735
  diff --git a/components/McpConfig.tsx b/components/McpConfig.tsx
22736
+ index 97e86ee..b5af79b 100644
22737
  --- a/components/McpConfig.tsx
22738
  +++ b/components/McpConfig.tsx
22739
  @@ -1,5 +1,6 @@
 
22743
  import { useCallback, useEffect, useId, useRef, useState, type Ref } from "react";
22744
  import type { McpExposure } from "@earendil-works/pi-coding-agent";
22745
  import type {
22746
+ @@ -170,6 +171,7 @@ import {
22747
+ type McpSignInRun,
22748
+ } from "./mcp-sign-in-helpers";
22749
+ import { projectTrustReloadKey } from "./settings-ui-helpers";
22750
+ +import { NativeSelect } from "./ui/select";
22751
+
22752
+ type Translate = ReturnType<typeof useI18n>["t"];
22753
+
22754
+ @@ -795,6 +797,7 @@ export function McpConfig({
22755
  setAddFailure(null);
22756
  }}
22757
  onAddSubmit={(request) => void submitAdd(request)}
 
22759
  onTest={(server) => void testServer(server)}
22760
  onSignIn={(server) => void startSignIn(server)}
22761
  onSignOut={(server) => void signOut(server)}
22762
+ @@ -850,6 +853,7 @@ export function McpConfigView({
22763
  onAddOpen = () => {},
22764
  onAddDraftChange = () => {},
22765
  onAddSubmit = () => {},
 
22767
  onSelect,
22768
  onRefresh,
22769
  onCodemodeChange,
22770
+ @@ -896,6 +900,8 @@ export function McpConfigView({
22771
  onAddOpen?: () => void;
22772
  onAddDraftChange?: (draft: McpAddDraft) => void;
22773
  onAddSubmit?: (request: McpAddActionRequest) => void;
 
22776
  onSelect: (key: string) => void;
22777
  onRefresh: () => void;
22778
  onCodemodeChange: (preference: McpCodemodePreference) => void;
22779
+ @@ -1099,6 +1105,15 @@ export function McpConfigView({
22780
  </>
22781
  ) : null}
22782
  </ConfigSidebarList>
 
22792
  {data && (
22793
  <ConfigListAction ref={addActionRef} active={adding} onClick={onAddOpen}>
22794
  {t("mcp.add.action")}
22795
+ @@ -1620,7 +1635,7 @@ function McpServerDetail({
22796
+ {server.exposure && (
22797
+ <>
22798
+ <span className="mcp-exposure-choice">
22799
+ - <select
22800
+ + <NativeSelect
22801
+ className="mcp-add-input mcp-exposure-select"
22802
+ aria-label={t("mcp.exposure.label", { name })}
22803
+ aria-describedby={[exposureId, block ? noteId : null].filter(Boolean).join(" ")}
22804
+ @@ -1638,7 +1653,7 @@ function McpServerDetail({
22805
+ : t(MCP_EXPOSURE_SHORT_KEYS[exposure])}
22806
+ </option>
22807
+ ))}
22808
+ - </select>
22809
+ + </NativeSelect>
22810
+ {busy === `exposure:${key}` && <span role="status" className="mcp-config-line is-dim">{t("i18n.saving")}</span>}
22811
+ </span>
22812
+ <span id={exposureId} className="mcp-config-line">{t(MCP_EXPOSURE_KEYS[server.exposure])}</span>
22813
  diff --git a/components/MermaidBlock.test.mjs b/components/MermaidBlock.test.mjs
22814
  index 45088d3..cf1df98 100644
22815
  --- a/components/MermaidBlock.test.mjs
 
25198
  + assert.equal(providerMatchesSearch(" ", oauth.id, oauth.name), true);
25199
  +});
25200
  diff --git a/components/ModelsConfig.tsx b/components/ModelsConfig.tsx
25201
+ index 3cfaabb..1a5c7e6 100644
25202
  --- a/components/ModelsConfig.tsx
25203
  +++ b/components/ModelsConfig.tsx
25204
  @@ -14,9 +14,11 @@ import {
 
25213
  trackAddedModels,
25214
  collectModelRenames,
25215
  renameProviderEntry,
25216
+ @@ -56,6 +58,9 @@ import { providerBadgeLabel } from "./enabled-models-helpers";
25217
  import { OAuthPastePanel } from "./OAuthPastePanel";
25218
  import { ProviderIcon } from "./ProviderIcon";
25219
  import { ProviderUsageSummary } from "./ProviderUsageSummary";
25220
  +import { CarbonIcon } from "./CarbonIcon";
25221
  +import * as Carbon from "./carbon/icons";
25222
+ +import { NativeSelect } from "./ui/select";
25223
 
25224
  // ── Types ─────────────────────────────────────────────────────────────────────
25225
 
25226
+ @@ -93,6 +98,7 @@ interface ModelEntry {
25227
  id: string;
25228
  name?: string;
25229
  api?: string;
 
25231
  reasoning?: boolean;
25232
  thinkingLevelMap?: Record<string, string | null>;
25233
  input?: string[];
25234
+ @@ -186,7 +192,7 @@ const inputStyle = {
25235
  padding: "6px 9px",
25236
  background: "var(--bg-panel)",
25237
  border: "1px solid var(--border)",
 
25240
  color: "var(--text)",
25241
  fontSize: 12,
25242
  outline: "none",
25243
+ @@ -260,17 +266,9 @@ function SecretTextInput({
25244
  }}
25245
  >
25246
  {visible ? (
 
25260
  )}
25261
  </button>
25262
  </div>
25263
+ @@ -284,11 +282,11 @@ function NumInput({ value, onChange, placeholder }: { value: string; onChange: (
25264
+ function Select({ value, onChange, options, required }: { value: string; onChange: (v: string) => void; options: readonly string[]; required?: boolean }) {
25265
+ const { t } = useI18n();
25266
+ return (
25267
+ - <select value={value} onChange={(e) => onChange(e.target.value)}
25268
+ + <NativeSelect pad={9} border={1} fill value={value} onChange={(e) => onChange(e.target.value)}
25269
+ style={{ ...inputStyle, color: value ? "var(--text)" : "var(--text-dim)" }}>
25270
+ - {!required && <option value="">— {t("i18n.default")} / none —</option>}
25271
+ + {!required && <option value="">— {t("i18n.default")} / none —</option>}
25272
+ {options.map((o) => <option key={o} value={o}>{o}</option>)}
25273
+ - </select>
25274
+ + </NativeSelect>
25275
+ );
25276
+ }
25277
+
25278
+ @@ -415,7 +413,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25279
  <TextInput value={editingName} onChange={onEditingNameChange} placeholder="provider-name" mono />
25280
  {editingName !== name && editingName.trim() && (
25281
  <button onClick={() => onRename(editingName.trim())}
 
25284
  {t("i18n.rename")}
25285
  </button>
25286
  )}
25287
+ @@ -424,17 +422,11 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25288
  <Field label="Base URL">
25289
  <TextInput value={provider.baseUrl ?? ""} onChange={(v) => set("baseUrl", v || undefined)}
25290
  placeholder="https://api.example.com/v1" mono />
 
25303
  </Field>
25304
 
25305
  <Field label="API">
25306
+ @@ -446,9 +438,6 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25307
  headers={provider.headers}
25308
  onChange={(headers) => set("headers", headers)}
25309
  />
 
25313
  </Field>
25314
 
25315
  <div style={{ borderTop: "1px solid var(--border)", paddingTop: 14, display: "flex", flexDirection: "column", gap: 10 }}>
25316
+ @@ -457,7 +446,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25317
  onClick={handleDiscoverModels}
25318
  disabled={discoveryState.phase === "loading"}
25319
  style={{
 
25322
  background: "var(--bg-panel)", color: discoveryState.phase === "loading" ? "var(--text-dim)" : "var(--text-muted)",
25323
  cursor: discoveryState.phase === "loading" ? "not-allowed" : "pointer", fontSize: 11,
25324
  }}
25325
+ @@ -467,7 +456,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25326
  )}
25327
 
25328
  {discoveryState.phase === "error" && (
 
25331
  {discoveryState.message}
25332
  </div>
25333
  )}
25334
+ @@ -482,7 +471,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25335
  style={{ ...inputStyle, width: "100%", minWidth: 0 }}
25336
  />
25337
 
 
25340
  <label
25341
  style={{
25342
  minHeight: 32, padding: "5px 9px", display: "flex", alignItems: "center", gap: 8,
25343
+ @@ -541,7 +530,7 @@ function ProviderDetail({ name, editingName, provider, onChange, onEditingNameCh
25344
  <button
25345
  onClick={addSelectedModels}
25346
  disabled={selectedCount === 0}
 
25349
  >
25350
  {selectedCount
25351
  ? t("models.discoveryAddSelectedCount", { count: selectedCount })
25352
+ @@ -563,10 +552,11 @@ type ThinkingLevel = typeof THINKING_LEVELS[number];
25353
 
25354
  const LEVEL_COLORS: Record<ThinkingLevel, string> = {
25355
  off: "var(--text-dim)",
 
25365
  xhigh: "#fb923c",
25366
  max: "#ef4444",
25367
  };
25368
+ @@ -629,7 +619,7 @@ function ThinkingLevelMapEditor({
25369
  alignItems: "center",
25370
  gap: 8,
25371
  padding: "5px 4px",
 
25374
  background: "transparent",
25375
  border: "1px solid transparent",
25376
  }}
25377
+ @@ -646,7 +636,7 @@ function ThinkingLevelMapEditor({
25378
  </span>
25379
  </div>
25380
 
 
25383
  <button
25384
  onClick={() => setLevel(level, "omit")}
25385
  style={{ ...btnBase, ...(state === "omit" ? btnActive : {}) }}
25386
+ @@ -661,7 +651,7 @@ function ThinkingLevelMapEditor({
25387
  </button>
25388
  </div>
25389
 
 
25392
  <button
25393
  onClick={() => setLevel(level, strVal || level)}
25394
  style={{ ...btnBase, ...(state === "string" ? btnActive : {}), borderRight: "1px solid var(--border)", flexShrink: 0 }}
25395
+ @@ -749,7 +739,7 @@ function HeaderListEditor({ headers, onChange }: {
25396
  padding: "6px 9px",
25397
  background: "none",
25398
  border: "1px solid rgba(239,68,68,0.3)",
 
25401
  color: "#ef4444",
25402
  cursor: "pointer",
25403
  fontSize: 11,
25404
+ @@ -770,7 +760,7 @@ function HeaderListEditor({ headers, onChange }: {
25405
  ...current,
25406
  { id: nextRowIdRef.current++, name: "", value: "" },
25407
  ])}
 
25410
  + Add header
25411
  </button>
25412
  </div>
25413
+ @@ -842,6 +832,8 @@ function ModelDetail({
25414
  const [costDraft, setCostDraft] = useState<ModelCostDraft>(() => modelCostToDraft(model.cost));
25415
  const costDraftRef = useRef(costDraft);
25416
  const [advancedOpen, setAdvancedOpen] = useState(false);
 
25419
  const catalogRequestIdRef = useRef(0);
25420
  const catalogUndoRef = useRef<ModelEntry | null>(null);
25421
  const costTemplateRef = useRef(model.cost);
25422
+ @@ -885,17 +877,21 @@ function ModelDetail({
25423
  })();
25424
 
25425
  useEffect(() => {
 
25443
  setTestState({ phase: "testing" });
25444
  try {
25445
  const res = await fetch("/api/models-config/test", {
25446
+ @@ -910,6 +906,7 @@ function ModelDetail({
25447
  status?: number;
25448
  responseText?: string;
25449
  };
 
25451
  if (!res.ok || !d.ok) {
25452
  setTestState({
25453
  phase: "error",
25454
+ @@ -926,6 +923,7 @@ function ModelDetail({
25455
  responseText: d.responseText,
25456
  });
25457
  } catch (e) {
 
25459
  setTestState({ phase: "error", message: e instanceof Error ? e.message : String(e) });
25460
  }
25461
  }, [model, provider, providerName, testState.phase]);
25462
+ @@ -937,7 +935,7 @@ function ModelDetail({
25463
  setCatalogState({ phase: "loading" });
25464
  try {
25465
  const params = new URLSearchParams({ q: query, provider: providerName, limit: "50" });
 
25468
  const res = await fetch(`/api/models-config/catalog?${params}`);
25469
  const data = await res.json() as { recommendation?: ModelCatalogRecommendation; error?: string };
25470
  if (requestId !== catalogRequestIdRef.current) return;
25471
+ @@ -960,7 +958,7 @@ function ModelDetail({
25472
  if (requestId !== catalogRequestIdRef.current) return;
25473
  setCatalogState({ phase: "error", message: error instanceof Error ? error.message : String(error) });
25474
  }
 
25477
 
25478
  const undoCatalogFill = () => {
25479
  const previous = catalogUndoRef.current;
25480
+ @@ -1023,6 +1021,7 @@ function ModelDetail({
25481
  }
25482
  compatibilityOverrideCount += remainingCompatKeys.size;
25483
  const advancedSummaryParts = [
 
25485
  model.api ? `API: ${model.api}` : null,
25486
  Object.keys(model.headers ?? {}).length
25487
  ? t("models.headersSummary", { count: Object.keys(model.headers ?? {}).length })
25488
+ @@ -1053,9 +1052,9 @@ function ModelDetail({
25489
  height: 28,
25490
  padding: "0 8px",
25491
  border: `1px solid ${testState.phase === "error" ? "#fecaca" : testState.phase === "success" ? "#bbf7d0" : "var(--border)"}`,
 
25498
  fontSize: 11,
25499
  display: "inline-flex",
25500
  alignItems: "center",
25501
+ @@ -1077,9 +1076,7 @@ function ModelDetail({
25502
  className={testState.phase === "success" ? "is-success" : undefined}
25503
  >
25504
  {testState.phase === "success" && (
 
25509
  )}
25510
  {testState.phase === "testing" ? t("i18n.checking") : testState.phase === "success" ? t("common.ok") : t("i18n.test")}
25511
  </ConfigButton>
25512
+ @@ -1098,7 +1095,7 @@ function ModelDetail({
25513
  onClick={() => void handleCatalogFill()}
25514
  disabled={!model.id.trim() || catalogState.phase === "loading"}
25515
  style={{
 
25518
  background: "var(--bg-panel)",
25519
  color: !model.id.trim() || catalogState.phase === "loading" ? "var(--text-dim)" : "var(--text-muted)",
25520
  cursor: !model.id.trim() || catalogState.phase === "loading" ? "not-allowed" : "pointer",
25521
+ @@ -1177,7 +1174,7 @@ function ModelDetail({
25522
  </div>
25523
 
25524
  <div style={{ marginTop: 16 }}>
 
25527
  {t("models.costPerMillion")}
25528
  </div>
25529
  {costEditing ? (
25530
+ @@ -1229,24 +1226,16 @@ function ModelDetail({
25531
  {advancedSummary}
25532
  </span>
25533
  </span>
 
25558
  <Field label={t("models.apiOverride")}>
25559
  <Select value={model.api ?? ""} onChange={(v) => set("api", v || undefined)} options={API_OPTIONS} />
25560
  </Field>
25561
+ @@ -1274,6 +1263,18 @@ function ModelDetail({
25562
  checked={effectiveCompat(provider, model)["supportsDeveloperRole"] !== false}
25563
  onChange={(v) => onChange(setCompatBool(model, "supportsDeveloperRole", v))}
25564
  />
 
25577
  <div style={{ marginTop: 4 }}>
25578
  <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 10, marginBottom: 8 }}>
25579
  <SectionTitle>{t("models.thinkingLevelMap")}</SectionTitle>
25580
+ @@ -1495,7 +1496,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25581
  <button
25582
  key={option.id}
25583
  onClick={() => submitSelection(loginState.token, option.id)}
 
25586
  >
25587
  {option.label}
25588
  </button>
25589
+ @@ -1530,7 +1531,7 @@ function OAuthDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25590
  <p style={{ margin: 0, fontSize: 12, color: "var(--text-muted)", lineHeight: 1.5 }}>
25591
  Open the verification page and enter this code:
25592
  </p>
 
25595
  {loginState.userCode}
25596
  </div>
25597
  <p style={{ margin: 0, fontSize: 11, color: "var(--text-dim)", lineHeight: 1.5 }}>
25598
+ @@ -1670,7 +1671,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25599
  style={{
25600
  padding: "6px 12px",
25601
  background: savedOk ? "#16a34a" : apiKey.trim() ? "var(--accent)" : "var(--bg-panel)",
 
25604
  color: savedOk ? "#fff" : apiKey.trim() ? "var(--accent-contrast)" : "var(--text-dim)",
25605
  cursor: (saving || !apiKey.trim() || savedOk) ? "not-allowed" : "pointer",
25606
  fontSize: 12, fontWeight: 600, flexShrink: 0,
25607
+ @@ -1678,9 +1679,7 @@ function ApiKeyDetail({ provider, onRefresh, enabledModels, contextSettings }: {
25608
  }}
25609
  >
25610
  {savedOk && (
 
25615
  )}
25616
  {savedOk ? t("i18n.saved") : saving ? t("i18n.saving") : t("i18n.save")}
25617
  </button>
25618
+ @@ -1718,8 +1717,8 @@ function AddProviderPicker({
25619
 
25620
  const q = search.trim().toLowerCase();
25621
 
 
25626
  const showCustom = !q || "custom".includes(q) || "openai-compatible".includes(q) || "anthropic-compatible".includes(q);
25627
 
25628
  const totalCount = availableOAuth.length + availableApiKey.length + (showCustom ? 1 : 0);
25629
+ @@ -1729,7 +1728,7 @@ function AddProviderPicker({
25630
  padding: "10px 12px",
25631
  background: "var(--bg-panel)",
25632
  border: "1px solid var(--border)",
 
25635
  boxSizing: "border-box",
25636
  cursor: "pointer",
25637
  minWidth: 0,
25638
+ @@ -1751,12 +1750,10 @@ function AddProviderPicker({
25639
  onClose();
25640
  }}
25641
  >
 
25650
  <input
25651
  ref={inputRef}
25652
  value={search}
25653
+ @@ -1773,7 +1770,7 @@ function AddProviderPicker({
25654
  ) : (
25655
  <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(240px, 100%), 1fr))", gap: 8 }}>
25656
  {showCustom && (
 
25659
  )}
25660
  {showCustom && (
25661
  <button
25662
+ @@ -1786,16 +1783,14 @@ function AddProviderPicker({
25663
  <div style={{ fontSize: 12, fontWeight: 600, color: "var(--text)", lineHeight: 1.3, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>OpenAI / Anthropic compatible</div>
25664
  <div style={{ fontSize: 10, color: "var(--text-dim)", marginTop: 2 }}>{t("i18n.customEndpoint")}</div>
25665
  </div>
 
25679
  )}
25680
  {availableOAuth.map((p) => (
25681
  <button key={p.id} onClick={() => { onSelectOAuth(p.id); onClose(); }}
25682
+ @@ -1812,7 +1807,7 @@ function AddProviderPicker({
25683
  ))}
25684
 
25685
  {availableApiKey.length > 0 && (
 
25688
  )}
25689
  {availableApiKey.map((p) => (
25690
  <button key={p.id} onClick={() => { onSelectApiKey(p.id); onClose(); }}
25691
+ @@ -2216,13 +2211,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25692
  onClick={() => setSelection({ type: "provider", name: pName })}
25693
  active={isProviderSelected}
25694
  >
 
25703
  <ConfigSidebarText className="is-grow">
25704
  {pName}
25705
  </ConfigSidebarText>
25706
+ @@ -2243,7 +2232,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25707
  {m.id || t("i18n.newModel")}
25708
  </ConfigSidebarText>
25709
  {m.reasoning && (
 
25712
  )}
25713
  </ConfigSidebarItem>
25714
  );
25715
+ @@ -2276,11 +2265,11 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25716
  </ConfigSplitView>
25717
 
25718
  {/* Footer */}
 
25726
  {!embedded && <ConfigButton onClick={onClose}>{t("i18n.cancel")}</ConfigButton>}
25727
  <ConfigButton
25728
  variant="primary"
25729
+ @@ -2289,10 +2278,7 @@ export function ModelsConfig({ onClose, embedded = false, cwd = null }: {
25730
  className={savedOk ? "is-success" : undefined}
25731
  >
25732
  {savedOk && (
 
32721
  + else if (also) (also as { current: T | null }).current = node;
32722
  + }, [also]);
32723
  +}
32724
+ diff --git a/components/ui/select.test.mjs b/components/ui/select.test.mjs
32725
+ new file mode 100644
32726
+ index 0000000..668e391
32727
+ --- /dev/null
32728
+ +++ b/components/ui/select.test.mjs
32729
+ @@ -0,0 +1,28 @@
32730
+ +import assert from "node:assert/strict";
32731
+ +import { readdirSync, readFileSync } from "node:fs";
32732
+ +import test from "node:test";
32733
+ +import { createJiti } from "jiti";
32734
+ +
32735
+ +const jiti = createJiti(import.meta.url, { jsx: { runtime: "automatic" }, tsconfigPaths: true });
32736
+ +const React = await jiti.import("react");
32737
+ +const { renderToStaticMarkup } = await jiti.import("react-dom/server");
32738
+ +const { NativeSelect } = await jiti.import("./select.tsx");
32739
+ +
32740
+ +test("every select in the app is a NativeSelect, with our chevron instead of the engine's arrow", () => {
32741
+ + const dir = new URL("..", import.meta.url);
32742
+ + const raw = readdirSync(dir).filter((name) => name.endsWith(".tsx"))
32743
+ + .filter((name) => /<select\b/.test(readFileSync(new URL(name, dir), "utf8")));
32744
+ + assert.deepEqual(raw, [], "use components/ui/select.tsx's NativeSelect");
32745
+ +});
32746
+ +
32747
+ +test("the chevron mirrors the text's padding and the select keeps its own attributes", () => {
32748
+ + const html = renderToStaticMarkup(React.createElement(NativeSelect, { pad: 9, fill: true, "aria-label": "Thinking", defaultValue: "b", style: { padding: "0 9px" } },
32749
+ + React.createElement("option", { value: "a" }, "A"), React.createElement("option", { value: "b" }, "B")));
32750
+ + assert.match(html, /^<span class="ui-select is-fill" style="--select-pad:9px">/);
32751
+ + assert.match(html, /<select aria-label="Thinking" style="padding:0 9px;padding-left:9px;padding-right:calc\(9px \+ 13.5px\)">/);
32752
+ + assert.match(html, /<option value="b" selected="">B<\/option><\/select><svg[^>]*class="[^"]*ui-select-chevron/);
32753
+ + const css = readFileSync(new URL("../../app/ui.css", import.meta.url), "utf8");
32754
+ + assert.match(css, /\.ui-select > select \{[^}]*appearance: none;/);
32755
+ + assert.match(css, /\.ui-select-chevron \{[^}]*right: calc\(var\(--select-pad, 10px\) \+ var\(--select-border, 0px\) - 1\.5px\);/);
32756
+ + assert.match(renderToStaticMarkup(React.createElement(NativeSelect, { pad: 9, border: 1 })), /style="--select-pad:9px;--select-border:1px"/);
32757
+ +});
32758
+ diff --git a/components/ui/select.tsx b/components/ui/select.tsx
32759
+ new file mode 100644
32760
+ index 0000000..d1a6266
32761
+ --- /dev/null
32762
+ +++ b/components/ui/select.tsx
32763
+ @@ -0,0 +1,37 @@
32764
+ +import type { CSSProperties, SelectHTMLAttributes } from "react";
32765
+ +import { CarbonIcon } from "../CarbonIcon";
32766
+ +import * as Carbon from "../carbon/icons";
32767
+ +
32768
+ +/**
32769
+ + * A native select with Carbon's chevron instead of the engine's arrow: each engine places
32770
+ + * its own (Chromium 3px from the edge whatever the padding, after 4.5px of its own padding
32771
+ + * before the text; Firefox and Safari elsewhere again). The chevron's ink sits as far from
32772
+ + * the right edge as the text's from the left (`pad`, the text's padding, inside `border`),
32773
+ + * and 6.25px after the text, as in the menu triggers. Styled in app/ui.css (.ui-select).
32774
+ + */
32775
+ +export function NativeSelect({
32776
+ + pad = 10,
32777
+ + border = 0,
32778
+ + fill = false,
32779
+ + wrapperStyle,
32780
+ + style,
32781
+ + children,
32782
+ + ...props
32783
+ +}: SelectHTMLAttributes<HTMLSelectElement> & {
32784
+ + /** The text's padding from the left edge, in px; the chevron mirrors it. */
32785
+ + pad?: number;
32786
+ + /** The select's border width, in px, which the text sits inside too. */
32787
+ + border?: number;
32788
+ + /** Take the row's whole width, as a field does. */
32789
+ + fill?: boolean;
32790
+ + wrapperStyle?: CSSProperties;
32791
+ +}) {
32792
+ + return (
32793
+ + <span className={`ui-select${fill ? " is-fill" : ""}`} style={{ "--select-pad": `${pad}px`, ...(border ? { "--select-border": `${border}px` } : {}), ...wrapperStyle } as CSSProperties}>
32794
+ + <select {...props} style={{ ...style, paddingLeft: pad, paddingRight: `calc(${pad}px + 13.5px)` }}>
32795
+ + {children}
32796
+ + </select>
32797
+ + <CarbonIcon icon={Carbon.ChevronDown} size={12} className="ui-select-chevron" />
32798
+ + </span>
32799
+ + );
32800
+ +}
32801
  diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
32802
  new file mode 100644
32803
  index 0000000..019f84d