@tangle-network/sandbox-ui 0.21.0 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/auth.js CHANGED
@@ -4,7 +4,9 @@ import {
4
4
  LoginLayout,
5
5
  TangleLoginButton,
6
6
  UserMenu
7
- } from "./chunk-IOB2PW5Z.js";
7
+ } from "./chunk-R3ZMMCT3.js";
8
+ import "./chunk-UHFJXO24.js";
9
+ import "./chunk-EI44GEQ5.js";
8
10
  export {
9
11
  AuthHeader,
10
12
  GitHubLoginButton,
package/dist/chat.d.ts CHANGED
@@ -28,6 +28,15 @@ interface AgentSessionHarnessControl {
28
28
  /** Filter the selectable harnesses (e.g. by plan tier). Defaults to all. */
29
29
  available?: ReadonlyArray<HarnessType>;
30
30
  disabled?: boolean;
31
+ /**
32
+ * A harness is bound to its chat session once the conversation has
33
+ * started. While locked the dropdown is inert and the model catalog
34
+ * is filtered to what this harness can run — fork the session to
35
+ * switch harness.
36
+ */
37
+ locked?: boolean;
38
+ /** Tooltip shown on the locked trigger. */
39
+ lockReason?: string;
31
40
  }
32
41
  interface AgentSessionModelControl {
33
42
  /** Canonical model id (provider-prefixed, e.g. "anthropic/claude-opus-4-8"). */
@@ -65,10 +74,61 @@ interface AgentSessionControlsProps {
65
74
  * thinking-effort pickers in one row. Every section is optional and only
66
75
  * renders when its control object is provided — never show a dead control.
67
76
  *
77
+ * When BOTH harness and model controls are present the pair is kept
78
+ * coherent automatically (see harness-model-compat): picking a harness
79
+ * snaps an incompatible model to that harness's best catalog option;
80
+ * picking a model the current harness can't run switches to the model's
81
+ * native harness — unless the harness is `locked`, in which case the
82
+ * catalog itself is filtered to compatible models.
83
+ *
68
84
  * Designed to slot into `SandboxWorkbench`'s `session.composerControls`.
69
85
  */
70
86
  declare function AgentSessionControls({ harness, model, reasoning, trailing, className, }: AgentSessionControlsProps): react_jsx_runtime.JSX.Element | null;
71
87
 
88
+ /**
89
+ * Harness ↔ model compatibility policy.
90
+ *
91
+ * Native CLI harnesses are vendor-locked: claude-code only drives
92
+ * Anthropic models, codex only OpenAI models. Router-backed harnesses
93
+ * (opencode) accept any catalog model. The pickers use this policy to
94
+ * keep the pair coherent: changing one side snaps the other to its
95
+ * nearest compatible choice instead of letting the user assemble a
96
+ * combination that fails at inference time.
97
+ */
98
+ interface HarnessModelPolicy {
99
+ /** Canonical-id provider prefixes the harness can run; null = any. */
100
+ providers: ReadonlyArray<string> | null;
101
+ /**
102
+ * Patterns ranking snap targets, best first. Within one pattern's
103
+ * matches the highest version (numeric-aware descending sort) wins,
104
+ * so "latest standard frontier" stays correct as catalogs rotate.
105
+ */
106
+ preferred: ReadonlyArray<RegExp>;
107
+ }
108
+ declare const HARNESS_MODEL_POLICIES: Record<HarnessType, HarnessModelPolicy>;
109
+ /** Provider prefix of a canonical id ("anthropic/claude-…" → "anthropic"). */
110
+ declare function modelProvider(modelId: string): string | null;
111
+ /**
112
+ * Ids without a provider prefix (consumer sentinels like "default",
113
+ * or an empty selection) are treated as compatible everywhere — they
114
+ * mean "the session's own configuration", which every harness honors.
115
+ */
116
+ declare function isModelCompatibleWithHarness(harness: HarnessType, modelId: string): boolean;
117
+ /**
118
+ * Keeps `modelId` when the harness can run it; otherwise returns the
119
+ * harness's best compatible catalog model (preferred patterns in order,
120
+ * highest version within a pattern). When the catalog holds nothing
121
+ * compatible the original id is returned unchanged — the caller sees
122
+ * the incompatibility instead of a silent wrong substitution.
123
+ */
124
+ declare function snapModelToHarness(harness: HarnessType, modelId: string, models: ReadonlyArray<ModelInfo>): string;
125
+ /**
126
+ * Keeps the harness when it can run `modelId`; otherwise returns the
127
+ * model's native harness (anthropic → claude-code, openai → codex),
128
+ * falling back to the router-backed opencode for everything else.
129
+ */
130
+ declare function snapHarnessToModel(harness: HarnessType, modelId: string): HarnessType;
131
+
72
132
  type ArtifactKind = string;
73
133
  interface ArtifactScope {
74
134
  kind: ArtifactKind;
@@ -166,4 +226,4 @@ declare function createFetchTransport(opts: {
166
226
  fetchImpl?: typeof fetch;
167
227
  }): ArtifactAgentDockTransport;
168
228
 
169
- export { AgentSessionControls, type AgentSessionControlsProps, type AgentSessionHarnessControl, type AgentSessionModelControl, type AgentSessionReasoningControl, ArtifactAgentDock, type ArtifactAgentDockProps, type ArtifactAgentDockTransport, type ArtifactDockMessage, type ArtifactDockStreamEvent, type ArtifactKind, type ArtifactScope, DEFAULT_REASONING_LEVEL_OPTIONS, type ReasoningLevel, type ReasoningLevelOption, ReasoningLevelPicker, type ReasoningLevelPickerProps, createFetchTransport };
229
+ export { AgentSessionControls, type AgentSessionControlsProps, type AgentSessionHarnessControl, type AgentSessionModelControl, type AgentSessionReasoningControl, ArtifactAgentDock, type ArtifactAgentDockProps, type ArtifactAgentDockTransport, type ArtifactDockMessage, type ArtifactDockStreamEvent, type ArtifactKind, type ArtifactScope, DEFAULT_REASONING_LEVEL_OPTIONS, HARNESS_MODEL_POLICIES, type ReasoningLevel, type ReasoningLevelOption, ReasoningLevelPicker, type ReasoningLevelPickerProps, createFetchTransport, isModelCompatibleWithHarness, modelProvider, snapHarnessToModel, snapModelToHarness };
package/dist/chat.js CHANGED
@@ -6,12 +6,17 @@ import {
6
6
  ChatInput,
7
7
  ChatMessage,
8
8
  DEFAULT_REASONING_LEVEL_OPTIONS,
9
+ HARNESS_MODEL_POLICIES,
9
10
  MessageList,
10
11
  ReasoningLevelPicker,
11
12
  ThinkingIndicator,
12
13
  UserMessage,
13
- createFetchTransport
14
- } from "./chunk-666PYT5K.js";
14
+ createFetchTransport,
15
+ isModelCompatibleWithHarness,
16
+ modelProvider,
17
+ snapHarnessToModel,
18
+ snapModelToHarness
19
+ } from "./chunk-TAAYDQGM.js";
15
20
  import "./chunk-ESRYVGHF.js";
16
21
  import "./chunk-4KAPMTPU.js";
17
22
  import "./chunk-EI44GEQ5.js";
@@ -23,9 +28,14 @@ export {
23
28
  ChatInput,
24
29
  ChatMessage,
25
30
  DEFAULT_REASONING_LEVEL_OPTIONS,
31
+ HARNESS_MODEL_POLICIES,
26
32
  MessageList,
27
33
  ReasoningLevelPicker,
28
34
  ThinkingIndicator,
29
35
  UserMessage,
30
- createFetchTransport
36
+ createFetchTransport,
37
+ isModelCompatibleWithHarness,
38
+ modelProvider,
39
+ snapHarnessToModel,
40
+ snapModelToHarness
31
41
  };
@@ -1928,9 +1928,9 @@ function CalendarView({
1928
1928
  children: date.getDate()
1929
1929
  }
1930
1930
  ),
1931
- /* @__PURE__ */ jsxs12("div", { className: "flex flex-wrap gap-0.5 w-full", children: [
1931
+ /* @__PURE__ */ jsxs12("div", { className: "flex flex-col gap-0.5 w-full min-w-0", children: [
1932
1932
  dayEvents.slice(0, 3).map(
1933
- (evt) => renderEventChip ? /* @__PURE__ */ jsx12("span", { children: renderEventChip(evt) }, evt.id) : /* @__PURE__ */ jsx12(
1933
+ (evt) => renderEventChip ? /* @__PURE__ */ jsx12("div", { className: "w-full min-w-0", children: renderEventChip(evt) }, evt.id) : /* @__PURE__ */ jsx12(
1934
1934
  "div",
1935
1935
  {
1936
1936
  className: "w-full truncate text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary",
@@ -1,3 +1,7 @@
1
+ import {
2
+ TangleKnot
3
+ } from "./chunk-UHFJXO24.js";
4
+
1
5
  // src/auth/index.ts
2
6
  import {
3
7
  AuthHeader,
@@ -10,18 +14,6 @@ import {
10
14
  import { Button } from "@tangle-network/ui/primitives";
11
15
  import { cn } from "@tangle-network/ui/utils";
12
16
  import { jsx, jsxs } from "react/jsx-runtime";
13
- function TangleMark({ className }) {
14
- return /* @__PURE__ */ jsx(
15
- "svg",
16
- {
17
- className,
18
- viewBox: "0 0 24 24",
19
- fill: "currentColor",
20
- "aria-hidden": "true",
21
- children: /* @__PURE__ */ jsx("path", { d: "M4 5h16v3.2h-6.4V19h-3.2V8.2H4V5z" })
22
- }
23
- );
24
- }
25
17
  function TangleLoginButton({
26
18
  authUrl = "/auth/tangle",
27
19
  variant = "default",
@@ -39,7 +31,7 @@ function TangleLoginButton({
39
31
  },
40
32
  ...props,
41
33
  children: [
42
- /* @__PURE__ */ jsx(TangleMark, { className: "h-5 w-5" }),
34
+ /* @__PURE__ */ jsx(TangleKnot, { size: 20 }),
43
35
  children ?? "Sign in with Tangle"
44
36
  ]
45
37
  }
@@ -0,0 +1,178 @@
1
+ // src/primitives/index.ts
2
+ import {
3
+ Avatar,
4
+ AvatarFallback,
5
+ AvatarImage,
6
+ Badge,
7
+ badgeVariants,
8
+ Button,
9
+ buttonVariants,
10
+ Card,
11
+ CardContent,
12
+ CardDescription,
13
+ CardFooter,
14
+ CardHeader,
15
+ CardTitle,
16
+ CodeBlock,
17
+ CopyButton,
18
+ Dialog,
19
+ DialogClose,
20
+ DialogContent,
21
+ DialogDescription,
22
+ DialogFooter,
23
+ DialogHeader,
24
+ DialogOverlay,
25
+ DialogPortal,
26
+ DialogTitle,
27
+ DialogTrigger,
28
+ DropZone,
29
+ DropdownMenu,
30
+ DropdownMenuCheckboxItem,
31
+ DropdownMenuContent,
32
+ DropdownMenuGroup,
33
+ DropdownMenuItem,
34
+ DropdownMenuLabel,
35
+ DropdownMenuPortal,
36
+ DropdownMenuRadioGroup,
37
+ DropdownMenuRadioItem,
38
+ DropdownMenuSeparator,
39
+ DropdownMenuShortcut,
40
+ DropdownMenuSub,
41
+ DropdownMenuSubContent,
42
+ DropdownMenuSubTrigger,
43
+ DropdownMenuTrigger,
44
+ EmptyState,
45
+ InlineCode,
46
+ Input,
47
+ Label,
48
+ Progress,
49
+ SegmentedControl,
50
+ Select,
51
+ SelectContent,
52
+ SelectGroup,
53
+ SelectItem,
54
+ SelectLabel,
55
+ SelectScrollDownButton,
56
+ SelectScrollUpButton,
57
+ SelectSeparator,
58
+ SelectTrigger,
59
+ SelectValue,
60
+ SidebarDropZone,
61
+ Skeleton,
62
+ SkeletonCard,
63
+ SkeletonTable,
64
+ StatCard,
65
+ Switch,
66
+ Table,
67
+ TableBody,
68
+ TableCaption,
69
+ TableCell,
70
+ TableFooter,
71
+ TableHead,
72
+ TableHeader,
73
+ TableRow,
74
+ Tabs,
75
+ TabsContent,
76
+ TabsList,
77
+ TabsTrigger,
78
+ TerminalCursor,
79
+ TerminalDisplay,
80
+ TerminalInput,
81
+ TerminalLine,
82
+ Textarea,
83
+ ThemeToggle,
84
+ ToastContainer,
85
+ ToastProvider,
86
+ UploadProgress,
87
+ useTheme,
88
+ useToast
89
+ } from "@tangle-network/ui/primitives";
90
+
91
+ export {
92
+ Avatar,
93
+ AvatarFallback,
94
+ AvatarImage,
95
+ Badge,
96
+ badgeVariants,
97
+ Button,
98
+ buttonVariants,
99
+ Card,
100
+ CardContent,
101
+ CardDescription,
102
+ CardFooter,
103
+ CardHeader,
104
+ CardTitle,
105
+ CodeBlock,
106
+ CopyButton,
107
+ Dialog,
108
+ DialogClose,
109
+ DialogContent,
110
+ DialogDescription,
111
+ DialogFooter,
112
+ DialogHeader,
113
+ DialogOverlay,
114
+ DialogPortal,
115
+ DialogTitle,
116
+ DialogTrigger,
117
+ DropZone,
118
+ DropdownMenu,
119
+ DropdownMenuCheckboxItem,
120
+ DropdownMenuContent,
121
+ DropdownMenuGroup,
122
+ DropdownMenuItem,
123
+ DropdownMenuLabel,
124
+ DropdownMenuPortal,
125
+ DropdownMenuRadioGroup,
126
+ DropdownMenuRadioItem,
127
+ DropdownMenuSeparator,
128
+ DropdownMenuShortcut,
129
+ DropdownMenuSub,
130
+ DropdownMenuSubContent,
131
+ DropdownMenuSubTrigger,
132
+ DropdownMenuTrigger,
133
+ EmptyState,
134
+ InlineCode,
135
+ Input,
136
+ Label,
137
+ Progress,
138
+ SegmentedControl,
139
+ Select,
140
+ SelectContent,
141
+ SelectGroup,
142
+ SelectItem,
143
+ SelectLabel,
144
+ SelectScrollDownButton,
145
+ SelectScrollUpButton,
146
+ SelectSeparator,
147
+ SelectTrigger,
148
+ SelectValue,
149
+ SidebarDropZone,
150
+ Skeleton,
151
+ SkeletonCard,
152
+ SkeletonTable,
153
+ StatCard,
154
+ Switch,
155
+ Table,
156
+ TableBody,
157
+ TableCaption,
158
+ TableCell,
159
+ TableFooter,
160
+ TableHead,
161
+ TableHeader,
162
+ TableRow,
163
+ Tabs,
164
+ TabsContent,
165
+ TabsList,
166
+ TabsTrigger,
167
+ TerminalCursor,
168
+ TerminalDisplay,
169
+ TerminalInput,
170
+ TerminalLine,
171
+ Textarea,
172
+ ThemeToggle,
173
+ ToastContainer,
174
+ ToastProvider,
175
+ UploadProgress,
176
+ useTheme,
177
+ useToast
178
+ };
@@ -22,9 +22,13 @@ function useSandboxMetrics({
22
22
  sandboxId,
23
23
  token,
24
24
  enabled = true,
25
- intervalMs = 3e3
25
+ intervalMs = 3e3,
26
+ historyLimit = 120
26
27
  }) {
27
28
  const [metrics, setMetrics] = React.useState(null);
29
+ const [system, setSystem] = React.useState(null);
30
+ const [latency, setLatency] = React.useState(null);
31
+ const [history, setHistory] = React.useState([]);
28
32
  const [loading, setLoading] = React.useState(false);
29
33
  const [error, setError] = React.useState(null);
30
34
  const [lastUpdatedAt, setLastUpdatedAt] = React.useState(null);
@@ -38,6 +42,9 @@ function useSandboxMetrics({
38
42
  sampleRef.current = null;
39
43
  hasLoadedRef.current = false;
40
44
  setMetrics(null);
45
+ setSystem(null);
46
+ setLatency(null);
47
+ setHistory([]);
41
48
  setLastUpdatedAt(null);
42
49
  setError(null);
43
50
  if (sandboxCleared) setLoading(false);
@@ -69,8 +76,8 @@ function useSandboxMetrics({
69
76
  }
70
77
  const data = await res.json();
71
78
  const user = data?.process?.cpuSeconds?.user ?? 0;
72
- const system = data?.process?.cpuSeconds?.system ?? 0;
73
- const cpuSeconds = user + system;
79
+ const system2 = data?.process?.cpuSeconds?.system ?? 0;
80
+ const cpuSeconds = user + system2;
74
81
  const wallMs = Date.now();
75
82
  if (cancelled) return;
76
83
  let cpuPercent = null;
@@ -89,6 +96,23 @@ function useSandboxMetrics({
89
96
  heapUsedBytes: data?.process?.memoryBytes?.heapUsed ?? 0,
90
97
  heapTotalBytes: data?.process?.memoryBytes?.heapTotal ?? 0
91
98
  });
99
+ const sys = data?.system ?? null;
100
+ setSystem(sys);
101
+ setLatency(data?.latency ?? null);
102
+ if (sys) {
103
+ const sample = {
104
+ at: wallMs,
105
+ cpuPercent: sys.cpuPercent,
106
+ memoryUsedBytes: sys.memory?.usedBytes ?? null,
107
+ memoryTotalBytes: sys.memory?.totalBytes ?? null,
108
+ diskUsedBytes: sys.disk?.usedBytes ?? null,
109
+ diskTotalBytes: sys.disk?.totalBytes ?? null
110
+ };
111
+ setHistory((prevHistory) => {
112
+ const next = [...prevHistory, sample];
113
+ return next.length > historyLimit ? next.slice(next.length - historyLimit) : next;
114
+ });
115
+ }
92
116
  setLastUpdatedAt(wallMs);
93
117
  setError(null);
94
118
  hasLoadedRef.current = true;
@@ -113,8 +137,8 @@ function useSandboxMetrics({
113
137
  controller.abort();
114
138
  if (timeoutId !== null) window.clearTimeout(timeoutId);
115
139
  };
116
- }, [apiBaseUrl, sandboxId, token, enabled, intervalMs]);
117
- return { metrics, loading, error, lastUpdatedAt };
140
+ }, [apiBaseUrl, sandboxId, token, enabled, intervalMs, historyLimit]);
141
+ return { metrics, system, latency, history, loading, error, lastUpdatedAt };
118
142
  }
119
143
 
120
144
  // src/hooks/use-session-crud.ts
@@ -2,7 +2,8 @@ import {
2
2
  HARNESS_OPTIONS
3
3
  } from "./chunk-ESRYVGHF.js";
4
4
  import {
5
- ModelPicker
5
+ ModelPicker,
6
+ canonicalModelId
6
7
  } from "./chunk-4KAPMTPU.js";
7
8
  import {
8
9
  cn
@@ -100,13 +101,76 @@ function ReasoningLevelPicker({
100
101
 
101
102
  // src/chat/agent-session-controls.tsx
102
103
  import * as DropdownMenu2 from "@radix-ui/react-dropdown-menu";
103
- import { Bot, ChevronDown as ChevronDown2 } from "lucide-react";
104
+ import { Bot, ChevronDown as ChevronDown2, Lock } from "lucide-react";
105
+
106
+ // src/chat/harness-model-compat.ts
107
+ var HARNESS_MODEL_POLICIES = {
108
+ opencode: { providers: null, preferred: [] },
109
+ "claude-code": {
110
+ providers: ["anthropic"],
111
+ preferred: [
112
+ /^anthropic\/claude-opus-[\d.-]+$/,
113
+ /^anthropic\/claude-sonnet-[\d.-]+$/,
114
+ /^anthropic\//
115
+ ]
116
+ },
117
+ codex: {
118
+ providers: ["openai"],
119
+ // Standard frontier tier first (gpt-N / gpt-N.N), then any gpt
120
+ // variant (mini/nano), then anything OpenAI.
121
+ preferred: [/^openai\/gpt-\d+(\.\d+)?$/, /^openai\/gpt/, /^openai\//]
122
+ },
123
+ amp: { providers: null, preferred: [] },
124
+ "factory-droids": { providers: null, preferred: [] },
125
+ "cli-base": { providers: null, preferred: [] }
126
+ };
127
+ var PROVIDER_PREFERRED_HARNESS = {
128
+ anthropic: "claude-code",
129
+ openai: "codex"
130
+ };
131
+ function modelProvider(modelId) {
132
+ const slash = modelId.indexOf("/");
133
+ return slash > 0 ? modelId.slice(0, slash) : null;
134
+ }
135
+ function isModelCompatibleWithHarness(harness, modelId) {
136
+ const policy = HARNESS_MODEL_POLICIES[harness];
137
+ if (!policy || policy.providers === null) return true;
138
+ const provider = modelProvider(modelId);
139
+ if (!provider) return true;
140
+ return policy.providers.includes(provider);
141
+ }
142
+ var numericDesc = new Intl.Collator(void 0, {
143
+ numeric: true,
144
+ sensitivity: "base"
145
+ });
146
+ function snapModelToHarness(harness, modelId, models) {
147
+ if (isModelCompatibleWithHarness(harness, modelId)) return modelId;
148
+ const ids = models.map(canonicalModelId);
149
+ const policy = HARNESS_MODEL_POLICIES[harness];
150
+ for (const pattern of policy.preferred) {
151
+ const matches = ids.filter((id) => pattern.test(id)).sort((a, b) => numericDesc.compare(b, a));
152
+ if (matches.length > 0) return matches[0];
153
+ }
154
+ const fallback = ids.find(
155
+ (id) => isModelCompatibleWithHarness(harness, id)
156
+ );
157
+ return fallback ?? modelId;
158
+ }
159
+ function snapHarnessToModel(harness, modelId) {
160
+ if (isModelCompatibleWithHarness(harness, modelId)) return harness;
161
+ const provider = modelProvider(modelId);
162
+ return provider && PROVIDER_PREFERRED_HARNESS[provider] || "opencode";
163
+ }
164
+
165
+ // src/chat/agent-session-controls.tsx
104
166
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
105
167
  function HarnessDropdown({
106
168
  value,
107
169
  onChange,
108
170
  available,
109
- disabled
171
+ disabled,
172
+ locked,
173
+ lockReason
110
174
  }) {
111
175
  const allowed = new Set(
112
176
  available ?? HARNESS_OPTIONS.map((h) => h.type)
@@ -118,19 +182,20 @@ function HarnessDropdown({
118
182
  "button",
119
183
  {
120
184
  type: "button",
121
- disabled,
185
+ disabled: disabled || locked,
186
+ title: locked ? lockReason : void 0,
122
187
  className: cn(
123
188
  "inline-flex h-8 items-center gap-1.5 rounded-lg border border-border bg-card px-2.5",
124
189
  "text-xs font-medium text-foreground shadow-sm transition-colors",
125
190
  "hover:border-primary/30 hover:bg-accent/30 focus:outline-none focus:border-primary/40",
126
191
  "data-[state=open]:border-primary/40 data-[state=open]:bg-accent/30",
127
- "disabled:cursor-not-allowed disabled:opacity-50"
192
+ "disabled:cursor-not-allowed disabled:opacity-60"
128
193
  ),
129
194
  "aria-label": "Agent harness",
130
195
  children: [
131
- /* @__PURE__ */ jsx2(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
196
+ locked ? /* @__PURE__ */ jsx2(Lock, { className: "h-3 w-3 text-muted-foreground" }) : /* @__PURE__ */ jsx2(Bot, { className: "h-3.5 w-3.5 text-muted-foreground" }),
132
197
  /* @__PURE__ */ jsx2("span", { children: selected?.label ?? value }),
133
- /* @__PURE__ */ jsx2(ChevronDown2, { className: "h-3.5 w-3.5 text-muted-foreground" })
198
+ !locked && /* @__PURE__ */ jsx2(ChevronDown2, { className: "h-3.5 w-3.5 text-muted-foreground" })
134
199
  ]
135
200
  }
136
201
  ) }),
@@ -177,25 +242,45 @@ function AgentSessionControls({
177
242
  className
178
243
  }) {
179
244
  if (!harness && !model && !reasoning && !trailing) return null;
245
+ const handleHarnessChange = (next) => {
246
+ harness?.onChange(next);
247
+ if (model) {
248
+ const snapped = snapModelToHarness(next, model.value, model.models);
249
+ if (snapped !== model.value) model.onChange(snapped);
250
+ }
251
+ };
252
+ const handleModelChange = (nextModelId) => {
253
+ model?.onChange(nextModelId);
254
+ if (harness && !harness.locked) {
255
+ const snapped = snapHarnessToModel(harness.value, nextModelId);
256
+ if (snapped !== harness.value) harness.onChange(snapped);
257
+ }
258
+ };
259
+ const visibleModels = model && harness?.locked ? model.models.filter(
260
+ (entry) => isModelCompatibleWithHarness(
261
+ harness.value,
262
+ canonicalModelId(entry)
263
+ )
264
+ ) : model?.models;
180
265
  return /* @__PURE__ */ jsxs2(
181
266
  "div",
182
267
  {
183
268
  className: cn("flex flex-wrap items-center gap-2", className),
184
269
  "data-testid": "agent-session-controls",
185
270
  children: [
186
- harness && /* @__PURE__ */ jsx2(HarnessDropdown, { ...harness }),
271
+ harness && /* @__PURE__ */ jsx2(HarnessDropdown, { ...harness, onChange: handleHarnessChange }),
187
272
  model && /* @__PURE__ */ jsx2(
188
273
  ModelPicker,
189
274
  {
190
275
  variant: "pill",
191
276
  label: "",
192
277
  value: model.value,
193
- onChange: model.onChange,
194
- models: model.models,
278
+ onChange: handleModelChange,
279
+ models: visibleModels ?? [],
195
280
  loading: model.loading,
196
281
  popular: model.popular,
197
282
  recents: model.recents,
198
- disabled: model.disabled || model.models.length === 0
283
+ disabled: model.disabled || (visibleModels ?? []).length === 0
199
284
  }
200
285
  ),
201
286
  reasoning && /* @__PURE__ */ jsx2(
@@ -554,6 +639,11 @@ function createFetchTransport(opts) {
554
639
  export {
555
640
  DEFAULT_REASONING_LEVEL_OPTIONS,
556
641
  ReasoningLevelPicker,
642
+ HARNESS_MODEL_POLICIES,
643
+ modelProvider,
644
+ isModelCompatibleWithHarness,
645
+ snapModelToHarness,
646
+ snapHarnessToModel,
557
647
  AgentSessionControls,
558
648
  ArtifactAgentDock,
559
649
  createFetchTransport,