@chrysb/alphaclaw 0.9.15 → 0.9.17

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 (37) hide show
  1. package/lib/public/css/tailwind.generated.css +1 -1
  2. package/lib/public/dist/app.bundle.js +2168 -2103
  3. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +59 -7
  4. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +124 -0
  5. package/lib/public/js/components/envars.js +1 -1
  6. package/lib/public/js/components/file-tree.js +82 -6
  7. package/lib/public/js/components/row-accessory-select.js +52 -0
  8. package/lib/public/js/lib/api.js +11 -1
  9. package/lib/public/js/lib/model-catalog.js +6 -0
  10. package/lib/public/js/lib/model-config.js +12 -7
  11. package/lib/public/js/lib/thinking-levels.js +37 -0
  12. package/lib/server/agents/agents.js +33 -7
  13. package/lib/server/agents/channels.js +4 -2
  14. package/lib/server/chat-ws.js +4 -1
  15. package/lib/server/constants.js +25 -0
  16. package/lib/server/cost-utils.js +2 -0
  17. package/lib/server/db/auth/index.js +147 -0
  18. package/lib/server/db/auth/schema.js +17 -0
  19. package/lib/server/gateway.js +158 -19
  20. package/lib/server/helpers.js +1 -3
  21. package/lib/server/init/register-server-routes.js +37 -18
  22. package/lib/server/init/runtime-init.js +4 -0
  23. package/lib/server/init/server-lifecycle.js +1 -24
  24. package/lib/server/login-throttle.js +242 -60
  25. package/lib/server/model-catalog-bootstrap.json +5 -0
  26. package/lib/server/onboarding/index.js +2 -2
  27. package/lib/server/openclaw-thinking.js +103 -0
  28. package/lib/server/openclaw-version.js +1 -1
  29. package/lib/server/routes/agents.js +10 -3
  30. package/lib/server/routes/browse/constants.js +3 -1
  31. package/lib/server/routes/browse/index.js +39 -11
  32. package/lib/server/routes/models.js +35 -1
  33. package/lib/server/routes/onboarding.js +2 -2
  34. package/lib/server/routes/system.js +2 -2
  35. package/lib/server/usage-tracker-config.js +52 -1
  36. package/lib/server.js +26 -22
  37. package/package.json +2 -2
@@ -3,6 +3,7 @@ import htm from "htm";
3
3
  import { Badge } from "../../badge.js";
4
4
  import { LoadingSpinner } from "../../loading-spinner.js";
5
5
  import { OverflowMenu, OverflowMenuItem } from "../../overflow-menu.js";
