@agent-native/toolkit 0.15.1 → 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 (60) hide show
  1. package/README.md +26 -0
  2. package/agent-native.eject.json +16 -0
  3. package/dist/composer/ComposerPlusMenu.d.ts.map +1 -1
  4. package/dist/composer/ComposerPlusMenu.js +29 -19
  5. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  6. package/dist/composer/PromptComposer.d.ts +11 -1
  7. package/dist/composer/PromptComposer.d.ts.map +1 -1
  8. package/dist/composer/PromptComposer.js +2 -2
  9. package/dist/composer/PromptComposer.js.map +1 -1
  10. package/dist/composer/TiptapComposer.d.ts +30 -1
  11. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  12. package/dist/composer/TiptapComposer.js +255 -145
  13. package/dist/composer/TiptapComposer.js.map +1 -1
  14. package/dist/composer/TiptapComposer.spec.js +33 -1
  15. package/dist/composer/TiptapComposer.spec.js.map +1 -1
  16. package/dist/composer/index.d.ts +1 -0
  17. package/dist/composer/index.d.ts.map +1 -1
  18. package/dist/composer/index.js +1 -0
  19. package/dist/composer/index.js.map +1 -1
  20. package/dist/composer/model-selection.d.ts +12 -0
  21. package/dist/composer/model-selection.d.ts.map +1 -0
  22. package/dist/composer/model-selection.js +31 -0
  23. package/dist/composer/model-selection.js.map +1 -0
  24. package/dist/composer/model-selection.spec.d.ts +2 -0
  25. package/dist/composer/model-selection.spec.d.ts.map +1 -0
  26. package/dist/composer/model-selection.spec.js +38 -0
  27. package/dist/composer/model-selection.spec.js.map +1 -0
  28. package/dist/data-grid/DataGrid.d.ts +92 -0
  29. package/dist/data-grid/DataGrid.d.ts.map +1 -0
  30. package/dist/data-grid/DataGrid.js +315 -0
  31. package/dist/data-grid/DataGrid.js.map +1 -0
  32. package/dist/data-grid/DataGrid.spec.d.ts +2 -0
  33. package/dist/data-grid/DataGrid.spec.d.ts.map +1 -0
  34. package/dist/data-grid/DataGrid.spec.js +134 -0
  35. package/dist/data-grid/DataGrid.spec.js.map +1 -0
  36. package/dist/data-grid/index.d.ts +2 -0
  37. package/dist/data-grid/index.d.ts.map +1 -0
  38. package/dist/data-grid/index.js +2 -0
  39. package/dist/data-grid/index.js.map +1 -0
  40. package/dist/sharing/SharePrimitives.d.ts +14 -3
  41. package/dist/sharing/SharePrimitives.d.ts.map +1 -1
  42. package/dist/sharing/SharePrimitives.js +11 -3
  43. package/dist/sharing/SharePrimitives.js.map +1 -1
  44. package/dist/sharing/index.d.ts +1 -1
  45. package/dist/sharing/index.d.ts.map +1 -1
  46. package/dist/sharing/index.js +1 -1
  47. package/dist/sharing/index.js.map +1 -1
  48. package/package.json +15 -1
  49. package/src/composer/ComposerPlusMenu.tsx +122 -65
  50. package/src/composer/PromptComposer.tsx +21 -0
  51. package/src/composer/TiptapComposer.spec.ts +46 -0
  52. package/src/composer/TiptapComposer.tsx +721 -349
  53. package/src/composer/index.ts +6 -0
  54. package/src/composer/model-selection.spec.ts +45 -0
  55. package/src/composer/model-selection.ts +51 -0
  56. package/src/data-grid/DataGrid.spec.tsx +243 -0
  57. package/src/data-grid/DataGrid.tsx +754 -0
  58. package/src/data-grid/index.ts +15 -0
  59. package/src/sharing/SharePrimitives.tsx +47 -8
  60. package/src/sharing/index.ts +4 -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,
