@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
@@ -0,0 +1,31 @@
1
+ export function isClaudeCodeAgentId(agentId) {
2
+ return agentId === "claude-code" || agentId === "claude-cli";
3
+ }
4
+ export function isLunaModel(model) {
5
+ return model.toLowerCase().includes("luna");
6
+ }
7
+ export function resolvePreferredAgentModel(agentId, groups) {
8
+ const candidates = groups.flatMap((group) => group.models
9
+ .filter((model) => model !== "auto")
10
+ .map((model) => ({
11
+ model,
12
+ engine: group.engine,
13
+ configured: group.configured !== false,
14
+ })));
15
+ const orderedCandidates = [
16
+ ...candidates.filter((candidate) => candidate.configured),
17
+ ...candidates.filter((candidate) => !candidate.configured),
18
+ ];
19
+ if (isClaudeCodeAgentId(agentId)) {
20
+ const preferred = orderedCandidates.find((candidate) => candidate.model.toLowerCase().includes("sonnet") &&
21
+ !isLunaModel(candidate.model)) ?? orderedCandidates.find((candidate) => !isLunaModel(candidate.model));
22
+ return preferred
23
+ ? { model: preferred.model, engine: preferred.engine }
24
+ : undefined;
25
+ }
26
+ const preferred = orderedCandidates.find((candidate) => isLunaModel(candidate.model));
27
+ return preferred
28
+ ? { model: preferred.model, engine: preferred.engine }
29
+ : undefined;
30
+ }
31
+ //# sourceMappingURL=model-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-selection.js","sourceRoot":"","sources":["../../src/composer/model-selection.ts"],"names":[],"mappings":"AAMA,MAAM,UAAU,mBAAmB,CAAC,OAA2B;IAC7D,OAAO,OAAO,KAAK,aAAa,IAAI,OAAO,KAAK,YAAY,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,OAA2B,EAC3B,MAAyC;IAEzC,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE,CAC1C,KAAK,CAAC,MAAM;SACT,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,MAAM,CAAC;SACnC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACf,KAAK;QACL,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,UAAU,EAAE,KAAK,CAAC,UAAU,KAAK,KAAK;KACvC,CAAC,CAAC,CACN,CAAC;IACF,MAAM,iBAAiB,GAAG;QACxB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC;QACzD,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC;KAC3D,CAAC;IAEF,IAAI,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;QACjC,MAAM,SAAS,GACb,iBAAiB,CAAC,IAAI,CACpB,CAAC,SAAS,EAAE,EAAE,CACZ,SAAS,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAChD,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAChC,IAAI,iBAAiB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5E,OAAO,SAAS;YACd,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE;YACtD,CAAC,CAAC,SAAS,CAAC;IAChB,CAAC;IAED,MAAM,SAAS,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CACrD,WAAW,CAAC,SAAS,CAAC,KAAK,CAAC,CAC7B,CAAC;IACF,OAAO,SAAS;QACd,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE;QACtD,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=model-selection.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-selection.spec.d.ts","sourceRoot":"","sources":["../../src/composer/model-selection.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { isClaudeCodeAgentId, isLunaModel, resolvePreferredAgentModel, } from "./model-selection.js";
3
+ describe("composer agent model defaults", () => {
4
+ const groups = [
5
+ {
6
+ engine: "builder",
7
+ models: ["gpt-5-6-luna"],
8
+ configured: true,
9
+ },
10
+ {
11
+ engine: "builder",
12
+ models: ["claude-sonnet-5"],
13
+ configured: true,
14
+ },
15
+ ];
16
+ it("identifies Claude Code and Luna model ids", () => {
17
+ expect(isClaudeCodeAgentId("claude-code")).toBe(true);
18
+ expect(isClaudeCodeAgentId("claude-cli")).toBe(true);
19
+ expect(isClaudeCodeAgentId("codex")).toBe(false);
20
+ expect(isLunaModel("gpt-5.6-luna")).toBe(true);
21
+ expect(isLunaModel("openai/gpt-5.6-sol")).toBe(false);
22
+ });
23
+ it("defaults Claude Code to Sonnet and other agents to Luna", () => {
24
+ expect(resolvePreferredAgentModel("claude-code", groups)).toEqual({
25
+ engine: "builder",
26
+ model: "claude-sonnet-5",
27
+ });
28
+ expect(resolvePreferredAgentModel("codex", groups)).toEqual({
29
+ engine: "builder",
30
+ model: "gpt-5-6-luna",
31
+ });
32
+ expect(resolvePreferredAgentModel(undefined, groups)).toEqual({
33
+ engine: "builder",
34
+ model: "gpt-5-6-luna",
35
+ });
36
+ });
37
+ });
38
+ //# sourceMappingURL=model-selection.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-selection.spec.js","sourceRoot":"","sources":["../../src/composer/model-selection.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C,OAAO,EACL,mBAAmB,EACnB,WAAW,EACX,0BAA0B,GAC3B,MAAM,sBAAsB,CAAC;AAE9B,QAAQ,CAAC,+BAA+B,EAAE,GAAG,EAAE;IAC7C,MAAM,MAAM,GAAG;QACb;YACE,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,CAAC,cAAc,CAAC;YACxB,UAAU,EAAE,IAAI;SACjB;QACD;YACE,MAAM,EAAE,SAAS;YACjB,MAAM,EAAE,CAAC,iBAAiB,CAAC;YAC3B,UAAU,EAAE,IAAI;SACjB;KACF,CAAC;IAEF,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,mBAAmB,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtD,MAAM,CAAC,mBAAmB,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACrD,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,CAAC,WAAW,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,MAAM,CAAC,WAAW,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,CAAC,0BAA0B,CAAC,aAAa,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;YAChE,MAAM,EAAE,SAAS;YACjB,KAAK,EAAE,iBAAiB;SACzB,CAAC,CAAC;QACH,MAAM,CAAC,0BAA0B,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;YAC1D,MAAM,EAAE,SAAS;YACjB,KAAK,EAAE,cAAc;SACtB,CAAC,CAAC;QACH,MAAM,CAAC,0BAA0B,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;YAC5D,MAAM,EAAE,SAAS;YACjB,KAAK,EAAE,cAAc;SACtB,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/toolkit",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Reusable app-building UI and helpers for Agent-Native apps.",
5
5
  "homepage": "https://github.com/BuilderIO/agent-native#readme",