6
+ import { RowAccessorySelect } from "../../row-accessory-select.js";
6
7
  import {
7
8
  getModelDisplayLabel,
8
9
  SearchableModelPicker,
@@ -22,16 +23,25 @@ export const AgentModelCard = ({
22
23
  canEditModel,
23
24
  effectiveModel,
24
25
  effectiveModelEntry,
26
+ formatInheritedThinkingLabel,
25
27
  handleClearModelOverride,
26
28
  handleSelectModel,
29
+ handleSelectThinkingDefault,
27
30
  hasDistinctModelOverride,
31
+ hasDistinctThinkingOverride,
32
+ inheritedThinkingDefault,
28
33
  loading,
29
34
  menuOpen,
30
35
  modelEntries,
31
36
  popularModels,
32
37
  remainingModelOptions,
33
38
  setMenuOpen,
39
+ showThinkingSelect,
40
+ thinkingOptionsLoading,
41
+ thinkingSelectOptions,
42
+ thinkingSelectValue,
34
43
  updatingModel,
44
+ updatingThinking,
35
45
  } = useModelCard({
36
46
  agent,
37
47
  onUpdateAgent,
@@ -63,7 +73,19 @@ export const AgentModelCard = ({
63
73
  handleClearModelOverride();
64
74
  }}
65
75
  >
66
- Inherit from defaults
76
+ Inherit model from defaults
77
+ </${OverflowMenuItem}>
78
+ `
79
+ : null}
80
+ ${hasDistinctThinkingOverride
81
+ ? html`
82
+ <${OverflowMenuItem}
83
+ onClick=${() => {
84
+ setMenuOpen(false);
85
+ handleSelectThinkingDefault("");
86
+ }}
87
+ >
88
+ Inherit thinking from defaults
67
89
  </${OverflowMenuItem}>
68
90
  `
69
91
  : null}
@@ -93,17 +115,20 @@ export const AgentModelCard = ({
93
115
  </p>`
94
116
  : html`
95
117
  <div class="space-y-1">
96
- ${modelEntries.map(
97
- (entry) => html`
118
+ ${modelEntries.map((entry) => {
119
+ const isPrimary = entry.key === effectiveModel;
120
+ const showThinkingPicker =
121
+ isPrimary && showThinkingSelect && !thinkingOptionsLoading;
122
+ return html`
98
123
  <div
99
124
  key=${entry.key}
100
- class="flex items-center justify-between py-1"
125
+ class="flex items-center justify-between gap-3 py-1"
101
126
  >
102
127
  <div class="flex items-center gap-2 min-w-0">
103
128
  <span class="text-sm text-body truncate">
104
129
  ${getModelDisplayLabel(entry)}
105
130
  </span>
106
- ${entry.key === effectiveModel
131
+ ${isPrimary
107
132
  ? html`<${Badge} tone="cyan">Primary</${Badge}>`
108
133
  : html`
109
134
  <button
@@ -115,9 +140,36 @@ export const AgentModelCard = ({
115
140
  </button>
116
141
  `}
117
142
  </div>
143
+ ${showThinkingPicker
144
+ ? html`
145
+ <${RowAccessorySelect}
146
+ ariaLabel="Agent thinking level"
147
+ title="Agent thinking level"
148
+ value=${thinkingSelectValue}
149
+ disabled=${saving ||
150
+ updatingModel ||
151
+ updatingThinking ||
152
+ !canEditModel}
153
+ onChange=${handleSelectThinkingDefault}
154
+ >
155
+ <option value="">
156
+ ${formatInheritedThinkingLabel(
157
+ inheritedThinkingDefault,
158
+ )}
159
+ </option>
160
+ ${thinkingSelectOptions.map(
161
+ (option) => html`
162
+ <option value=${option.value}>
163
+ ${option.label}
164
+ </option>
165
+ `,
166
+ )}
167
+ </${RowAccessorySelect}>
168
+ `
169
+ : null}
118
170
  </div>
119
- `,
120
- )}
171
+ `;
172
+ })}
121
173
  </div>
122
174
  `}
123
175
  ${loading
@@ -1,4 +1,10 @@
1
1
  import { useEffect, useMemo, useState } from "preact/hooks";
2
+ import { fetchThinkingOptions } from "../../../lib/api.js";
3
+ import {
4
+ formatInheritedThinkingLabel,
5
+ formatThinkingLevelLabel,
6
+ shouldShowThinkingLevelSelect,
7
+ } from "../../../lib/thinking-levels.js";
2
8
  import { useModels } from "../../models-tab/use-models.js";
3
9
  import {
4
10
  buildProviderHasAuth,
@@ -25,7 +31,14 @@ export const useModelCard = ({
25
31
  onUpdateAgent = async () => {},
26
32
  }) => {
27
33
  const [updatingModel, setUpdatingModel] = useState(false);
34
+ const [updatingThinking, setUpdatingThinking] = useState(false);
28
35
  const [menuOpen, setMenuOpen] = useState(false);
36
+ const [thinkingOptions, setThinkingOptions] = useState({
37
+ levels: [],
38
+ inheritedDefault: "off",
39
+ modelDefault: "off",
40
+ });
41
+ const [thinkingOptionsLoading, setThinkingOptionsLoading] = useState(false);
29
42
  const {
30
43
  catalog,
31
44
  primary: defaultPrimaryModel,
@@ -41,6 +54,45 @@ export const useModelCard = ({
41
54
  const hasDistinctModelOverride =
42
55
  !!explicitModel &&
43
56
  String(explicitModel).trim() !== String(defaultPrimaryModel || "").trim();
57
+ const explicitThinkingDefault = String(agent.thinkingDefault || "").trim();
58
+ const inheritedThinkingDefault = String(
59
+ thinkingOptions.inheritedDefault || thinkingOptions.modelDefault || "off",
60
+ ).trim();
61
+ const hasDistinctThinkingOverride =
62
+ !!explicitThinkingDefault &&
63
+ explicitThinkingDefault !== inheritedThinkingDefault;
64
+ const showThinkingSelect = shouldShowThinkingLevelSelect(
65
+ thinkingOptions.levels,
66
+ );
67
+
68
+ useEffect(() => {
69
+ const modelKey = String(effectiveModel || "").trim();
70
+ if (!modelKey.includes("/")) {
71
+ setThinkingOptions({
72
+ levels: [],
73
+ inheritedDefault: "off",
74
+ modelDefault: "off",
75
+ });
76
+ return undefined;
77
+ }
78
+ let cancelled = false;
79
+ setThinkingOptionsLoading(true);
80
+ fetchThinkingOptions(modelKey)
81
+ .then((payload) => {
82
+ if (cancelled || !payload?.ok) return;
83
+ setThinkingOptions({
84
+ levels: Array.isArray(payload.levels) ? payload.levels : [],
85
+ inheritedDefault: String(payload.inheritedDefault || "off").trim(),
86
+ modelDefault: String(payload.modelDefault || "off").trim(),
87
+ });
88
+ })
89
+ .finally(() => {
90
+ if (!cancelled) setThinkingOptionsLoading(false);
91
+ });
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ }, [effectiveModel]);
44
96
 
45
97
  const providerHasAuth = useMemo(
46
98
  () => buildProviderHasAuth({ authProfiles, codexStatus }),
@@ -149,6 +201,69 @@ export const useModelCard = ({
149
201
  }
150
202
  };
151
203
 
204
+ const handleSelectThinkingDefault = async (nextValue) => {
205
+ const normalizedValue = String(nextValue || "").trim();
206
+ const isInherit = !normalizedValue;
207
+ if (isInherit) {
208
+ if (!hasDistinctThinkingOverride) return;
209
+ setUpdatingThinking(true);
210
+ try {
211
+ await onUpdateAgent(
212
+ String(agent.id || "").trim(),
213
+ { thinkingDefault: null },
214
+ "Agent thinking level reset to default",
215
+ );
216
+ } finally {
217
+ setUpdatingThinking(false);
218
+ }
219
+ return;
220
+ }
221
+ if (normalizedValue === explicitThinkingDefault) return;
222
+ setUpdatingThinking(true);
223
+ try {
224
+ await onUpdateAgent(
225
+ String(agent.id || "").trim(),
226
+ { thinkingDefault: normalizedValue },
227
+ "Agent thinking level updated",
228
+ );
229
+ } finally {
230
+ setUpdatingThinking(false);
231
+ }
232
+ };
233
+
234
+ const thinkingSelectValue = hasDistinctThinkingOverride
235
+ ? explicitThinkingDefault
236
+ : "";
237
+ const thinkingSelectOptions = useMemo(() => {
238
+ const seen = new Set();
239
+ const options = [];
240
+ const addOption = (value, label) => {
241
+ const normalizedValue = String(value || "").trim();
242
+ if (!normalizedValue || seen.has(normalizedValue)) return;
243
+ seen.add(normalizedValue);
244
+ options.push({
245
+ value: normalizedValue,
246
+ label: String(label || formatThinkingLevelLabel(normalizedValue)).trim(),
247
+ });
248
+ };
249
+ for (const entry of thinkingOptions.levels) {
250
+ addOption(
251
+ entry?.id,
252
+ formatThinkingLevelLabel(entry?.label || entry?.id),
253
+ );
254
+ }
255
+ if (
256
+ explicitThinkingDefault &&
257
+ !seen.has(explicitThinkingDefault)
258
+ ) {
259
+ addOption(
260
+ explicitThinkingDefault,
261
+ `${formatThinkingLevelLabel(explicitThinkingDefault)} (custom)`,
262
+ );
263
+ }
264
+ return options;
265
+ }, [explicitThinkingDefault, thinkingOptions.levels]);
266
+
152
267
  return {
153
268
  authorizedModelOptions,
154
269
  canEditModel: modelsReady && !loadingModels,
@@ -156,13 +271,22 @@ export const useModelCard = ({
156
271
  effectiveModelEntry,
157
272
  handleClearModelOverride,
158
273
  handleSelectModel,
274
+ handleSelectThinkingDefault,
159
275
  hasDistinctModelOverride,
276
+ hasDistinctThinkingOverride,
277
+ inheritedThinkingDefault,
160
278
  loading: !modelsReady || loadingModels,
161
279
  menuOpen,
162
280
  modelEntries,
163
281
  popularModels,
164
282
  remainingModelOptions,
165
283
  setMenuOpen,
284
+ showThinkingSelect,
285
+ thinkingOptionsLoading,
286
+ thinkingSelectOptions,
287
+ thinkingSelectValue,
288
+ formatInheritedThinkingLabel,
166
289
  updatingModel,
290
+ updatingThinking,
167
291
  };
168
292
  };
@@ -58,7 +58,7 @@ const normalizeEnvVarKey = (raw) =>
58
58
  .toUpperCase()
59
59
  .replace(/[^A-Z0-9_]/g, "_");
60
60
  const kManagedChannelTokenPattern =
61
- /^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN)(?:_[A-Z0-9_]+)?$/;
61
+ /^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN|WHATSAPP_OWNER_NUMBER)(?:_[A-Z0-9_]+)?$/;
62
62
  const stripSurroundingQuotes = (raw) => {
63
63
  const value = String(raw || "").trim();
64
64
  if (value.length < 2) return value;
@@ -76,6 +76,16 @@ const collectFolderPaths = (node, folderPaths) => {
76
76
  );
77
77
  };
78
78
 
79
+ const collectTruncatedExpandedFolderPaths = (node, expandedPaths, folderPaths) => {
80
+ if (!node || node.type !== "folder") return;
81
+ if (node.truncated && expandedPaths.has(node.path || "")) {
82
+ folderPaths.push(node.path || "");
83
+ }
84
+ (node.children || []).forEach((childNode) =>
85
+ collectTruncatedExpandedFolderPaths(childNode, expandedPaths, folderPaths),
86
+ );
87
+ };
88
+
79
89
  const collectFilePaths = (node, filePaths) => {
80
90
  if (!node) return;
81
91
  if (node.type === "file") {
@@ -104,6 +114,19 @@ const removeTreePath = (node, targetPath) => {
104
114
  };
105
115
  };
106
116
 
117
+ const replaceTreeNode = (node, nextNode) => {
118
+ if (!node || !nextNode) return node;
119
+ if (String(node.path || "") === String(nextNode.path || "")) return nextNode;
120
+ if (node.type !== "folder") return node;
121
+ const nextChildren = (node.children || []).map((childNode) =>
122
+ replaceTreeNode(childNode, nextNode),
123
+ );
124
+ return {
125
+ ...node,
126
+ children: nextChildren,
127
+ };
128
+ };
129
+
107
130
  const filterTreeNode = (node, normalizedQuery) => {
108
131
  if (!node) return null;
109
132
  const query = String(normalizedQuery || "")
@@ -410,6 +433,7 @@ const TreeNode = ({
410
433
  onCreationConfirm,
411
434
  onCreationCancel,
412
435
  dragSourcePath = "",
436
+ loadingFolderPaths = new Set(),
413
437
  }) => {
414
438
  if (!node) return null;
415
439
  if (node.type === "file") {
@@ -471,6 +495,7 @@ const TreeNode = ({
471
495
 
472
496
  const folderPath = node.path || "";
473
497
  const isCollapsed = isSearchActive ? false : !expandedPaths.has(folderPath);
498
+ const isLoadingFolder = loadingFolderPaths.has(folderPath);
474
499
  const isFolderActive = selectedPath === folderPath;
475
500
  const isFolderLocked = folderPath && matchesBrowsePolicyPath(
476
501
  kLockedBrowsePaths,
@@ -484,7 +509,7 @@ const TreeNode = ({
484
509
  class=${`tree-folder ${isCollapsed ? "collapsed" : ""} ${isFolderActive ? "active" : ""} ${isDropTarget ? "is-drop-target" : ""}`.trim()}
485
510
  onclick=${() => {
486
511
  if (!folderPath) return;
487
- onSetFolderExpanded(folderPath, isCollapsed);
512
+ onSetFolderExpanded(folderPath, isCollapsed, node);
488
513
  onSelectFolder(folderPath);
489
514
  }}
490
515
  oncontextmenu=${(e) => {
@@ -540,8 +565,9 @@ const TreeNode = ({
540
565
  event.preventDefault();
541
566
  event.stopPropagation();
542
567
  if (!folderPath) return;
543
- onSetFolderExpanded(folderPath, isCollapsed);
568
+ onSetFolderExpanded(folderPath, isCollapsed, node);
544
569
  }}
570
+ disabled=${isLoadingFolder}
545
571
  >
546
572
  <span class="arrow">▼</span>
547
573
  </button>
@@ -587,6 +613,7 @@ const TreeNode = ({
587
613
  onCreationConfirm=${onCreationConfirm}
588
614
  onCreationCancel=${onCreationCancel}
589
615
  dragSourcePath=${dragSourcePath}
616
+ loadingFolderPaths=${loadingFolderPaths}
590
617
  />
591
618
  `,
592
619
  )}
@@ -623,6 +650,7 @@ const TreeNode = ({
623
650
  onCreationConfirm=${onCreationConfirm}
624
651
  onCreationCancel=${onCreationCancel}
625
652
  dragSourcePath=${dragSourcePath}
653
+ loadingFolderPaths=${loadingFolderPaths}
626
654
  />
627
655
  `,
628
656
  )}
@@ -650,6 +678,7 @@ export const FileTree = ({
650
678
  const [creatingType, setCreatingType] = useState("");
651
679
  const [contextMenu, setContextMenu] = useState(null);
652
680
  const [dragSourcePath, setDragSourcePath] = useState("");
681
+ const [loadingFolderPaths, setLoadingFolderPaths] = useState(new Set());
653
682
  const [selectedFolder, setSelectedFolder] = useState("");
654
683
  const effectiveSelectedPath = selectedFolder || selectedPath;
655
684
  const searchInputRef = useRef(null);
@@ -661,10 +690,31 @@ export const FileTree = ({
661
690
  try {
662
691
  const data = await fetchBrowseTree();
663
692
  const nextRoot = data.root || null;
664
- const nextSignature = JSON.stringify(nextRoot || {});
693
+ const nextExpandedPaths =
694
+ expandedPaths instanceof Set ? expandedPaths : new Set();
695
+ let hydratedRoot = nextRoot;
696
+ const hydratedPaths = new Set();
697
+ while (true) {
698
+ const truncatedExpandedPaths = [];
699
+ collectTruncatedExpandedFolderPaths(
700
+ hydratedRoot,
701
+ nextExpandedPaths,
702
+ truncatedExpandedPaths,
703
+ );
704
+ const nextFolderPath = truncatedExpandedPaths.find(
705
+ (folderPath) => !hydratedPaths.has(folderPath),
706
+ );
707
+ if (!nextFolderPath) break;
708
+ hydratedPaths.add(nextFolderPath);
709
+ const subtreeData = await fetchBrowseTree({ path: nextFolderPath });
710
+ if (subtreeData.root) {
711
+ hydratedRoot = replaceTreeNode(hydratedRoot, subtreeData.root);
712
+ }
713
+ }
714
+ const nextSignature = JSON.stringify(hydratedRoot || {});
665
715
  if (treeSignatureRef.current !== nextSignature) {
666
716
  treeSignatureRef.current = nextSignature;
667
- setTreeRoot(nextRoot);
717
+ setTreeRoot(hydratedRoot);
668
718
  }
669
719
  setExpandedPaths((previousPaths) =>
670
720
  previousPaths instanceof Set ? previousPaths : new Set(),
@@ -677,7 +727,7 @@ export const FileTree = ({
677
727
  } finally {
678
728
  if (showLoading) setLoading(false);
679
729
  }
680
- }, []);
730
+ }, [expandedPaths]);
681
731
 
682
732
  useEffect(() => {
683
733
  loadTree({ showLoading: true });
@@ -834,7 +884,31 @@ export const FileTree = ({
834
884
  onPreviewFile("");
835
885
  }, [isSearchActive, filteredFilePaths, searchActivePath, onPreviewFile]);
836
886
 
837
- const setFolderExpanded = (folderPath, nextExpanded) => {
887
+ const setFolderExpanded = async (folderPath, nextExpanded, node = null) => {
888
+ if (nextExpanded === true && node?.truncated) {
889
+ setLoadingFolderPaths((previousPaths) => {
890
+ const nextPaths =
891
+ previousPaths instanceof Set ? new Set(previousPaths) : new Set();
892
+ nextPaths.add(folderPath);
893
+ return nextPaths;
894
+ });
895
+ try {
896
+ const data = await fetchBrowseTree({ path: folderPath });
897
+ if (data.root) {
898
+ setTreeRoot((previousRoot) => replaceTreeNode(previousRoot, data.root));
899
+ }
900
+ } catch (loadError) {
901
+ showToast(loadError.message || "Could not load folder", "error");
902
+ return;
903
+ } finally {
904
+ setLoadingFolderPaths((previousPaths) => {
905
+ const nextPaths =
906
+ previousPaths instanceof Set ? new Set(previousPaths) : new Set();
907
+ nextPaths.delete(folderPath);
908
+ return nextPaths;
909
+ });
910
+ }
911
+ }
838
912
  setExpandedPaths((previousPaths) => {
839
913
  const nextPaths =
840
914
  previousPaths instanceof Set ? new Set(previousPaths) : new Set();
@@ -1210,6 +1284,7 @@ export const FileTree = ({
1210
1284
  onCreationConfirm=${confirmCreate}
1211
1285
  onCreationCancel=${cancelCreate}
1212
1286
  dragSourcePath=${dragSourcePath}
1287
+ loadingFolderPaths=${loadingFolderPaths}
1213
1288
  />
1214
1289
  `,
1215
1290
  )}
@@ -1245,6 +1320,7 @@ export const FileTree = ({
1245
1320
  onCreationConfirm=${confirmCreate}
1246
1321
  onCreationCancel=${cancelCreate}
1247
1322
  dragSourcePath=${dragSourcePath}
1323
+ loadingFolderPaths=${loadingFolderPaths}
1248
1324
  />
1249
1325
  `,
1250
1326
  )}
@@ -0,0 +1,52 @@
1
+ import { h } from "preact";
2
+ import htm from "htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ const RowAccessoryChevron = () => html`
7
+ <svg
8
+ width="14"
9
+ height="14"
10
+ viewBox="0 0 16 16"
11
+ fill="none"
12
+ class="text-fg-dim"
13
+ aria-hidden="true"
14
+ >
15
+ <path
16
+ d="M3.5 6L8 10.5L12.5 6"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ />
22
+ </svg>
23
+ `;
24
+
25
+ export const RowAccessorySelect = ({
26
+ ariaLabel = "",
27
+ title = "",
28
+ value = "",
29
+ disabled = false,
30
+ onChange = () => {},
31
+ children = null,
32
+ }) => html`
33
+ <label
34
+ class=${`relative inline-flex shrink-0 items-center justify-end max-w-[12rem] min-w-[5.5rem] ${disabled
35
+ ? "opacity-50 cursor-not-allowed"
36
+ : "cursor-pointer"}`}
37
+ >
38
+ <select
39
+ aria-label=${ariaLabel}
40
+ title=${title || ariaLabel}
41
+ value=${value}
42
+ disabled=${disabled}
43
+ onInput=${(event) => onChange(String(event.currentTarget?.value ?? ""))}
44
+ class="appearance-none bg-transparent border-0 py-0 pl-0 pr-5 w-full text-right text-xs text-fg-muted hover:text-body cursor-pointer focus:outline-none focus-visible:ring-1 focus-visible:ring-border rounded disabled:cursor-not-allowed truncate"
45
+ >
46
+ ${children}
47
+ </select>
48
+ <span class="pointer-events-none absolute right-0 top-1/2 -translate-y-1/2">
49
+ <${RowAccessoryChevron} />
50
+ </span>
51
+ </label>
52
+ `;
@@ -857,6 +857,13 @@ export const fetchModelStatus = async () => {
857
857
  return res.json();
858
858
  };
859
859
 
860
+ export const fetchThinkingOptions = async (modelKey) => {
861
+ const normalized = String(modelKey || "").trim();
862
+ const qs = new URLSearchParams({ modelKey: normalized });
863
+ const res = await authFetch(`/api/models/thinking-options?${qs.toString()}`);
864
+ return res.json();
865
+ };
866
+
860
867
  export const setPrimaryModel = async (modelKey) => {
861
868
  const res = await authFetch("/api/models/set", {
862
869
  method: "POST",
@@ -1245,8 +1252,11 @@ export async function fetchWebhookRequest(name, id) {
1245
1252
  return parseJsonOrThrow(res, "Could not load webhook request");
1246
1253
  }
1247
1254
 
1248
- export const fetchBrowseTree = async (depth = 10) => {
1255
+ export const fetchBrowseTree = async (options = {}) => {
1256
+ const { depth = 3, path = "" } =
1257
+ typeof options === "number" ? { depth: options, path: "" } : options;
1249
1258
  const params = new URLSearchParams({ depth: String(depth) });
1259
+ if (path) params.set("path", String(path));
1250
1260
  const res = await authFetch(`/api/browse/tree?${params.toString()}`);
1251
1261
  return parseJsonOrThrow(res, "Could not load file tree");
1252
1262
  };
@@ -4,6 +4,7 @@ import { getFeaturedModels } from "./model-config.js";
4
4
 
5
5
  export const kModelCatalogCacheKey = "/api/models";
6
6
  export const kModelCatalogPollIntervalMs = 3000;
7
+ export const kDefaultOnboardingModelKey = "anthropic/claude-opus-4-8";
7
8
 
8
9
  export const getModelCatalogModels = (payload) =>
9
10
  Array.isArray(payload?.models) ? payload.models : [];
@@ -26,6 +27,11 @@ export const getInitialOnboardingModelKey = ({
26
27
  } = {}) => {
27
28
  const normalizedCurrent = String(currentModelKey || "").trim();
28
29
  if (normalizedCurrent) return normalizedCurrent;
30
+ const catalogHasKey = (key) =>
31
+ catalog.some((model) => String(model?.key || "") === key);
32
+ if (catalogHasKey(kDefaultOnboardingModelKey)) {
33
+ return kDefaultOnboardingModelKey;
34
+ }
29
35
  const featuredModels = getFeaturedModels(catalog);
30
36
  return String(featuredModels[0]?.key || catalog[0]?.key || "");
31
37
  };
@@ -9,6 +9,10 @@ export const getAuthProviderFromModelProvider = (provider) => {
9
9
  };
10
10
 
11
11
  export const kFeaturedModelDefs = [
12
+ {
13
+ label: "Opus 4.8",
14
+ preferredKeys: ["anthropic/claude-opus-4-8"],
15
+ },
12
16
  {
13
17
  label: "Opus 4.7",
14
18
  preferredKeys: ["anthropic/claude-opus-4-7"],
@@ -58,13 +62,14 @@ export const kProviderAuthFields = {
58
62
  linkText: "Get key",
59
63
  placeholder: "sk-ant-...",
60
64
  },
61
- {
62
- key: "ANTHROPIC_TOKEN",
63
- label: "Anthropic Setup Token",
64
- hint: "From claude setup-token (uses your Claude subscription)",
65
- linkText: "Get token",
66
- placeholder: "Token...",
67
- },
65
+ // Temporarily hidden — setup-token flow is not supported in onboarding yet.
66
+ // {
67
+ // key: "ANTHROPIC_TOKEN",
68
+ // label: "Anthropic Setup Token",
69
+ // hint: "From claude setup-token (uses your Claude subscription)",
70
+ // linkText: "Get token",
71
+ // placeholder: "Token...",
72
+ // },
68
73
  ],
69
74
  openai: [
70
75
  {
@@ -0,0 +1,37 @@
1
+ const kThinkingLevelLabelOverrides = {
2
+ off: "Off",
3
+ on: "On",
4
+ minimal: "Minimal",
5
+ low: "Low",
6
+ medium: "Medium",
7
+ high: "High",
8
+ adaptive: "Adaptive",
9
+ xhigh: "Extra high",
10
+ max: "Maximum",
11
+ };
12
+
13
+ export const formatThinkingLevelLabel = (levelId = "") => {
14
+ const normalized = String(levelId || "").trim().toLowerCase();
15
+ if (!normalized) return "";
16
+ if (kThinkingLevelLabelOverrides[normalized]) {
17
+ return kThinkingLevelLabelOverrides[normalized];
18
+ }
19
+ return normalized
20
+ .split(/[-_]/g)
21
+ .filter(Boolean)
22
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
23
+ .join(" ");
24
+ };
25
+
26
+ export const formatInheritedThinkingLabel = (levelId = "") => {
27
+ const label = formatThinkingLevelLabel(levelId);
28
+ return label ? `Inherited: ${label}` : "Inherited";
29
+ };
30
+
31
+ export const shouldShowThinkingLevelSelect = (levels = []) => {
32
+ const normalized = (Array.isArray(levels) ? levels : [])
33
+ .map((entry) => String(entry?.id || entry || "").trim().toLowerCase())
34
+ .filter(Boolean);
35
+ if (normalized.length === 0) return false;
36
+ return !(normalized.length === 1 && normalized[0] === "off");
37
+ };