@@ -652,6 +663,19 @@ function ComposerModeChip({
652
663
 
653
664
  type ExecMode = "build" | "plan";
654
665
 
666
+ export interface ComposerAgentOption {
667
+ /** Stable host-defined identifier for the agent runtime. */
668
+ id: string;
669
+ /** Human-readable runtime name shown in the picker. */
670
+ label: string;
671
+ /** Optional short detail shown below the runtime name. */
672
+ description?: string;
673
+ /** Whether this runtime can be selected right now. */
674
+ configured?: boolean;
675
+ /** Optional status text such as "Installed" or "Sign in". */
676
+ statusLabel?: string;
677
+ }
678
+
655
679
  export interface TiptapComposerProps {
656
680
  placeholder?: string;
657
681
  disabled?: boolean;
@@ -729,6 +753,8 @@ export interface TiptapComposerProps {
729
753
  selectedEffort?: ReasoningEffort;
730
754
  /** Show the legacy provider-level Auto model option (default: true). */
731
755
  showAutoModelOption?: boolean;
756
+ /** Controlled open state for hosts that resize around the model picker. */
757
+ modelSelectorOpen?: boolean;
732
758
  /** Available models grouped by provider */
733
759
  availableModels?: Array<{
734
760
  engine: string;
@@ -744,6 +770,16 @@ export interface TiptapComposerProps {
744
770
  onModelChange?: (model: string, engine: string) => void;
745
771
  /** Callback when user picks an effort */
746
772
  onEffortChange?: (effort: ReasoningEffort) => void;
773
+ /** Local or hosted agent runtimes shown above the model list. */
774
+ availableAgents?: ComposerAgentOption[];
775
+ /** Selected agent runtime identifier. Defaults to the built-in agent. */
776
+ selectedAgent?: string;
777
+ /** Mark the selected runtime as the hosted tools-only harness mode. */
778
+ hostedHarness?: boolean;
779
+ /** Callback when the user picks an agent runtime. */
780
+ onAgentChange?: (agent: string) => void;
781
+ /** Called when the shared model picker opens or closes. */
782
+ onModelSelectorOpenChange?: (open: boolean) => void;
747
783
  /**
748
784
  * Disable Builder/provider status polling for hosts that supply provider
749
785
  * state through another channel, such as Electron IPC.
@@ -918,7 +954,7 @@ function ModeSelector({
918
954
  </p>
919
955
  </div>
920
956
  {mode === "build" && (
921
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
957
+ <IconCheck className="size-4 shrink-0 text-primary" />
922
958
  )}
923
959
  </button>
924
960
  <button
@@ -950,7 +986,7 @@ function ModeSelector({
950
986
  </p>
951
987
  </div>
952
988
  {mode === "plan" && !planModeDisabled && (
953
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
989
+ <IconCheck className="size-4 shrink-0 text-primary" />
954
990
  )}
955
991
  </button>
956
992
  </PopoverContent>
@@ -961,17 +997,61 @@ function ModeSelector({
961
997
  const FRIENDLY_MODEL_NAMES: Record<string, string> = {
962
998
  auto: "Default model",
963
999
  "codex-cli": "Codex",
1000
+ "claude-cli": "Claude Code",
1001
+ "pi-cli": "Pi",
1002
+ "opencode-cli": "OpenCode",
964
1003
  "claude-fable-5": "Fable 5",
965
1004
  "kimi-k2-5": "Kimi K2.5",
966
1005
  "deepseek-v3-1": "DeepSeek v3.1",
967
1006
  "z-ai/glm-5.2": "GLM 5.2",
968
1007
  };
969
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
+
970
1044
  export const MODEL_SELECTOR_POPOVER_STYLE = {
971
1045
  fontSize: 13,
972
1046
  maxHeight: "min(500px, var(--radix-popover-content-available-height, 500px))",
973
1047
  } satisfies React.CSSProperties;
974
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
+
975
1055
  export function shouldShowModelSelectorSkeleton(
976
1056
  isLoading: boolean,
977
1057
  engineCount: number,
@@ -1182,17 +1262,7 @@ function ModelCostTier({ model }: { model: string }) {
1182
1262
  : tier === 2
1183
1263
  ? t("agentChat.composer.costMedium", { defaultValue: "Medium cost" })
1184
1264
  : t("agentChat.composer.costHigher", { defaultValue: "Higher cost" });
1185
- return (
1186
- <>
1187
- <span
1188
- aria-hidden="true"
1189
- className="shrink-0 text-[11px] tabular-nums text-muted-foreground/60"
1190
- >
1191
- {"$".repeat(tier)}
1192
- </span>
1193
- <span className="sr-only">{costLabel}</span>
1194
- </>
1195
- );
1265
+ return <span className="sr-only">{costLabel}</span>;
1196
1266
  }
1197
1267
 
1198
1268
  /**
@@ -1225,10 +1295,16 @@ function ModelSelector({
1225
1295
  model,
1226
1296
  effort,
1227
1297
  engines,
1298
+ agents,
1299
+ selectedAgent,
1300
+ hostedHarness = false,
1228
1301
  showAutoModelOption = true,
1229
1302
  modelListLoading = false,
1303
+ open: controlledOpen,
1230
1304
  onChange,
1231
1305
  onEffortChange,
1306
+ onAgentChange,
1307
+ onModelSelectorOpenChange,
1232
1308
  providerConnectStatusEnabled = true,
1233
1309
  onConnectProvider,
1234
1310
  onConnectLocalRuntime,
@@ -1236,6 +1312,9 @@ function ModelSelector({
1236
1312
  }: {
1237
1313
  model: string;
1238
1314
  effort?: ReasoningEffort;
1315
+ agents?: ComposerAgentOption[];
1316
+ selectedAgent?: string;
1317
+ hostedHarness?: boolean;
1239
1318
  engines: Array<{
1240
1319
  engine: string;
1241
1320
  label: string;
@@ -1248,18 +1327,25 @@ function ModelSelector({
1248
1327
  modelListLoading?: boolean;
1249
1328
  onChange: (model: string, engine: string) => void;
1250
1329
  onEffortChange?: (effort: ReasoningEffort) => void;
1330
+ onAgentChange?: (agent: string) => void;
1251
1331
  providerConnectStatusEnabled?: boolean;
1252
1332
  onConnectProvider?: () => void;
1253
1333
  onConnectLocalRuntime?: (engine: string) => void;
1334
+ onModelSelectorOpenChange?: (open: boolean) => void;
1254
1335
  imageModel?: ComposerImageModelMenu;
1336
+ open?: boolean;
1255
1337
  }) {
1256
1338
  const adapters = useComposerRuntimeAdapters();
1257
1339
  const t = adapters.translate!;
1258
1340
  const reasoning = adapters.models?.reasoning;
1259
1341
  const defaultEffort = reasoning?.defaultEffort ?? DEFAULT_REASONING_EFFORT;
1260
- const [open, setOpen] = useState(false);
1342
+ const [internalOpen, setInternalOpen] = useState(false);
1343
+ const open = controlledOpen ?? internalOpen;
1344
+ const isClaudeCodeAgent = isClaudeCodeAgentId(selectedAgent);
1261
1345
  const autoModelGroup = showAutoModelOption
1262
- ? engines.find((group) => group.models.includes("auto"))
1346
+ ? isClaudeCodeAgent
1347
+ ? undefined
1348
+ : engines.find((group) => group.models.includes("auto"))
1263
1349
  : undefined;
1264
1350
  const providerGroups = useMemo(
1265
1351
  () =>
@@ -1271,6 +1357,60 @@ function ModelSelector({
1271
1357
  .filter((group) => group.models.length > 0),
1272
1358
  [engines],
1273
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]);
1274
1414
  const effortOptions =
1275
1415
  reasoning?.getOptionsForModel?.(model) ??
1276
1416
  getComposerReasoningEffortOptions(model);
@@ -1286,61 +1426,35 @@ function ModelSelector({
1286
1426
  ),
1287
1427
  [reasoning?.label, t],
1288
1428
  );
1289
-
1290
- // Collapse non-selected families by default. The family containing the
1291
- // currently-selected model stays expanded so the user sees their pick at
1292
- // a glance; clicking another family's header expands it inline.
1293
- const selectedGroupKey = useMemo(() => {
1294
- const found = providerGroups.find((g) => g.models.includes(model));
1295
- return found ? `${found.engine}:${found.label}` : null;
1296
- }, [model, providerGroups]);
1297
-
1298
- const [expandedGroups, setExpandedGroups] = useState<Set<string>>(
1299
- () => new Set(selectedGroupKey ? [selectedGroupKey] : []),
1429
+ const selectedAgentOption = agents?.find(
1430
+ (agent) => agent.id === (selectedAgent ?? "default"),
1300
1431
  );
1432
+ const selectedAgentLabel = selectedAgentOption?.label ?? "Default";
1433
+ const selectedModelLabel = friendlyModelName(model, t).replace(/^GPT-/, "");
1301
1434
 
1302
- // Reset expansion when the popover re-opens so the picker always lands
1303
- // on the "selected family expanded, others collapsed" view.
1304
- useEffect(() => {
1305
- if (open) {
1306
- setExpandedGroups(new Set(selectedGroupKey ? [selectedGroupKey] : []));
1307
- }
1308
- }, [open, selectedGroupKey]);
1309
-
1310
- const toggleGroup = useCallback((key: string) => {
1311
- setExpandedGroups((prev) => {
1312
- const next = new Set(prev);
1313
- if (next.has(key)) next.delete(key);
1314
- else next.add(key);
1315
- return next;
1316
- });
1317
- }, []);
1435
+ const [detailSection, setDetailSection] = useState<
1436
+ "agent" | "model" | "effort" | null
1437
+ >(null);
1438
+ const resolvedSection = detailSection ?? "model";
1318
1439
 
1319
- // The effort list is collapsed by default — it's a secondary
1320
- // control most users don't touch, so it stays tucked behind a header that
1321
- // reveals the current effort at a glance. Reset to collapsed on each open.
1322
- const [effortExpanded, setEffortExpanded] = useState(false);
1323
- useEffect(() => {
1324
- if (open) setEffortExpanded(false);
1325
- }, [open]);
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],
1447
+ );
1326
1448
 
1327
- // The optional image-model section follows the same collapsed-by-default
1328
- // pattern; the current selection shows next to the header.
1329
- const [imageExpanded, setImageExpanded] = useState(false);
1330
- useEffect(() => {
1331
- if (open) setImageExpanded(false);
1332
- }, [open]);
1333
- const imageModelLabel =
1334
- imageModel?.options.find((option) => option.value === imageModel.value)
1335
- ?.label ?? imageModel?.value;
1449
+ const visibleProviderGroups = modelProviderGroups;
1336
1450
  const showModelListSkeleton = shouldShowModelSelectorSkeleton(
1337
1451
  modelListLoading,
1338
1452
  engines.length,
1339
1453
  );
1340
1454
 
1341
- // When Builder.io isn't connected, surface a one-click connect path
1342
- // it unlocks every model family (Claude, OpenAI, Gemini) without the
1343
- // 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.
1344
1458
  const builderFlow = adapters.builder!.useConnectFlow!({
1345
1459
  enabled: providerConnectStatusEnabled,
1346
1460
  trackingSource: "composer_builder_cta",
@@ -1351,34 +1465,39 @@ function ModelSelector({
1351
1465
  const hasConnectedSubscription = providerGroups.some(
1352
1466
  (group) => group.configured && group.isSubscription,
1353
1467
  );
1354
- const showBuilderCta =
1355
- (builderFlow.hasFetchedStatus ||
1356
- (!providerConnectStatusEnabled && !!onConnectProvider)) &&
1357
- !builderFlow.configured &&
1358
- !builderFlow.envManaged &&
1359
- !hasConfiguredBuilderModels &&
1360
- !hasConnectedSubscription;
1361
- const onlyConnectPathAvailable = shouldShowOnlyConnectPath(
1362
- showBuilderCta,
1363
- providerGroups,
1468
+ const hasUnconfiguredVisibleModels = modelProviderGroups.some(
1469
+ (group) => !group.configured,
1364
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;
1365
1484
  const openLlmSettings = useCallback(() => {
1366
1485
  try {
1367
1486
  window.location.hash = "llm";
1368
1487
  } catch {}
1369
1488
  window.dispatchEvent(new CustomEvent("agent-panel:open-settings"));
1370
- setOpen(false);
1371
- }, []);
1489
+ setPickerOpen(false);
1490
+ }, [setPickerOpen]);
1372
1491
  const connectLocalRuntime = useCallback(
1373
1492
  (engine: string) => {
1374
1493
  onConnectLocalRuntime?.(engine);
1375
- setOpen(false);
1494
+ setPickerOpen(false);
1376
1495
  },
1377
- [onConnectLocalRuntime],
1496
+ [onConnectLocalRuntime, setPickerOpen],
1378
1497
  );
1379
1498
 
1380
1499
  return (
1381
- <Popover open={open} onOpenChange={setOpen}>
1500
+ <Popover open={open} onOpenChange={setPickerOpen}>
1382
1501
  <PopoverTrigger asChild>
1383
1502
  <button
1384
1503
  type="button"
@@ -1387,15 +1506,21 @@ function ModelSelector({
1387
1506
  defaultValue: "Model",
1388
1507
  })}: ${friendlyModelName(model, t)}${
1389
1508
  effortOptions.length > 0
1390
- ? `. ${t("agentChat.composer.reasoning", {
1391
- defaultValue: "Reasoning",
1509
+ ? `. ${t("agentChat.composer.effort", {
1510
+ defaultValue: "Effort",
1392
1511
  })}: ${effortLabel(selectedEffort)}`
1393
1512
  : ""
1513
+ }${
1514
+ selectedAgentOption && selectedAgentOption.id !== "default"
1515
+ ? `. Agent: ${selectedAgentLabel}`
1516
+ : ""
1394
1517
  }`}
1395
1518
  className="agent-composer-model-button flex min-w-0 max-w-[10.5rem] shrink items-center gap-1 rounded-md px-2 py-1 text-[12px] font-medium text-muted-foreground hover:bg-accent/50 hover:text-foreground"
1396
1519
  >
1397
1520
  <span className="min-w-0 truncate">
1398
- {compactComposerModelName(model, t)}
1521
+ {selectedAgentOption && selectedAgentOption.id !== "default"
1522
+ ? selectedAgentLabel
1523
+ : compactComposerModelName(model, t)}
1399
1524
  </span>
1400
1525
  {effortOptions.length > 0 && (
1401
1526
  <span className="agent-composer-model-effort min-w-0 shrink truncate text-muted-foreground/70">
@@ -1411,292 +1536,527 @@ function ModelSelector({
1411
1536
  sideOffset={6}
1412
1537
  collisionPadding={8}
1413
1538
  data-agent-native-composer-popover="true"
1414
- 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}`}
1415
1540
  style={MODEL_SELECTOR_POPOVER_STYLE}
1416
1541
  >
1417
- {showBuilderCta && (
1418
- <>
1419
- <button
1420
- type="button"
1421
- onClick={() => {
1422
- if (onConnectProvider) {
1423
- onConnectProvider();
1424
- } else {
1425
- builderFlow.start();
1426
- }
1427
- }}
1428
- disabled={!onConnectProvider && builderFlow.connecting}
1429
- className="flex w-full items-start gap-2 px-3 py-2 text-start hover:bg-accent/50 disabled:opacity-60"
1430
- >
1431
- <IconPlugConnected className="h-4 w-4 shrink-0 mt-0.5 text-blue-500" />
1432
- <span className="flex-1 min-w-0">
1433
- <span className="block text-[12px] font-medium text-foreground">
1434
- {!onConnectProvider && builderFlow.connecting
1435
- ? t("agentPanel.connectingBuilder", {
1436
- defaultValue: "Connecting Builder.io…",
1437
- })
1438
- : t("agentPanel.connectBuilderIo", {
1439
- defaultValue: "Connect Builder.io",
1440
- })}
1441
- </span>
1442
- <span className="block text-[11px] text-muted-foreground">
1443
- {t("agentPanel.builderModelCredits", {
1444
- defaultValue: "Free credits for Claude, OpenAI & Gemini",
1445
- })}
1446
- </span>
1447
- </span>
1448
- </button>
1449
- {!onConnectProvider && builderFlow.error && (
1450
- <p
1451
- role="alert"
1452
- className="px-3 pb-2 ps-9 text-[11px] text-destructive"
1453
- >
1454
- {builderFlow.error}
1455
- </p>
1456
- )}
1457
- <button
1458
- type="button"
1459
- onClick={openLlmSettings}
1460
- className="flex w-full items-start gap-2 px-3 py-2 text-start hover:bg-accent/50"
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
+ })}
1461
1550
  >
1462
- <IconKey className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
1463
- <span className="min-w-0 flex-1">
1464
- <span className="block text-[12px] font-medium text-foreground">
1465
- {t("agentPanel.addOwnKeys", {
1466
- defaultValue: "Add your own keys",
1467
- })}
1468
- </span>
1469
- <span className="block text-[11px] text-muted-foreground">
1470
- {t("agentPanel.configureProviderKeys", {
1471
- defaultValue: "Choose a cloud, gateway, or local provider",
1472
- })}
1473
- </span>
1474
- </span>
1475
- </button>
1476
- {!onlyConnectPathAvailable && (
1477
- <div className="my-1 border-t border-border" />
1478
- )}
1479
- </>
1480
- )}
1481
- {imageModel && imageModel.options.length > 0 && (
1482
- <>
1483
- <div className="flex items-center hover:bg-accent/30">
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
+ }`}
1563
+ >
1564
+ <span className="flex min-w-0 items-center">
1565
+ <span className="shrink-0 text-[12px] font-medium">
1566
+ Agent
1567
+ </span>
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
+ )}
1484
1616
  <button
1485
1617
  type="button"
1486
- aria-expanded={imageExpanded}
1487
- onClick={() => setImageExpanded((prev) => !prev)}
1488
- 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
+ }`}
1489
1627
  >
1490
- {imageExpanded ? (
1491
- <IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
1492
- ) : (
1493
- <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1494
- )}
1495
- <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1496
- {imageModel.label ??
1497
- t("agentChat.composer.imageModel", {
1498
- defaultValue: "Image model",
1499
- })}
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}
1500
1631
  </span>
1501
- {!imageExpanded && imageModelLabel && (
1502
- <span className="text-[11px] text-muted-foreground/80 truncate">
1503
- {imageModelLabel}
1504
- </span>
1505
- )}
1632
+ <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1506
1633
  </button>
1507
- </div>
1508
- {imageExpanded &&
1509
- imageModel.options.map((option) => (
1634
+ {effortOptions.length > 0 && (
1510
1635
  <button
1511
- key={option.value}
1512
1636
  type="button"
1513
- onClick={() => {
1514
- imageModel.onChange(option.value);
1515
- setImageExpanded(false);
1516
- }}
1517
- 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
+ }`}
1518
1646
  >
1519
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1520
- {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)}
1521
1654
  </span>
1522
- {option.value === imageModel.value && (
1523
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1524
- )}
1655
+ <IconChevronRight className="h-3 w-3 shrink-0 opacity-60 rtl:-scale-x-100" />
1525
1656
  </button>
1526
- ))}
1527
- <div className="my-1 border-t border-border" />
1528
- </>
1529
- )}
1530
- {showModelListSkeleton && <ModelSelectorSkeleton />}
1531
- {autoModelGroup && !onlyConnectPathAvailable && (
1532
- <button
1533
- type="button"
1534
- onClick={() => {
1535
- onChange("auto", autoModelGroup.engine);
1536
- setOpen(false);
1537
- }}
1538
- 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}
1539
1668
  >
1540
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1541
- {t("agentChat.composer.auto", { defaultValue: "Auto" })}
1542
- </span>
1543
- {model === "auto" && (
1544
- <IconCheck className="h-3.5 w-3.5 shrink-0 text-blue-500" />
1545
- )}
1546
- </button>
1547
- )}
1548
- {autoModelGroup &&
1549
- providerGroups.length > 0 &&
1550
- !onlyConnectPathAvailable && (
1551
- <div className="my-1 border-t border-border" />
1552
- )}
1553
- {!onlyConnectPathAvailable &&
1554
- providerGroups.map((group) => {
1555
- const models = latestModelsOnly(group.models);
1556
- const groupKey = `${group.engine}:${group.label}`;
1557
- const isExpanded = expandedGroups.has(groupKey);
1558
- const ChevronIcon = isExpanded ? IconChevronDown : IconChevronRight;
1559
- const isLocalRuntime =
1560
- group.engine === "codex-cli" || group.engine === "claude-cli";
1561
- const canConnectLocalRuntime =
1562
- isLocalRuntime && Boolean(onConnectLocalRuntime);
1563
- const statusLabel =
1564
- group.statusLabel ??
1565
- (!group.configured
1566
- ? canConnectLocalRuntime
1567
- ? t("agentChat.auth.logIn", { defaultValue: "Sign in" })
1568
- : t("agentChat.composer.needsApiKey", {
1569
- defaultValue: "needs API key",
1570
- })
1571
- : undefined);
1572
- return (
1573
- <div key={groupKey}>
1574
- <div className="flex items-center hover:bg-accent/30">
1575
- <button
1576
- type="button"
1577
- aria-expanded={isExpanded}
1578
- onClick={() => toggleGroup(groupKey)}
1579
- className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
1580
- >
1581
- <ChevronIcon className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1582
- <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1583
- {group.label}
1584
- </span>
1585
- {!isExpanded && groupKey === selectedGroupKey && (
1586
- <span className="text-[11px] text-muted-foreground/80 truncate">
1587
- {friendlyModelName(model, t)}
1588
- </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
+ </>
1589
1836
  )}
1590
- </button>
1591
- {!group.configured && statusLabel && (
1592
- <button
1593
- type="button"
1594
- 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"
1595
- onClick={() =>
1596
- canConnectLocalRuntime
1597
- ? connectLocalRuntime(group.engine)
1598
- : openLlmSettings()
1599
- }
1600
- >
1601
- {statusLabel}
1602
- </button>
1603
- )}
1604
- {group.configured && statusLabel && (
1605
- <span className="ms-auto max-w-[9rem] shrink-0 pe-3 text-end text-[10px] text-muted-foreground/70">
1606
- {statusLabel}
1607
- </span>
1608
- )}
1609
- </div>
1610
- {isExpanded &&
1611
- models.map((m) => (
1612
- <button
1613
- key={m}
1614
- type="button"
1615
- onClick={() => {
1616
- if (!group.configured) {
1617
- if (canConnectLocalRuntime) {
1618
- connectLocalRuntime(group.engine);
1619
- } else {
1620
- openLlmSettings();
1621
- }
1622
- return;
1623
- }
1624
- onChange(m, group.engine);
1625
- const nextOptions =
1626
- getReasoningEffortOptionsForModel(m);
1627
- if (
1628
- nextOptions.length > 0 &&
1629
- !nextOptions.includes(selectedEffort)
1630
- ) {
1631
- onEffortChange?.(defaultEffort);
1632
- }
1633
- setOpen(false);
1634
- }}
1635
- className={`flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start ${
1636
- group.configured
1637
- ? "hover:bg-accent/50"
1638
- : "opacity-40 cursor-default"
1639
- }`}
1640
- >
1641
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1642
- {friendlyModelName(m, t)}
1643
- </span>
1644
- <ModelCostTier model={m} />
1645
- {m === model && group.configured && (
1646
- <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>
1647
1890
  )}
1648
- </button>
1649
- ))}
1650
- </div>
1651
- );
1652
- })}
1653
- {!showModelListSkeleton &&
1654
- !onlyConnectPathAvailable &&
1655
- effortOptions.length > 0 && (
1656
- <>
1657
- <div className="my-1 border-t border-border" />
1658
- <div className="flex items-center hover:bg-accent/30">
1659
- <button
1660
- type="button"
1661
- aria-expanded={effortExpanded}
1662
- onClick={() => setEffortExpanded((prev) => !prev)}
1663
- className="flex flex-1 min-w-0 items-center gap-1.5 px-2 py-1.5 cursor-pointer text-start"
1664
- >
1665
- {effortExpanded ? (
1666
- <IconChevronDown className="h-3 w-3 shrink-0 text-muted-foreground" />
1667
- ) : (
1668
- <IconChevronRight className="h-3 w-3 shrink-0 text-muted-foreground rtl:-scale-x-100" />
1669
- )}
1670
- <span className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide shrink-0">
1671
- {t("agentChat.composer.reasoning", {
1672
- defaultValue: "Reasoning",
1673
- })}
1674
- </span>
1675
- {!effortExpanded && (
1676
- <span className="text-[11px] text-muted-foreground/80 truncate">
1677
- {effortLabel(selectedEffort)}
1678
- </span>
1679
- )}
1680
- </button>
1681
- </div>
1682
- {effortExpanded &&
1683
- effortOptions.map((option) => (
1684
- <button
1685
- key={option}
1686
- type="button"
1687
- onClick={() => onEffortChange?.(option)}
1688
- className="flex w-full items-center gap-3 ps-7 pe-3 py-1.5 text-start hover:bg-accent/50"
1689
- >
1690
- <span className="flex-1 min-w-0 text-[13px] text-foreground truncate">
1691
- {effortLabel(option)}
1692
- </span>
1693
- {option === selectedEffort && (
1694
- <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>
1695
1915
  )}
1696
- </button>
1697
- ))}
1698
- </>
1699
- )}
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>
1700
2060
  </PopoverContent>
1701
2061
  </Popover>
1702
2062
  );
@@ -1767,10 +2127,16 @@ export function TiptapComposer({
1767
2127
  selectedModel,
1768
2128
  selectedEffort,
1769
2129
  showAutoModelOption = true,
2130
+ modelSelectorOpen,
1770
2131
  availableModels,
1771
2132
  modelListLoading,
1772
2133
  onModelChange,
1773
2134
  onEffortChange,
2135
+ availableAgents,
2136
+ selectedAgent,
2137
+ hostedHarness,
2138
+ onAgentChange,
2139
+ onModelSelectorOpenChange,
1774
2140
  providerConnectStatusEnabled,
1775
2141
  onConnectProvider,
1776
2142
  onConnectLocalRuntime,
@@ -3274,12 +3640,18 @@ export function TiptapComposer({
3274
3640
  {selectedModel && availableModels && onModelChange && (
3275
3641
  <ModelSelector
3276
3642
  model={selectedModel}
3643
+ open={modelSelectorOpen}
3277
3644
  effort={selectedEffort}
3278
3645
  engines={availableModels}
3646
+ agents={availableAgents}
3647
+ selectedAgent={selectedAgent}
3648
+ hostedHarness={hostedHarness}
3279
3649
  showAutoModelOption={showAutoModelOption}
3280
3650
  modelListLoading={modelListLoading}
3281
3651
  onChange={onModelChange}
3282
3652
  onEffortChange={onEffortChange}
3653
+ onAgentChange={onAgentChange}
3654
+ onModelSelectorOpenChange={onModelSelectorOpenChange}
3283
3655
  providerConnectStatusEnabled={providerConnectStatusEnabled}
3284
3656
  onConnectProvider={onConnectProvider}
3285
3657
  onConnectLocalRuntime={onConnectLocalRuntime}