@agent-native/toolkit 0.16.0 → 0.16.1

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.
Files changed (33) hide show
  1. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  2. package/dist/composer/ComposerPlusMenu.js +29 -19
  3. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  4. package/dist/composer/PromptComposer.d.ts +4 -0
  5. package/dist/composer/PromptComposer.d.ts.map +1 -1
  6. package/dist/composer/PromptComposer.js +2 -2
  7. package/dist/composer/PromptComposer.js.map +1 -1
  8. package/dist/composer/TiptapComposer.d.ts +12 -1
  9. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  10. package/dist/composer/TiptapComposer.js +249 -162
  11. package/dist/composer/TiptapComposer.js.map +1 -1
  12. package/dist/composer/TiptapComposer.spec.js +33 -1
  13. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  14. package/dist/composer/index.d.ts +1 -0
  15. package/dist/composer/index.d.ts.map +1 -1
  16. package/dist/composer/index.js +1 -0
  17. package/dist/composer/index.js.map +1 -1
  18. package/dist/composer/model-selection.d.ts +12 -0
  19. package/dist/composer/model-selection.d.ts.map +1 -0
  20. package/dist/composer/model-selection.js +31 -0
  21. package/dist/composer/model-selection.js.map +1 -0
  22. package/dist/composer/model-selection.spec.d.ts +2 -0
  23. package/dist/composer/model-selection.spec.d.ts.map +1 -0
  24. package/dist/composer/model-selection.spec.js +38 -0
  25. package/dist/composer/model-selection.spec.js.map +1 -0
  26. package/package.json +1 -1
  27. package/src/composer/ComposerPlusMenu.tsx +122 -65
  28. package/src/composer/PromptComposer.tsx +8 -0
  29. package/src/composer/TiptapComposer.spec.ts +46 -0
  30. package/src/composer/TiptapComposer.tsx +678 -412
  31. package/src/composer/index.ts +6 -0
  32. package/src/composer/model-selection.spec.ts +45 -0
  33. package/src/composer/model-selection.ts +51 -0
@@ -14,6 +14,7 @@ import {
14
14
  IconKey,
15
15
  IconPencil,
16
16
  IconPlugConnected,
17
+ IconHelpCircle,
17
18
  } from "@tabler/icons-react";
18
19
  import Placeholder from "@tiptap/extension-placeholder";
19
20
  import type { EditorView } from "@tiptap/pm/view";
@@ -28,7 +29,12 @@ import React, {
28
29
  useMemo,
29
30
  } from "react";
30
31
 
31
- import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
32
+ import {
33
+ Popover,
34
+ PopoverAnchor,
35
+ PopoverContent,
36
+ PopoverTrigger,
37
+ } from "../ui/popover.js";
32
38
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
33
39
  import { ComposerPlusMenu } from "./ComposerPlusMenu.js";
34
40
  import { getComposerDraftKey } from "./draft-key.js";
@@ -36,6 +42,11 @@ import { FileReference } from "./extensions/FileReference.js";
36
42
  import { MentionReference } from "./extensions/MentionReference.js";
37
43
  import { SkillReference } from "./extensions/SkillReference.js";
38
44
  import { MentionPopover, type MentionPopoverRef } from "./MentionPopover.js";