6
6
  "bugs": {
@@ -11,6 +11,7 @@ import {
11
11
  IconLoader2,
12
12
  IconArrowLeft,
13
13
  IconX,
14
+ IconHelpCircle,
14
15
  } from "@tabler/icons-react";
15
16
  import React, {
16
17
  useState,
@@ -196,6 +197,39 @@ function formatAttachmentError(error: unknown, fallback: string): string {
196
197
  return error instanceof Error ? error.message : fallback;
197
198
  }
198
199
 
200
+ function MenuItemHelp({
201
+ label,
202
+ description,
203
+ }: {
204
+ label: string;
205
+ description?: string;
206
+ }) {
207
+ const normalizedDescription = description?.trim();
208
+ if (!normalizedDescription) return null;
209
+ return (
210
+ <Tooltip>
211
+ <TooltipTrigger asChild>
212
+ <span
213
+ role="img"
214
+ tabIndex={0}
215
+ aria-label={`${label}: ${normalizedDescription}`}
216
+ onClick={(event) => event.stopPropagation()}
217
+ className="flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
218
+ >
219
+ <IconHelpCircle
220
+ aria-hidden="true"
221
+ className="size-3"
222
+ strokeWidth={1.8}
223
+ />
224
+ </span>
225
+ </TooltipTrigger>
226
+ <TooltipContent side="right" className="max-w-xs">
227
+ {normalizedDescription}
228
+ </TooltipContent>
229
+ </Tooltip>
230
+ );
231
+ }
232
+
199
233
  function UploadOnlyAttachButton({
200
234
  onAttachmentError,
201
235
  }: Pick<ComposerPlusMenuProps, "onAttachmentError">) {
@@ -612,7 +646,11 @@ function ComposerPlusMenuFull({
612
646
  return (
613
647
  <div
614
648
  key={item.label}
615
- className={cn("relative", isSkill && "group/skill")}
649
+ className={cn(
650
+ "relative flex items-center hover:bg-accent/50",
651
+ isSkill && "group/skill",
652
+ isSkill && skillFlyoutOpen && "bg-accent/50",
653
+ )}
616
654
  onMouseEnter={(e) => {
617
655
  if (isSkill) {
618
656
  openSkillFlyout(e.currentTarget);
@@ -640,25 +678,25 @@ function ComposerPlusMenuFull({
640
678
  type="button"
641
679
  onClick={item.action}
642
680
  className={cn(
643
- "flex w-full items-center gap-2.5 px-3 py-2 text-start hover:bg-accent/50",
644
- isSkill && skillFlyoutOpen && "bg-accent/50",
681
+ "flex min-w-0 flex-1 items-center gap-2.5 px-3 py-2 text-start",
645
682
  )}
646
683
  >
647
684
  <span className="text-muted-foreground">{item.icon}</span>
648
- <div className="min-w-0 flex-1">
649
- <div className="text-[12px] font-medium text-foreground">
685
+ <span className="flex min-w-0 items-center gap-0.5">
686
+ <span className="min-w-0 truncate text-[12px] font-medium text-foreground">
650
687
  {item.label}
651
- </div>
652
- <div className="mt-0.5 text-[10px] text-muted-foreground/60">
653
- {item.desc}
654
- </div>
655
- </div>
656
- {isSkill && (
657
- <span className="ms-auto text-muted-foreground/60">
658
-
659
688
  </span>
660
- )}
689
+ <MenuItemHelp
690
+ label={item.label}
691
+ description={item.desc}
692
+ />
693
+ </span>
661
694
  </button>
695
+ {isSkill && (
696
+ <span className="me-3 ms-auto text-muted-foreground/60">
697
+
698
+ </span>
699
+ )}
662
700
  {isSkill && skillFlyoutOpen && (
663
701
  <div
664
702
  role="menu"
@@ -671,57 +709,76 @@ function ComposerPlusMenuFull({
671
709
  : "right-full mr-1",
672
710
  )}
673
711
  >
674
- <button
675
- type="button"
676
- onClick={() => {
677
- onSelectMode?.("skill");
678
- setSkillFlyoutOpen(false);
679
- setOpen(false);
680
- }}
681
- className="flex w-full items-center gap-2.5 px-3 py-2 text-start hover:bg-accent/50"
682
- >
683
- <span className="text-muted-foreground">
684
- <IconBulb className="h-3.5 w-3.5" />
685
- </span>
686
- <div className="min-w-0">
687
- <div className="text-[12px] font-medium text-foreground">
688
- {t("agentChat.composer.skill.createNew", {
689
- defaultValue: "Create new skill",
690
- })}
691
- </div>
692
- <div className="mt-0.5 text-[10px] text-muted-foreground/60">
693
- {t("agentChat.composer.skill.createDescription", {
694
- defaultValue:
695
- "Describe a skill and let the agent draft it",
696
- })}
697
- </div>
698
- </div>
699
- </button>
700
- <button
701
- type="button"
702
- onClick={() => {
703
- setSkillFlyoutOpen(false);
704
- skillFileInputRef.current?.click();
705
- }}
706
- className="flex w-full items-center gap-2.5 px-3 py-2 text-start hover:bg-accent/50"
707
- >
708
- <span className="text-muted-foreground">
709
- <IconUpload className="h-3.5 w-3.5" />
710
- </span>
711
- <div className="min-w-0">
712
- <div className="text-[12px] font-medium text-foreground">
713
- {t("agentChat.composer.skill.uploadFile", {
714
- defaultValue: "Upload skill file",
715
- })}
716
- </div>
717
- <div className="mt-0.5 text-[10px] text-muted-foreground/60">
718
- {t("agentChat.composer.skill.uploadDescription", {
719
- defaultValue:
720
- "Import an existing SKILL.md file",
721
- })}
722
- </div>
723
- </div>
724
- </button>
712
+ <div className="flex items-center hover:bg-accent/50">
713
+ <button
714
+ type="button"
715
+ onClick={() => {
716
+ onSelectMode?.("skill");
717
+ setSkillFlyoutOpen(false);
718
+ setOpen(false);
719
+ }}
720
+ className="flex min-w-0 flex-1 items-center gap-2.5 px-3 py-2 text-start"
721
+ >
722
+ <span className="text-muted-foreground">
723
+ <IconBulb className="h-3.5 w-3.5" />
724
+ </span>
725
+ <span className="flex min-w-0 items-center gap-0.5">
726
+ <span className="min-w-0 truncate text-[12px] font-medium text-foreground">
727
+ {t("agentChat.composer.skill.createNew", {
728
+ defaultValue: "Create new skill",
729
+ })}
730
+ </span>
731
+ <MenuItemHelp
732
+ label={t("agentChat.composer.skill.createNew", {
733
+ defaultValue: "Create new skill",
734
+ })}
735
+ description={t(
736
+ "agentChat.composer.skill.createDescription",
737
+ {
738
+ defaultValue:
739
+ "Describe a skill and let the agent draft it",
740
+ },
741
+ )}
742
+ />
743
+ </span>
744
+ </button>
745
+ </div>
746
+ <div className="flex items-center hover:bg-accent/50">
747
+ <button
748
+ type="button"
749
+ onClick={() => {
750
+ setSkillFlyoutOpen(false);
751
+ skillFileInputRef.current?.click();
752
+ }}
753
+ className="flex min-w-0 flex-1 items-center gap-2.5 px-3 py-2 text-start"
754
+ >
755
+ <span className="text-muted-foreground">
756
+ <IconUpload className="h-3.5 w-3.5" />
757
+ </span>
758
+ <span className="flex min-w-0 items-center gap-0.5">
759
+ <span className="min-w-0 truncate text-[12px] font-medium text-foreground">
760
+ {t("agentChat.composer.skill.uploadFile", {
761
+ defaultValue: "Upload skill file",
762
+ })}
763
+ </span>
764
+ <MenuItemHelp
765
+ label={t(
766
+ "agentChat.composer.skill.uploadFile",
767
+ {
768
+ defaultValue: "Upload skill file",
769
+ },
770
+ )}
771
+ description={t(
772
+ "agentChat.composer.skill.uploadDescription",
773
+ {
774
+ defaultValue:
775
+ "Import an existing SKILL.md file",
776
+ },
777
+ )}
778
+ />
779
+ </span>
780
+ </button>
781
+ </div>
725
782
  </div>
726
783
  )}
727
784
  </div>
@@ -100,6 +100,8 @@ export interface PromptComposerProps {
100
100
  preserveDraftOnSubmit?: boolean;
101
101
  /** Show the model selector (default: true). */
102
102
  showModelSelector?: boolean;
103
+ /** Controlled open state for hosts that resize around the model picker. */
104
+ modelSelectorOpen?: boolean;
103
105
  /** Show the legacy provider-level Auto model option (default: true). */
104
106
  showAutoModelOption?: boolean;
105
107
  /** Show the voice dictation button. Defaults to DEFAULT_VOICE_DICTATION_ENABLED. */
@@ -156,6 +158,8 @@ export interface PromptComposerProps {
156
158
  selectedAgent?: string;
157
159
  /** Callback when the user picks an agent runtime. */
158
160
  onAgentChange?: (agent: string) => void;
161
+ /** Called when the shared model picker opens or closes. */
162
+ onModelSelectorOpenChange?: (open: boolean) => void;
159
163
  /**
160
164
  * Enable server-backed model/provider status checks. Defaults off when the
161
165
  * host supplies model state and callbacks, otherwise on.
@@ -497,6 +501,7 @@ function PromptComposerInner({
497
501
  draftScope,
498
502
  preserveDraftOnSubmit = false,
499
503
  showModelSelector = true,
504
+ modelSelectorOpen,
500
505
  showAutoModelOption = true,
501
506
  voiceEnabled = DEFAULT_VOICE_DICTATION_ENABLED,
502
507
  attachmentsEnabled = true,
@@ -525,6 +530,7 @@ function PromptComposerInner({
525
530
  availableAgents,
526
531
  selectedAgent,
527
532
  onAgentChange,
533
+ onModelSelectorOpenChange,
528
534
  modelStatusChecksEnabled,
529
535
  onTextChange,
530
536
  onConnectProvider,
@@ -697,6 +703,7 @@ function PromptComposerInner({
697
703
  onTextChange={onTextChange}
698
704
  draftScope={draftScope}
699
705
  selectedModel={composerModel}
706
+ modelSelectorOpen={modelSelectorOpen}
700
707
  selectedEffort={composerEffort}
701
708
  availableModels={composerModelGroups}
702
709
  availableAgents={availableAgents}
@@ -706,6 +713,7 @@ function PromptComposerInner({
706
713
  onModelChange={handleModelChange}
707
714
  onEffortChange={handleEffortChange}
708
715
  onAgentChange={onAgentChange}
716
+ onModelSelectorOpenChange={onModelSelectorOpenChange}
709
717
  providerConnectStatusEnabled={resolvedModelStatusChecksEnabled}
710
718
  onConnectProvider={onConnectProvider}
711
719
  onConnectLocalRuntime={onConnectLocalRuntime}
@@ -18,6 +18,7 @@ import {
18
18
  getOversizedDocumentAttachmentError,
19
19
  handleComposerFileDrop,
20
20
  insertComposerHardBreakAndScrollIntoView,
21
+ isOpenAiModelProviderGroup,
21
22
  isComposerEditorUsable,
22
23
  formatVoiceTranscriptForComposer,
23
24
  MODEL_SELECTOR_POPOVER_STYLE,
@@ -106,6 +107,51 @@ describe("createTiptapComposerExtensions", () => {
106
107
  );
107
108
  });
108
109
 
110
+ it("limits Codex to OpenAI model providers", () => {
111
+ expect(
112
+ isOpenAiModelProviderGroup({
113
+ engine: "builder",
114
+ label: "OpenAI",
115
+ models: ["gpt-5.6-luna"],
116
+ }),
117
+ ).toBe(true);
118
+ expect(
119
+ isOpenAiModelProviderGroup({
120
+ engine: "ai-sdk:google",
121
+ label: "Gemini",
122
+ models: ["gemini-3.5-flash"],
123
+ }),
124
+ ).toBe(false);
125
+ expect(
126
+ isOpenAiModelProviderGroup({
127
+ engine: "custom-gateway",
128
+ label: "Custom",
129
+ models: ["gpt-5.6-luna"],
130
+ }),
131
+ ).toBe(true);
132
+ expect(
133
+ isOpenAiModelProviderGroup({
134
+ engine: "ai-sdk:openrouter",
135
+ label: "OpenRouter",
136
+ models: ["openai/gpt-5.6-luna"],
137
+ }),
138
+ ).toBe(true);
139
+ expect(
140
+ isOpenAiModelProviderGroup({
141
+ engine: "codex-cli",
142
+ label: "OpenAI",
143
+ models: ["gpt-5.6-luna"],
144
+ }),
145
+ ).toBe(true);
146
+ expect(
147
+ isOpenAiModelProviderGroup({
148
+ engine: "codex-cli",
149
+ label: "OpenAI",
150
+ models: ["codex-cli"],
151
+ }),
152
+ ).toBe(false);
153
+ });
154
+
109
155
  it("keeps the prompt composer schema minimal and restores legacy draft HTML", () => {
110
156
  const editor = new Editor({
111
157
  element: document.createElement("div"),