45
+ import {
46
+ isClaudeCodeAgentId,
47
+ isLunaModel,
48
+ resolvePreferredAgentModel,
49
+ } from "./model-selection.js";
39
50
  import {
40
51
  createPastedAttachmentFile,
41
52
  readClipboardPaste,
@@ -742,6 +753,8 @@ export interface TiptapComposerProps {
742
753
  selectedEffort?: ReasoningEffort;
743
754
  /** Show the legacy provider-level Auto model option (default: true). */
744
755
  showAutoModelOption?: boolean;
756
+ /** Controlled open state for hosts that resize around the model picker. */
757
+ modelSelectorOpen?: boolean;
745
758
  /** Available models grouped by provider */
746
759
  availableModels?: Array<{
747
760
  engine: string;
@@ -761,8 +774,12 @@ export interface TiptapComposerProps {
761
774
  availableAgents?: ComposerAgentOption[];
762
775
  /** Selected agent runtime identifier. Defaults to the built-in agent. */
763
776
  selectedAgent?: string;
777
+ /** Mark the selected runtime as the hosted tools-only harness mode. */
778
+ hostedHarness?: boolean;
764
779
  /** Callback when the user picks an agent runtime. */
765
780
  onAgentChange?: (agent: string) => void;
781
+ /** Called when the shared model picker opens or closes. */
782
+ onModelSelectorOpenChange?: (open: boolean) => void;
766
783
  /**
767
784
  * Disable Builder/provider status polling for hosts that supply provider
768
785
  * state through another channel, such as Electron IPC.
@@ -937,7 +954,7 @@ function ModeSelector({
937
954
  </p>
938
955
  </div>
939
956
  {mode === "build" && (
940
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
957
+ <IconCheck className="size-4 shrink-0 text-primary" />
941
958
  )}
942
959
  </button>
943
960
  <button
@@ -969,7 +986,7 @@ function ModeSelector({
969
986
  </p>
970
987
  </div>
971
988
  {mode === "plan" && !planModeDisabled && (
972
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
989
+ <IconCheck className="size-4 shrink-0 text-primary" />
973
990
  )}
974
991
  </button>
975
992
  </PopoverContent>
@@ -980,17 +997,61 @@ function ModeSelector({
980
997
  const FRIENDLY_MODEL_NAMES: Record<string, string> = {
981
998
  auto: "Default model",
982
999
  "codex-cli": "Codex",
1000
+ "claude-cli": "Claude Code",
1001
+ "pi-cli": "Pi",
1002
+ "opencode-cli": "OpenCode",
983
1003
  "claude-fable-5": "Fable 5",
984
1004
  "kimi-k2-5": "Kimi K2.5",
985
1005
  "deepseek-v3-1": "DeepSeek v3.1",
986
1006
  "z-ai/glm-5.2": "GLM 5.2",
987
1007
  };
988
1008
 
1009
+ const LOCAL_RUNTIME_ENGINES = new Set([
1010
+ "codex-cli",
1011
+ "claude-cli",
1012
+ "pi-cli",
1013
+ "opencode-cli",
1014
+ ]);
1015
+
1016
+ function isOpenAiModelId(model: string): boolean {
1017
+ const normalizedModel = model.toLowerCase();
1018
+ return (
1019
+ normalizedModel.startsWith("gpt-") ||
1020
+ normalizedModel.startsWith("openai/gpt-")
1021
+ );
1022
+ }
1023
+
1024
+ export function isOpenAiModelProviderGroup(group: {
1025
+ engine: string;
1026
+ label: string;
1027
+ models: string[];
1028
+ }): boolean {
1029
+ const engine = group.engine.toLowerCase();
1030
+ const label = group.label.toLowerCase();
1031
+ if (engine === "codex-cli") {
1032
+ return group.models.some(isOpenAiModelId);
1033
+ }
1034
+ return (
1035
+ engine.includes("openai") ||
1036
+ label.includes("openai") ||
1037
+ group.models.some(isOpenAiModelId)
1038
+ );
1039
+ }
1040
+
1041
+ const HARNESS_AGENTS_DOCS_URL =
1042
+ "https://www.agent-native.com/docs/harness-agents";
1043
+
989
1044
  export const MODEL_SELECTOR_POPOVER_STYLE = {
990
1045
  fontSize: 13,
991
1046
  maxHeight: "min(500px, var(--radix-popover-content-available-height, 500px))",
992
1047
  } satisfies React.CSSProperties;
993
1048
 
1049
+ const MODEL_SELECTOR_POPOVER_CLASS =
1050
+ "box-border w-64 rounded-lg border border-border bg-popover p-1 text-popover-foreground shadow-lg outline-none";
1051
+ const PICKER_HELP_BUTTON_CLASS =
1052
+ "flex size-3 shrink-0 items-center justify-center rounded text-muted-foreground/60 hover:text-foreground";
1053
+ const PICKER_HELP_ICON_CLASS = "size-2";
1054
+
994
1055
  export function shouldShowModelSelectorSkeleton(
995
1056
  isLoading: boolean,
996
1057
  engineCount: number,
@@ -1201,17 +1262,7 @@ function ModelCostTier({ model }: { model: string }) {
1201
1262
  : tier === 2
1202
1263
  ? t("agentChat.composer.costMedium", { defaultValue: "Medium cost" })
1203
1264
  : t("agentChat.composer.costHigher", { defaultValue: "Higher cost" });
1204
- return (
1205
- <>
1206
- <span
1207
- aria-hidden="true"
1208
- className="shrink-0 text-[11px] tabular-nums text-muted-foreground/60"
1209
- >
1210
- {"$".repeat(tier)}
1211
- </span>
1212
- <span className="sr-only">{costLabel}</span>
1213
- </>
1214
- );
1265
+ return <span className="sr-only">{costLabel}</span>;
1215
1266
  }
1216
1267
 
1217
1268
  /**
@@ -1246,11 +1297,14 @@ function ModelSelector({
1246
1297
  engines,
1247
1298
  agents,
1248
1299
  selectedAgent,
1300
+ hostedHarness = false,
1249
1301
  showAutoModelOption = true,
1250
1302
  modelListLoading = false,
1303
+ open: controlledOpen,
1251
1304
  onChange,
1252
1305
  onEffortChange,
1253
1306
  onAgentChange,
1307
+ onModelSelectorOpenChange,
1254
1308
  providerConnectStatusEnabled = true,
1255
1309
  onConnectProvider,
1256
1310
  onConnectLocalRuntime,
@@ -1260,6 +1314,7 @@ function ModelSelector({
1260
1314
  effort?: ReasoningEffort;
1261
1315
  agents?: ComposerAgentOption[];
1262
1316
  selectedAgent?: string;
1317
+ hostedHarness?: boolean;
1263
1318
  engines: Array<{
1264
1319
  engine: string;
1265
1320
  label: string;
@@ -1276,15 +1331,21 @@ function ModelSelector({
1276
1331
  providerConnectStatusEnabled?: boolean;
1277
1332
  onConnectProvider?: () => void;
1278
1333
  onConnectLocalRuntime?: (engine: string) => void;
1334
+ onModelSelectorOpenChange?: (open: boolean) => void;
1279
1335
  imageModel?: ComposerImageModelMenu;
1336
+ open?: boolean;
1280
1337
  }) {
1281
1338
  const adapters = useComposerRuntimeAdapters();
1282
1339
  const t = adapters.translate!;
1283
1340
  const reasoning = adapters.models?.reasoning;
1284
1341
  const defaultEffort = reasoning?.defaultEffort ?? DEFAULT_REASONING_EFFORT;
1285
- const [open, setOpen] = useState(false);
1342
+ const [internalOpen, setInternalOpen] = useState(false);
1343
+ const open = controlledOpen ?? internalOpen;
1344
+ const isClaudeCodeAgent = isClaudeCodeAgentId(selectedAgent);
1286
1345
  const autoModelGroup = showAutoModelOption
1287
- ? engines.find((group) => group.models.includes("auto"))
1346
+ ? isClaudeCodeAgent
1347
+ ? undefined
1348
+ : engines.find((group) => group.models.includes("auto"))
1288
1349
  : undefined;
1289
1350
  const providerGroups = useMemo(
1290
1351
  () =>
@@ -1296,6 +1357,60 @@ function ModelSelector({
1296
1357
  .filter((group) => group.models.length > 0),
1297
1358
  [engines],
1298
1359
  );
1360
+ const isCodexAgent =
1361
+ selectedAgent === "codex" || selectedAgent === "codex-cli";
1362
+ const modelProviderGroups = useMemo(() => {
1363
+ const hasCodexLocalGroup = providerGroups.some(
1364
+ (group) => group.engine === "codex-cli",
1365
+ );
1366
+ const groups = providerGroups.flatMap((group) => {
1367
+ if (group.engine === "codex-cli") {
1368
+ return isCodexAgent && isOpenAiModelProviderGroup(group) ? [group] : [];
1369
+ }
1370
+ if (
1371
+ isCodexAgent &&
1372
+ hasCodexLocalGroup &&
1373
+ group.engine === "ai-sdk:openai"
1374
+ ) {
1375
+ return [];
1376
+ }
1377
+ if (LOCAL_RUNTIME_ENGINES.has(group.engine)) return [];
1378
+ if (!isCodexAgent) return [group];
1379
+ if (!isOpenAiModelProviderGroup(group)) return [];
1380
+
1381
+ const isExplicitOpenAiProvider =
1382
+ group.engine.toLowerCase().includes("openai") ||
1383
+ group.label.toLowerCase().includes("openai");
1384
+ const models = isExplicitOpenAiProvider
1385
+ ? group.models
1386
+ : group.models.filter(isOpenAiModelId);
1387
+ return models.length > 0 ? [{ ...group, models }] : [];
1388
+ });
1389
+ if (!isClaudeCodeAgent) return groups;
1390
+ return groups
1391
+ .map((group) => ({
1392
+ ...group,
1393
+ models: group.models.filter((candidate) => !isLunaModel(candidate)),
1394
+ }))
1395
+ .filter((group) => group.models.length > 0);
1396
+ }, [isClaudeCodeAgent, isCodexAgent, providerGroups]);
1397
+ const preferredAgentModel = useMemo(
1398
+ () => resolvePreferredAgentModel(selectedAgent, providerGroups),
1399
+ [providerGroups, selectedAgent],
1400
+ );
1401
+ useEffect(() => {
1402
+ if (!isClaudeCodeAgent || !isLunaModel(model) || !preferredAgentModel) {
1403
+ return;
1404
+ }
1405
+ if (
1406
+ preferredAgentModel.model === model &&
1407
+ preferredAgentModel.engine ===
1408
+ engines.find((group) => group.models.includes(model))?.engine
1409
+ ) {
1410
+ return;
1411
+ }
1412
+ onChange(preferredAgentModel.model, preferredAgentModel.engine);
1413
+ }, [engines, isClaudeCodeAgent, model, onChange, preferredAgentModel]);
1299
1414
  const effortOptions =
1300
1415
  reasoning?.getOptionsForModel?.(model) ??
1301
1416
  getComposerReasoningEffortOptions(model);
@@ -1315,61 +1430,31 @@ function ModelSelector({
1315
1430
  (agent) => agent.id === (selectedAgent ?? "default"),
1316
1431
  );
1317
1432
  const selectedAgentLabel = selectedAgentOption?.label ?? "Default";
1433
+ const selectedModelLabel = friendlyModelName(model, t).replace(/^GPT-/, "");
1318
1434
 
1319
- // Collapse non-selected families by default. The family containing the
1320
- // currently-selected model stays expanded so the user sees their pick at
1321
- // a glance; clicking another family's header expands it inline.
1322
- const selectedGroupKey = useMemo(() => {
1323
- const found = providerGroups.find((g) => g.models.includes(model));
1324
- return found ? `${found.engine}:${found.label}` : null;
1325
- }, [model, providerGroups]);
1435
+ const [detailSection, setDetailSection] = useState<
1436
+ "agent" | "model" | "effort" | null
1437
+ >(null);
1438
+ const resolvedSection = detailSection ?? "model";
1326
1439
 
1327
- const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
1328
- () => new Set(selectedGroupKey ? [selectedGroupKey] : []),
1440
+ const setPickerOpen = useCallback(
1441
+ (nextOpen: boolean) => {
1442
+ if (controlledOpen === undefined) setInternalOpen(nextOpen);
1443
+ if (nextOpen) setDetailSection(null);
1444
+ onModelSelectorOpenChange?.(nextOpen);
1445
+ },
1446
+ [controlledOpen, onModelSelectorOpenChange],
1329
1447
  );
1330
1448
 
1331
- // Reset expansion when the popover re-opens so the picker always lands
1332
- // on the "selected family expanded, others collapsed" view.
1333
- useEffect(() => {
1334
- if (open) {
1335
- setExpandedGroups(new Set(selectedGroupKey ? [selectedGroupKey] : []));
1336
- }
1337
- }, [open, selectedGroupKey]);
1338
-
1339
- const toggleGroup = useCallback((key: string) => {
1340
- setExpandedGroups((prev) => {
1341
- const next = new Set(prev);
1342
- if (next.has(key)) next.delete(key);
1343
- else next.add(key);
1344
- return next;
1345
- });
1346
- }, []);
1347
-
1348
- // The effort list is collapsed by default — it's a secondary
1349
- // control most users don't touch, so it stays tucked behind a header that
1350
- // reveals the current effort at a glance. Reset to collapsed on each open.
1351
- const [effortExpanded, setEffortExpanded] = useState(false);
1352
- useEffect(() => {
1353
- if (open) setEffortExpanded(false);
1354
- }, [open]);
1355
-
1356
- // The optional image-model section follows the same collapsed-by-default
1357
- // pattern; the current selection shows next to the header.
1358
- const [imageExpanded, setImageExpanded] = useState(false);
1359
- useEffect(() => {
1360
- if (open) setImageExpanded(false);
1361
- }, [open]);
1362
- const imageModelLabel =
1363
- imageModel?.options.find((option) => option.value === imageModel.value)
1364
- ?.label ?? imageModel?.value;
1449
+ const visibleProviderGroups = modelProviderGroups;
1365
1450
  const showModelListSkeleton = shouldShowModelSelectorSkeleton(
1366
1451
  modelListLoading,
1367
1452
  engines.length,
1368
1453
  );
1369
1454
 
1370
- // When Builder.io isn't connected, surface a one-click connect path
1371
- // it unlocks every model family (Claude, OpenAI, Gemini) without the
1372
- // user having to paste individual API keys.
1455
+ // Keep setup actions beside model choices while any visible provider still
1456
+ // needs configuration. The model rows remain visible so the user can see
1457
+ // what becomes available after connecting or adding a key.
1373
1458
  const builderFlow = adapters.builder!.useConnectFlow!({
1374
1459
  enabled: providerConnectStatusEnabled,
1375
1460
  trackingSource: "composer_builder_cta",
@@ -1380,34 +1465,39 @@ function ModelSelector({
1380
1465
  const hasConnectedSubscription = providerGroups.some(
1381
1466
  (group) => group.configured && group.isSubscription,
1382
1467
  );
1383
- const showBuilderCta =
1384
- (builderFlow.hasFetchedStatus ||
1385
- (!providerConnectStatusEnabled && !!onConnectProvider)) &&
1386
- !builderFlow.configured &&
1387
- !builderFlow.envManaged &&
1388
- !hasConfiguredBuilderModels &&
1389
- !hasConnectedSubscription;
1390
- const onlyConnectPathAvailable = shouldShowOnlyConnectPath(
1391
- showBuilderCta,
1392
- providerGroups,
1468
+ const hasUnconfiguredVisibleModels = modelProviderGroups.some(
1469
+ (group) => !group.configured,
1393
1470
  );
1471
+ const showBuilderAction =
1472
+ hasUnconfiguredVisibleModels &&
1473
+ (Boolean(onConnectProvider) ||
1474
+ (providerConnectStatusEnabled &&
1475
+ !builderFlow.configured &&
1476
+ !builderFlow.envManaged &&
1477
+ !hasConfiguredBuilderModels &&
1478
+ !hasConnectedSubscription));
1479
+ const showAddKeysAction = hasUnconfiguredVisibleModels;
1480
+ const showProviderActions = showBuilderAction || showAddKeysAction;
1481
+ const onlyConnectPathAvailable =
1482
+ shouldShowOnlyConnectPath(showBuilderAction, providerGroups) &&
1483
+ modelProviderGroups.length === 0;
1394
1484
  const openLlmSettings = useCallback(() => {
1395
1485
  try {
1396
1486
  window.location.hash = "llm";
1397
1487
  } catch {}
1398
1488
  window.dispatchEvent(new CustomEvent("agent-panel:open-settings"));
1399
- setOpen(false);
1400
- }, []);
1489
+ setPickerOpen(false);
1490
+ }, [setPickerOpen]);
1401
1491
  const connectLocalRuntime = useCallback(
1402
1492
  (engine: string) => {
1403
1493
  onConnectLocalRuntime?.(engine);
1404
- setOpen(false);
1494
+ setPickerOpen(false);
1405
1495
  },
1406
- [onConnectLocalRuntime],
1496
+ [onConnectLocalRuntime, setPickerOpen],
1407
1497
  );
1408
1498
 
1409
1499
  return (
1410
- <Popover open={open} onOpenChange={setOpen}>
1500
+ <Popover open={open} onOpenChange={setPickerOpen}>
1411
1501
  <PopoverTrigger asChild>
1412
1502
  <button
1413
1503
  type="button"
@@ -1416,8 +1506,8 @@ function ModelSelector({
1416
1506
  defaultValue: "Model",
1417
1507
  })}: ${friendlyModelName(model, t)}${
1418
1508
  effortOptions.length > 0
1419
- ? `. ${t("agentChat.composer.reasoning", {
1420
- defaultValue: "Reasoning",
1509
+ ? `. ${t("agentChat.composer.effort", {
1510
+ defaultValue: "Effort",
1421
1511
  })}: ${effortLabel(selectedEffort)}`
1422
1512
  : ""
1423
1513
  }${
@@ -1446,357 +1536,527 @@ function ModelSelector({
1446
1536
  sideOffset={6}
1447
1537
  collisionPadding={8}
1448
1538
  data-agent-native-composer-popover="true"
1449
- className="z-[260] box-border w-72 overflow-y-auto rounded-lg border-border p-0 py-1 shadow-lg"
1539
+ className={`z-[260] overflow-visible ${MODEL_SELECTOR_POPOVER_CLASS}`}
1450
1540
  style={MODEL_SELECTOR_POPOVER_STYLE}
1451
1541
  >
1452
- {agents && agents.length > 0 && (
1453
- <>
1454
- <div className="px-3 pb-1 pt-2 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
1455
- Agents
1456
- </div>
1457
- {agents.map((agent) => {
1458
- const isSelected = agent.id === (selectedAgent ?? "default");
1459
- const isConfigured = agent.configured !== false;
1460
- const canConnect =
1461
- !isConfigured && Boolean(onConnectLocalRuntime);
1462
- const statusLabel =
1463
- agent.statusLabel ??
1464
- (!isConfigured ? "Unavailable" : undefined);
1465
- return (
1466
- <div
1467
- key={agent.id}
1468
- className="flex items-center hover:bg-accent/30"
1542
+ <Popover open={detailSection !== null} onOpenChange={() => undefined}>
1543
+ <PopoverAnchor asChild>
1544
+ <div
1545
+ className="flex flex-col"
1546
+ role="tablist"
1547
+ aria-label={t("agentChat.composer.pickerSections", {
1548
+ defaultValue: "Picker sections",
1549
+ })}
1550
+ >
1551
+ {agents && agents.length > 0 && (
1552
+ <button
1553
+ type="button"
1554
+ role="tab"
1555
+ aria-selected={resolvedSection === "agent"}
1556
+ onClick={() => setDetailSection("agent")}
1557
+ onMouseEnter={() => setDetailSection("agent")}
1558
+ className={`flex w-full min-w-0 items-center gap-1 rounded-md px-2 py-2 text-start transition-colors ${
1559
+ resolvedSection === "agent"
1560
+ ? "bg-accent text-foreground"
1561
+ : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1562
+ }`}
1469
1563
  >
1470
- <button
1471
- type="button"
1472
- disabled={!isConfigured || !onAgentChange}
1473
- aria-current={isSelected ? "true" : undefined}
1474
- onClick={() => {
1475
- if (!isConfigured) return;
1476
- onAgentChange?.(agent.id);
1477
- setOpen(false);
1478
- }}
1479
- className={`flex min-w-0 flex-1 items-center gap-3 px-3 py-1.5 text-start ${
1480
- isConfigured
1481
- ? "hover:bg-accent/50"
1482
- : "cursor-default opacity-50"
1483
- }`}
1484
- >
1485
- <span className="min-w-0 flex-1">
1486
- <span className="block truncate text-[13px] text-foreground">
1487
- {agent.label}
1488
- </span>
1489
- {agent.description && (
1490
- <span className="block truncate text-[11px] text-muted-foreground">
1491
- {agent.description}
1492
- </span>
1493
- )}
1564
+ <span className="flex min-w-0 items-center">
1565
+ <span className="shrink-0 text-[12px] font-medium">
1566
+ Agent
1494
1567
  </span>
1495
- {isSelected && isConfigured && (
1496
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-primary" />
1497
- )}
1498
- </button>
1499
- {!isConfigured && statusLabel && (
1500
- <button
1501
- type="button"
1502
- disabled={!canConnect}
1503
- className="ms-auto max-w-[6rem] shrink-0 truncate px-3 py-1.5 text-end text-[10px] text-muted-foreground/70 hover:text-foreground disabled:cursor-default"
1504
- onClick={() => {
1505
- if (canConnect) connectLocalRuntime(agent.id);
1506
- }}
1507
- >
1508
- {statusLabel}
1509
- </button>
1510
- )}
1511
- </div>
1512
- );
1513
- })}
1514
- <div className="my-1 border-t border-border" />
1515
- </>
1516
- )}
1517
- {showBuilderCta && (
1518
- <>
1519
- <button
1520
- type="button"
1521
- onClick={() => {
1522
- if (onConnectProvider) {
1523
- onConnectProvider();
1524
- } else {
1525
- builderFlow.start();
1526
- }
1527
- }}
1528
- disabled={!onConnectProvider && builderFlow.connecting}
1529
- className="flex w-full items-start gap-2 px-3 py-2 text-start hover:bg-accent/50 disabled:opacity-60"
1530
- >
1531
- <IconPlugConnected className="h-4 w-4 shrink-0 mt-0.5 text-blue-500" />
1532
- <span className="flex-1 min-w-0">
1533
- <span className="block text-[12px] font-medium text-foreground">
1534
- {!onConnectProvider && builderFlow.connecting
1535
- ? t("agentPanel.connectingBuilder", {
1536
- defaultValue: "Connecting Builder.io…",
1537
- })
1538
- : t("agentPanel.connectBuilderIo", {
1539
- defaultValue: "Connect Builder.io",
1540
- })}
1541
- </span>
1542
- <span className="block text-[11px] text-muted-foreground">
1543
- {t("agentPanel.builderModelCredits", {
1544
- defaultValue: "Free credits for Claude, OpenAI & Gemini",
1545
- })}
1546
- </span>
1547
- </span>
1548
- </button>
1549
- {!onConnectProvider && builderFlow.error && (
1550
- <p
1551
- role="alert"
1552
- className="px-3 pb-2 ps-9 text-[11px] text-destructive"
1553
- >
1554
- {builderFlow.error}
1555
- </p>
1556
- )}
1557
- <button
1558
- type="button"
1559
- onClick={openLlmSettings}
1560
- className="flex w-full items-start gap-2 px-3 py-2 text-start hover:bg-accent/50"
1561
- >
1562
- <IconKey className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
1563
- <span className="min-w-0 flex-1">
1564
- <span className="block text-[12px] font-medium text-foreground">
1565
- {t("agentPanel.addOwnKeys", {
1566
- defaultValue: "Add your own keys",
1567
- })}
1568
- </span>
1569
- <span className="block text-[11px] text-muted-foreground">
1570
- {t("agentPanel.configureProviderKeys", {
1571
- defaultValue: "Choose a cloud, gateway, or local provider",
1572
- })}
1573
- </span>
1574
- </span>
1575
- </button>
1576
- {!onlyConnectPathAvailable && (
1577
- <div className="my-1 border-t border-border" />
1578
- )}
1579
- </>
1580
- )}
1581
- {imageModel && imageModel.options.length > 0 && (
1582
- <>
1583
- <div className="flex items-center hover:bg-accent/30">
1568
+ <Tooltip>
1569
+ <TooltipTrigger asChild>
1570
+ <span
1571
+ role="img"
1572
+ aria-label={t("agentChat.composer.harnessAgentHelp", {
1573
+ defaultValue: "Learn about harness agents",
1574
+ })}
1575
+ onClick={(event) => event.stopPropagation()}
1576
+ className={`ms-1.5 ${PICKER_HELP_BUTTON_CLASS}`}
1577
+ >
1578
+ <IconHelpCircle
1579
+ aria-hidden="true"
1580
+ className={PICKER_HELP_ICON_CLASS}
1581
+ strokeWidth={1.8}
1582
+ />
1583
+ </span>
1584
+ </TooltipTrigger>
1585
+ <TooltipContent side="right" className="z-[400] max-w-xs">
1586
+ <span className="block">
1587
+ {hostedHarness
1588
+ ? t("agentChat.composer.hostedHarnessDescription", {
1589
+ defaultValue:
1590
+ "Hosted mode uses app tools only. For full coding with a repository and shell, use Agent Native Desktop.",
1591
+ })
1592
+ : t("agentChat.composer.harnessAgentDescription", {
1593
+ defaultValue:
1594
+ "Harnesses run their own coding loop and local tools.",
1595
+ })}
1596
+ </span>
1597
+ <a
1598
+ href={HARNESS_AGENTS_DOCS_URL}
1599
+ target="_blank"
1600
+ rel="noreferrer"
1601
+ className="mt-1 block underline underline-offset-2 hover:no-underline"
1602
+ >
1603
+ {t("agentChat.composer.learnMoreHarnessAgents", {
1604
+ defaultValue: "Learn more about harness agents",
1605
+ })}
1606
+ </a>
1607
+ </TooltipContent>
1608
+ </Tooltip>
1609
+ </span>
1610
+ <span className="ms-auto min-w-0 max-w-[6rem] truncate text-end text-[11px] text-muted-foreground/80">
1611
+ {selectedAgentLabel}
1612
+ </span>
1613
+ <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1614
+ </button>
1615
+ )}
1584
1616
  <button
1585
1617
  type="button"
1586
- aria-expanded={imageExpanded}
1587
- onClick={() => setImageExpanded((prev) => !prev)}
1588
- className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
1618
+ role="tab"
1619
+ aria-selected={resolvedSection === "model"}
1620
+ onClick={() => setDetailSection("model")}
1621
+ onMouseEnter={() => setDetailSection("model")}
1622
+ className={`flex w-full min-w-0 items-center gap-1 rounded-md px-2 py-2 text-start transition-colors ${
1623
+ resolvedSection === "model"
1624
+ ? "bg-accent text-foreground"
1625
+ : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1626
+ }`}
1589
1627
  >
1590
- {imageExpanded ? (
1591
- <IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
1592
- ) : (
1593
- <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1594
- )}
1595
- <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1596
- {imageModel.label ??
1597
- t("agentChat.composer.imageModel", {
1598
- defaultValue: "Image model",
1599
- })}
1628
+ <span className="shrink-0 text-[12px] font-medium">Model</span>
1629
+ <span className="ms-auto min-w-0 max-w-[6rem] truncate text-end text-[11px] text-muted-foreground/80">
1630
+ {selectedModelLabel}
1600
1631
  </span>
1601
- {!imageExpanded && imageModelLabel && (
1602
- <span className="text-[11px] text-muted-foreground/80 truncate">
1603
- {imageModelLabel}
1604
- </span>
1605
- )}
1632
+ <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1606
1633
  </button>
1607
- </div>
1608
- {imageExpanded &&
1609
- imageModel.options.map((option) => (
1634
+ {effortOptions.length > 0 && (
1610
1635
  <button
1611
- key={option.value}
1612
1636
  type="button"
1613
- onClick={() => {
1614
- imageModel.onChange(option.value);
1615
- setImageExpanded(false);
1616
- }}
1617
- className="flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start hover:bg-accent/50"
1637
+ role="tab"
1638
+ aria-selected={resolvedSection === "effort"}
1639
+ onClick={() => setDetailSection("effort")}
1640
+ onMouseEnter={() => setDetailSection("effort")}
1641
+ className={`flex w-full min-w-0 items-center gap-1 rounded-md px-2 py-2 text-start transition-colors ${
1642
+ resolvedSection === "effort"
1643
+ ? "bg-accent text-foreground"
1644
+ : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1645
+ }`}
1618
1646
  >
1619
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1620
- {option.label}
1647
+ <span className="shrink-0 text-[12px] font-medium">
1648
+ {t("agentChat.composer.effort", {
1649
+ defaultValue: "Effort",
1650
+ })}
1651
+ </span>
1652
+ <span className="ms-auto min-w-0 max-w-[6rem] truncate text-end text-[11px] text-muted-foreground/80">
1653
+ {effortLabel(selectedEffort)}
1621
1654
  </span>
1622
- {option.value === imageModel.value && (
1623
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1624
- )}
1655
+ <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1625
1656
  </button>
1626
- ))}
1627
- <div className="my-1 border-t border-border" />
1628
- </>
1629
- )}
1630
- {showModelListSkeleton && <ModelSelectorSkeleton />}
1631
- {autoModelGroup && !onlyConnectPathAvailable && (
1632
- <button
1633
- type="button"
1634
- onClick={() => {
1635
- onChange("auto", autoModelGroup.engine);
1636
- setOpen(false);
1637
- }}
1638
- className="flex w-full items-center gap-3 px-3 py-1.5 text-start hover:bg-accent/50"
1657
+ )}
1658
+ </div>
1659
+ </PopoverAnchor>
1660
+ <PopoverContent
1661
+ side="right"
1662
+ align="start"
1663
+ sideOffset={4}
1664
+ alignOffset={-8}
1665
+ collisionPadding={8}
1666
+ className={`z-[320] overflow-visible ${MODEL_SELECTOR_POPOVER_CLASS}`}
1667
+ style={MODEL_SELECTOR_POPOVER_STYLE}
1639
1668
  >
1640
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1641
- {t("agentChat.composer.auto", { defaultValue: "Auto" })}
1642
- </span>
1643
- {model === "auto" && (
1644
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1645
- )}
1646
- </button>
1647
- )}
1648
- {autoModelGroup &&
1649
- providerGroups.length > 0 &&
1650
- !onlyConnectPathAvailable && (
1651
- <div className="my-1 border-t border-border" />
1652
- )}
1653
- {!onlyConnectPathAvailable &&
1654
- providerGroups.map((group) => {
1655
- const models = latestModelsOnly(group.models);
1656
- const groupKey = `${group.engine}:${group.label}`;
1657
- const isExpanded = expandedGroups.has(groupKey);
1658
- const ChevronIcon = isExpanded ? IconChevronDown : IconChevronRight;
1659
- const isLocalRuntime =
1660
- group.engine === "codex-cli" || group.engine === "claude-cli";
1661
- const canConnectLocalRuntime =
1662
- isLocalRuntime && Boolean(onConnectLocalRuntime);
1663
- const statusLabel =
1664
- group.statusLabel ??
1665
- (!group.configured
1666
- ? canConnectLocalRuntime
1667
- ? t("agentChat.auth.logIn", { defaultValue: "Sign in" })
1668
- : t("agentChat.composer.needsApiKey", {
1669
- defaultValue: "needs API key",
1670
- })
1671
- : undefined);
1672
- return (
1673
- <div key={groupKey}>
1674
- <div className="flex items-center hover:bg-accent/30">
1675
- <button
1676
- type="button"
1677
- aria-expanded={isExpanded}
1678
- onClick={() => toggleGroup(groupKey)}
1679
- className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
1680
- >
1681
- <ChevronIcon className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1682
- <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1683
- {group.label}
1684
- </span>
1685
- {!isExpanded && groupKey === selectedGroupKey && (
1686
- <span className="text-[11px] text-muted-foreground/80 truncate">
1687
- {friendlyModelName(model, t)}
1688
- </span>
1669
+ <div className="max-h-[min(500px,var(--radix-popover-content-available-height,500px))] overflow-y-auto">
1670
+ <div
1671
+ className="min-h-0 min-w-0"
1672
+ role="tabpanel"
1673
+ aria-label={resolvedSection}
1674
+ >
1675
+ {resolvedSection === "agent" && agents && (
1676
+ <div className="flex flex-col">
1677
+ {agents.map((agent) => {
1678
+ const isSelected =
1679
+ agent.id === (selectedAgent ?? "default");
1680
+ const isConfigured = agent.configured !== false;
1681
+ const canConnect =
1682
+ !isConfigured && Boolean(onConnectLocalRuntime);
1683
+ const statusLabel =
1684
+ agent.statusLabel ??
1685
+ (!isConfigured ? "Unavailable" : undefined);
1686
+ return (
1687
+ <div
1688
+ key={agent.id}
1689
+ className="group flex items-center rounded-md hover:bg-accent/30"
1690
+ >
1691
+ <button
1692
+ type="button"
1693
+ disabled={!isConfigured || !onAgentChange}
1694
+ aria-current={isSelected ? "true" : undefined}
1695
+ onClick={() => {
1696
+ if (!isConfigured) return;
1697
+ onAgentChange?.(agent.id);
1698
+ setPickerOpen(false);
1699
+ }}
1700
+ className={`flex min-w-0 flex-1 items-center gap-0.5 px-2 py-2 text-start ${
1701
+ isConfigured
1702
+ ? "hover:bg-accent/50"
1703
+ : "cursor-default opacity-50"
1704
+ }`}
1705
+ >
1706
+ <span
1707
+ className={`min-w-0 truncate text-[12px] ${
1708
+ isSelected
1709
+ ? "text-foreground"
1710
+ : "text-muted-foreground"
1711
+ }`}
1712
+ >
1713
+ {agent.label}
1714
+ </span>
1715
+ {agent.description && (
1716
+ <Tooltip>
1717
+ <TooltipTrigger asChild>
1718
+ <span
1719
+ role="img"
1720
+ aria-label={`${agent.label} details`}
1721
+ onClick={(event) => event.stopPropagation()}
1722
+ className={PICKER_HELP_BUTTON_CLASS}
1723
+ >
1724
+ <IconHelpCircle
1725
+ aria-hidden="true"
1726
+ className={PICKER_HELP_ICON_CLASS}
1727
+ strokeWidth={1.8}
1728
+ />
1729
+ </span>
1730
+ </TooltipTrigger>
1731
+ <TooltipContent
1732
+ side="left"
1733
+ className="z-[400] max-w-xs"
1734
+ >
1735
+ {agent.description}
1736
+ </TooltipContent>
1737
+ </Tooltip>
1738
+ )}
1739
+ <span
1740
+ className="min-w-0 flex-1"
1741
+ aria-hidden="true"
1742
+ />
1743
+ {isSelected && isConfigured && (
1744
+ <IconCheck className="size-4 shrink-0 text-primary" />
1745
+ )}
1746
+ </button>
1747
+ {!isConfigured && statusLabel && (
1748
+ <button
1749
+ type="button"
1750
+ disabled={!canConnect}
1751
+ className="ms-auto max-w-[6rem] shrink-0 truncate px-2 py-2 text-end text-[10px] text-muted-foreground/70 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 hover:text-foreground focus-visible:opacity-100 disabled:cursor-default"
1752
+ onClick={() => {
1753
+ if (canConnect) connectLocalRuntime(agent.id);
1754
+ }}
1755
+ >
1756
+ {statusLabel}
1757
+ </button>
1758
+ )}
1759
+ </div>
1760
+ );
1761
+ })}
1762
+ </div>
1763
+ )}
1764
+ {resolvedSection === "model" && (
1765
+ <>
1766
+ {showProviderActions && (
1767
+ <>
1768
+ {showBuilderAction && (
1769
+ <>
1770
+ <button
1771
+ type="button"
1772
+ onClick={() => {
1773
+ if (onConnectProvider) {
1774
+ onConnectProvider();
1775
+ } else {
1776
+ builderFlow.start();
1777
+ }
1778
+ }}
1779
+ disabled={
1780
+ !onConnectProvider && builderFlow.connecting
1781
+ }
1782
+ className="flex w-full items-start gap-2 rounded-md px-2 py-2 text-start hover:bg-accent/50 disabled:opacity-60"
1783
+ >
1784
+ <IconPlugConnected className="mt-0.5 h-4 w-4 shrink-0 text-primary" />
1785
+ <span className="min-w-0 flex-1">
1786
+ <span className="block text-[12px] font-medium text-foreground">
1787
+ {!onConnectProvider && builderFlow.connecting
1788
+ ? t("agentPanel.connectingBuilder", {
1789
+ defaultValue: "Connecting Builder.io…",
1790
+ })
1791
+ : t("agentPanel.connectBuilderIo", {
1792
+ defaultValue: "Connect Builder.io",
1793
+ })}
1794
+ </span>
1795
+ <span className="block text-[11px] text-muted-foreground">
1796
+ {t("agentPanel.builderModelCredits", {
1797
+ defaultValue:
1798
+ "Free credits for Claude, OpenAI & Gemini",
1799
+ })}
1800
+ </span>
1801
+ </span>
1802
+ </button>
1803
+ {!onConnectProvider && builderFlow.error && (
1804
+ <p
1805
+ role="alert"
1806
+ className="px-2 pb-2 ps-8 text-[11px] text-destructive"
1807
+ >
1808
+ {builderFlow.error}
1809
+ </p>
1810
+ )}
1811
+ </>
1812
+ )}
1813
+ {showAddKeysAction && (
1814
+ <button
1815
+ type="button"
1816
+ onClick={openLlmSettings}
1817
+ className="flex w-full items-start gap-2 rounded-md px-2 py-2 text-start hover:bg-accent/50"
1818
+ >
1819
+ <IconKey className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
1820
+ <span className="min-w-0 flex-1">
1821
+ <span className="block text-[12px] font-medium text-foreground">
1822
+ {t("agentPanel.addOwnKeys", {
1823
+ defaultValue: "Add your own keys",
1824
+ })}
1825
+ </span>
1826
+ <span className="block text-[11px] text-muted-foreground">
1827
+ {t("agentPanel.configureProviderKeys", {
1828
+ defaultValue:
1829
+ "Choose a cloud, gateway, or local provider",
1830
+ })}
1831
+ </span>
1832
+ </span>
1833
+ </button>
1834
+ )}
1835
+ </>
1689
1836
  )}
1690
- </button>
1691
- {!group.configured && statusLabel && (
1692
- <button
1693
- type="button"
1694
- className="ms-auto max-w-[9rem] shrink-0 text-end text-[10px] text-muted-foreground/60 hover:text-foreground cursor-pointer pe-3 py-1.5"
1695
- onClick={() =>
1696
- canConnectLocalRuntime
1697
- ? connectLocalRuntime(group.engine)
1698
- : openLlmSettings()
1699
- }
1700
- >
1701
- {statusLabel}
1702
- </button>
1703
- )}
1704
- {group.configured && statusLabel && (
1705
- <span className="ms-auto max-w-[9rem] shrink-0 pe-3 text-end text-[10px] text-muted-foreground/70">
1706
- {statusLabel}
1707
- </span>
1708
- )}
1709
- </div>
1710
- {isExpanded &&
1711
- models.map((m) => (
1712
- <button
1713
- key={m}
1714
- type="button"
1715
- onClick={() => {
1716
- if (!group.configured) {
1717
- if (canConnectLocalRuntime) {
1718
- connectLocalRuntime(group.engine);
1719
- } else {
1720
- openLlmSettings();
1721
- }
1722
- return;
1723
- }
1724
- onChange(m, group.engine);
1725
- const nextOptions =
1726
- getReasoningEffortOptionsForModel(m);
1727
- if (
1728
- nextOptions.length > 0 &&
1729
- !nextOptions.includes(selectedEffort)
1730
- ) {
1731
- onEffortChange?.(defaultEffort);
1732
- }
1733
- setOpen(false);
1734
- }}
1735
- className={`flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start ${
1736
- group.configured
1737
- ? "hover:bg-accent/50"
1738
- : "opacity-40 cursor-default"
1739
- }`}
1740
- >
1741
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1742
- {friendlyModelName(m, t)}
1743
- </span>
1744
- <ModelCostTier model={m} />
1745
- {m === model && group.configured && (
1746
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1837
+ {imageModel && imageModel.options.length > 0 && (
1838
+ <div className="mt-2 pt-1">
1839
+ <div className="px-2 py-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
1840
+ {imageModel.label ??
1841
+ t("agentChat.composer.imageModel", {
1842
+ defaultValue: "Image model",
1843
+ })}
1844
+ </div>
1845
+ {imageModel.options.map((option) => (
1846
+ <button
1847
+ key={option.value}
1848
+ type="button"
1849
+ onClick={() => {
1850
+ imageModel.onChange(option.value);
1851
+ setPickerOpen(false);
1852
+ }}
1853
+ className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-start hover:bg-accent/50"
1854
+ >
1855
+ <span
1856
+ className={`min-w-0 flex-1 truncate text-[12px] ${
1857
+ option.value === imageModel.value
1858
+ ? "text-foreground"
1859
+ : "text-muted-foreground"
1860
+ }`}
1861
+ >
1862
+ {option.label}
1863
+ </span>
1864
+ {option.value === imageModel.value && (
1865
+ <IconCheck className="size-4 shrink-0 text-primary" />
1866
+ )}
1867
+ </button>
1868
+ ))}
1869
+ </div>
1870
+ )}
1871
+ {showModelListSkeleton && <ModelSelectorSkeleton />}
1872
+ {isCodexAgent &&
1873
+ !showModelListSkeleton &&
1874
+ !onlyConnectPathAvailable &&
1875
+ modelProviderGroups.length === 0 && (
1876
+ <button
1877
+ type="button"
1878
+ onClick={openLlmSettings}
1879
+ className="flex w-full items-center rounded-md px-2 py-2 text-start hover:bg-accent/50"
1880
+ >
1881
+ <span className="min-w-0 flex-1 truncate text-[12px] text-muted-foreground">
1882
+ {t("agentChat.composer.noOpenAiModels", {
1883
+ defaultValue: "No OpenAI models configured",
1884
+ })}
1885
+ </span>
1886
+ <span className="shrink-0 text-[11px] text-muted-foreground/70">
1887
+ Configure
1888
+ </span>
1889
+ </button>
1747
1890
  )}
1748
- </button>
1749
- ))}
1750
- </div>
1751
- );
1752
- })}
1753
- {!showModelListSkeleton &&
1754
- !onlyConnectPathAvailable &&
1755
- effortOptions.length > 0 && (
1756
- <>
1757
- <div className="my-1 border-t border-border" />
1758
- <div className="flex items-center hover:bg-accent/30">
1759
- <button
1760
- type="button"
1761
- aria-expanded={effortExpanded}
1762
- onClick={() => setEffortExpanded((prev) => !prev)}
1763
- className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
1764
- >
1765
- {effortExpanded ? (
1766
- <IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
1767
- ) : (
1768
- <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1769
- )}
1770
- <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1771
- {t("agentChat.composer.reasoning", {
1772
- defaultValue: "Reasoning",
1773
- })}
1774
- </span>
1775
- {!effortExpanded && (
1776
- <span className="text-[11px] text-muted-foreground/80 truncate">
1777
- {effortLabel(selectedEffort)}
1778
- </span>
1779
- )}
1780
- </button>
1781
- </div>
1782
- {effortExpanded &&
1783
- effortOptions.map((option) => (
1784
- <button
1785
- key={option}
1786
- type="button"
1787
- onClick={() => onEffortChange?.(option)}
1788
- className="flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start hover:bg-accent/50"
1789
- >
1790
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1791
- {effortLabel(option)}
1792
- </span>
1793
- {option === selectedEffort && (
1794
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1891
+ {autoModelGroup && !onlyConnectPathAvailable && (
1892
+ <button
1893
+ type="button"
1894
+ onClick={() => {
1895
+ onChange("auto", autoModelGroup.engine);
1896
+ setPickerOpen(false);
1897
+ }}
1898
+ className="mt-1 flex w-full items-center gap-3 rounded-md px-2 py-1.5 text-start hover:bg-accent/50"
1899
+ >
1900
+ <span
1901
+ className={`min-w-0 flex-1 truncate text-[13px] ${
1902
+ model === "auto"
1903
+ ? "text-foreground"
1904
+ : "text-muted-foreground"
1905
+ }`}
1906
+ >
1907
+ {t("agentChat.composer.auto", {
1908
+ defaultValue: "Auto",
1909
+ })}
1910
+ </span>
1911
+ {model === "auto" && (
1912
+ <IconCheck className="size-4 shrink-0 text-primary" />
1913
+ )}
1914
+ </button>
1795
1915
  )}
1796
- </button>
1797
- ))}
1798
- </>
1799
- )}
1916
+ {!onlyConnectPathAvailable &&
1917
+ visibleProviderGroups.map((group, groupIndex) => {
1918
+ const models = latestModelsOnly(group.models);
1919
+ const showProviderLabels =
1920
+ visibleProviderGroups.length > 1;
1921
+ const isLocalRuntime =
1922
+ group.engine === "codex-cli" ||
1923
+ group.engine === "claude-cli";
1924
+ const canConnectLocalRuntime =
1925
+ isLocalRuntime && Boolean(onConnectLocalRuntime);
1926
+ const statusLabel =
1927
+ group.statusLabel ??
1928
+ (!group.configured
1929
+ ? canConnectLocalRuntime
1930
+ ? t("agentChat.auth.logIn", {
1931
+ defaultValue: "Sign in",
1932
+ })
1933
+ : t("agentChat.composer.needsApiKey", {
1934
+ defaultValue: "needs API key",
1935
+ })
1936
+ : undefined);
1937
+ return (
1938
+ <div
1939
+ key={`${group.engine}:${group.label}`}
1940
+ className={
1941
+ showProviderLabels && groupIndex > 0
1942
+ ? "mt-2 pt-1"
1943
+ : ""
1944
+ }
1945
+ >
1946
+ {showProviderLabels && (
1947
+ <div className="group flex items-center px-2 py-1">
1948
+ <span className="shrink-0 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
1949
+ {group.label}
1950
+ </span>
1951
+ {!group.configured && statusLabel && (
1952
+ <button
1953
+ type="button"
1954
+ aria-label={statusLabel}
1955
+ className="ms-auto max-w-[9rem] shrink-0 cursor-pointer truncate px-0 py-1 text-end text-[10px] text-muted-foreground/60 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 hover:text-foreground focus-visible:opacity-100"
1956
+ onClick={() =>
1957
+ canConnectLocalRuntime
1958
+ ? connectLocalRuntime(group.engine)
1959
+ : openLlmSettings()
1960
+ }
1961
+ >
1962
+ {statusLabel}
1963
+ </button>
1964
+ )}
1965
+ {group.configured && statusLabel && (
1966
+ <span className="ms-auto max-w-[9rem] shrink-0 truncate py-1 text-end text-[10px] text-muted-foreground/70 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
1967
+ {statusLabel}
1968
+ </span>
1969
+ )}
1970
+ </div>
1971
+ )}
1972
+ {models.map((m) => {
1973
+ const isSelected =
1974
+ m === model && group.configured;
1975
+ return (
1976
+ <button
1977
+ key={m}
1978
+ type="button"
1979
+ onClick={() => {
1980
+ if (!group.configured) {
1981
+ if (canConnectLocalRuntime) {
1982
+ connectLocalRuntime(group.engine);
1983
+ } else {
1984
+ openLlmSettings();
1985
+ }
1986
+ return;
1987
+ }
1988
+ onChange(m, group.engine);
1989
+ const nextOptions =
1990
+ getReasoningEffortOptionsForModel(m);
1991
+ if (
1992
+ nextOptions.length > 0 &&
1993
+ !nextOptions.includes(selectedEffort)
1994
+ ) {
1995
+ onEffortChange?.(defaultEffort);
1996
+ }
1997
+ setPickerOpen(false);
1998
+ }}
1999
+ className={`group flex w-full items-center gap-3 rounded-md px-2 py-2 text-start ${
2000
+ group.configured
2001
+ ? "hover:bg-accent/50"
2002
+ : "cursor-default opacity-40"
2003
+ }`}
2004
+ >
2005
+ <span
2006
+ className={`min-w-0 flex-1 truncate text-[12px] ${
2007
+ isSelected
2008
+ ? "text-foreground"
2009
+ : "text-muted-foreground"
2010
+ }`}
2011
+ >
2012
+ {friendlyModelName(m, t)}
2013
+ </span>
2014
+ <ModelCostTier model={m} />
2015
+ {!showProviderLabels && statusLabel && (
2016
+ <span className="hidden max-w-[9rem] shrink-0 truncate text-[10px] text-muted-foreground/70 group-hover:inline">
2017
+ {statusLabel}
2018
+ </span>
2019
+ )}
2020
+ {isSelected && (
2021
+ <IconCheck className="size-4 shrink-0 text-primary" />
2022
+ )}
2023
+ </button>
2024
+ );
2025
+ })}
2026
+ </div>
2027
+ );
2028
+ })}
2029
+ </>
2030
+ )}
2031
+ {resolvedSection === "effort" && effortOptions.length > 0 && (
2032
+ <div className="flex flex-col">
2033
+ {effortOptions.map((option) => (
2034
+ <button
2035
+ key={option}
2036
+ type="button"
2037
+ onClick={() => onEffortChange?.(option)}
2038
+ className="flex w-full items-center gap-3 rounded-md px-2 py-2 text-start hover:bg-accent/50"
2039
+ >
2040
+ <span
2041
+ className={`min-w-0 flex-1 truncate text-[12px] ${
2042
+ option === selectedEffort
2043
+ ? "text-foreground"
2044
+ : "text-muted-foreground"
2045
+ }`}
2046
+ >
2047
+ {effortLabel(option)}
2048
+ </span>
2049
+ {option === selectedEffort && (
2050
+ <IconCheck className="size-4 shrink-0 text-primary" />
2051
+ )}
2052
+ </button>
2053
+ ))}
2054
+ </div>
2055
+ )}
2056
+ </div>
2057
+ </div>
2058
+ </PopoverContent>
2059
+ </Popover>
1800
2060
  </PopoverContent>
1801
2061
  </Popover>
1802
2062
  );
@@ -1867,13 +2127,16 @@ export function TiptapComposer({
1867
2127
  selectedModel,
1868
2128
  selectedEffort,
1869
2129
  showAutoModelOption = true,
2130
+ modelSelectorOpen,
1870
2131
  availableModels,
1871
2132
  modelListLoading,
1872
2133
  onModelChange,
1873
2134
  onEffortChange,
1874
2135
  availableAgents,
1875
2136
  selectedAgent,
2137
+ hostedHarness,
1876
2138
  onAgentChange,
2139
+ onModelSelectorOpenChange,
1877
2140
  providerConnectStatusEnabled,
1878
2141
  onConnectProvider,
1879
2142
  onConnectLocalRuntime,
@@ -3377,15 +3640,18 @@ export function TiptapComposer({
3377
3640
  {selectedModel && availableModels && onModelChange && (
3378
3641
  <ModelSelector
3379
3642
  model={selectedModel}
3643
+ open={modelSelectorOpen}
3380
3644
  effort={selectedEffort}
3381
3645
  engines={availableModels}
3382
3646
  agents={availableAgents}
3383
3647
  selectedAgent={selectedAgent}
3648
+ hostedHarness={hostedHarness}
3384
3649
  showAutoModelOption={showAutoModelOption}
3385
3650
  modelListLoading={modelListLoading}
3386
3651
  onChange={onModelChange}
3387
3652
  onEffortChange={onEffortChange}
3388
3653
  onAgentChange={onAgentChange}
3654
+ onModelSelectorOpenChange={onModelSelectorOpenChange}
3389
3655
  providerConnectStatusEnabled={providerConnectStatusEnabled}
3390
3656
  onConnectProvider={onConnectProvider}
3391
3657
  onConnectLocalRuntime={onConnectLocalRuntime}