@circuitwall/jarela 2.2.0 → 2.2.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 (86) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +2 -2
  3. package/.next/standalone/.next/build-manifest.json +2 -2
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/api/v1/models/[name]/route.js +61 -20
  19. package/.next/standalone/.next/server/app/api/v1/models/route.js +5 -3
  20. package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +27 -24
  21. package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/page.js +313 -251
  23. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  24. package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  25. package/.next/standalone/.next/server/app-paths-manifest.json +2 -2
  26. package/.next/standalone/.next/server/chunks/2471.js +17 -17
  27. package/.next/standalone/.next/server/chunks/2563.js +24 -50
  28. package/.next/standalone/.next/server/chunks/3399.js +10695 -10695
  29. package/.next/standalone/.next/server/chunks/3524.js +5 -33
  30. package/.next/standalone/.next/server/chunks/424.js +10 -5
  31. package/.next/standalone/.next/server/chunks/4508.js +2 -2
  32. package/.next/standalone/.next/server/chunks/46.js +111 -46
  33. package/.next/standalone/.next/server/chunks/466.js +88 -13
  34. package/.next/standalone/.next/server/chunks/6649.js +1 -1
  35. package/.next/standalone/.next/server/chunks/9590.js +108 -46
  36. package/.next/standalone/.next/server/chunks/9640.js +3 -6
  37. package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
  38. package/.next/standalone/.next/server/middleware.js +104 -45
  39. package/.next/standalone/.next/server/pages/404.html +1 -1
  40. package/.next/standalone/.next/server/pages/500.html +1 -1
  41. package/.next/standalone/.next/server/preview-props.json +3 -3
  42. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  43. package/.next/standalone/.next/static/chunks/{1395-f201e3e2e1f57715.js → 1395-a340fb0a345382a4.js} +89 -14
  44. package/.next/standalone/.next/static/chunks/{4576-13f7546a145a3624.js → 7045-8bc2f14e430ca661.js} +58 -2
  45. package/.next/standalone/.next/static/chunks/app/{page-c6e972a7fd13dae8.js → page-7017f07f8a7a083c.js} +170 -151
  46. package/.next/standalone/package.json +1 -1
  47. package/CHANGELOG.md +8 -0
  48. package/api/client.models.test.ts +31 -0
  49. package/api/client.ts +16 -1
  50. package/api/types.ts +8 -0
  51. package/app/api/v1/models/[name]/route.test.ts +89 -0
  52. package/app/api/v1/models/[name]/route.ts +54 -3
  53. package/app/api/v1/models/route.ts +4 -2
  54. package/app/api/v1/providers/[provider]/models/route.test.ts +53 -0
  55. package/app/api/v1/providers/[provider]/models/route.ts +27 -16
  56. package/components/agents/agent-editor/AdvancedSection.tsx +8 -2
  57. package/components/agents/agent-editor/ModelSection.tsx +6 -3
  58. package/components/models/ModelEditor.tsx +1 -1
  59. package/components/models/ModelsPanel.tsx +34 -8
  60. package/components/models/model-editor/GitHubCopilotAuth.tsx +1 -1
  61. package/components/models/model-editor/save-helpers.ts +2 -2
  62. package/components/models/model-editor/useModelEditorForm.ts +1 -1
  63. package/components/models/model-editor/useModelSaveHandlers.ts +1 -1
  64. package/components/setup/wizard/StepCredentials.tsx +1 -1
  65. package/components/setup/wizard/StepModel.tsx +3 -8
  66. package/hooks/useModels.ts +17 -1
  67. package/lib/agents/effective-model.test.ts +45 -0
  68. package/lib/agents/effective-model.ts +11 -5
  69. package/lib/agents/llm.ts +2 -2
  70. package/lib/agents/run-thread.ts +10 -5
  71. package/lib/agents/warm-summary-background.test.ts +1 -2
  72. package/lib/agents/warm-summary-background.ts +4 -7
  73. package/lib/db/migrations.ts +10 -0
  74. package/lib/health/probes.test.ts +8 -11
  75. package/lib/health/probes.ts +5 -31
  76. package/lib/models/editor-payload.ts +1 -1
  77. package/lib/providers/github-copilot-auth.ts +2 -3
  78. package/lib/providers/github-copilot.test.ts +27 -0
  79. package/lib/providers/github-copilot.ts +21 -44
  80. package/lib/stores/integrations.test.ts +7 -0
  81. package/lib/stores/integrations.ts +4 -4
  82. package/lib/stores/model-config.test.ts +88 -0
  83. package/lib/stores/model-config.ts +90 -8
  84. package/package.json +1 -1
  85. /package/.next/standalone/.next/static/{e2NV1wk6mWg3SIeELXevg → NUrC9xax1sp1Q70nlFqS6}/_buildManifest.js +0 -0
  86. /package/.next/standalone/.next/static/{e2NV1wk6mWg3SIeELXevg → NUrC9xax1sp1Q70nlFqS6}/_ssgManifest.js +0 -0
@@ -1,105 +1,6 @@
1
1
  (self["webpackChunk_N_E"] = self["webpackChunk_N_E"] || []).push([[8974],{
2
2
 
3
- /***/ 30737:
4
- /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
5
-
6
- "use strict";
7
- /* harmony export */ __webpack_require__.d(__webpack_exports__, {
8
- /* harmony export */ D: () => (/* binding */ useTheme),
9
- /* harmony export */ ThemeProvider: () => (/* binding */ ThemeProvider)
10
- /* harmony export */ });
11
- /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(95155);
12
- /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12115);
13
- /* __next_internal_client_entry_do_not_use__ ThemeProvider,useTheme auto */
14
-
15
- const STORAGE_KEY = "jarela-theme";
16
- function isTheme(v) {
17
- return v === "light" || v === "dark" || v === "system";
18
- }
19
- function readStored() {
20
- if (false) {}
21
- try {
22
- const v = window.localStorage.getItem(STORAGE_KEY);
23
- return isTheme(v) ? v : "system";
24
- } catch {
25
- return "system";
26
- }
27
- }
28
- const LIGHT_CHROME = "#ffffff";
29
- const DARK_CHROME = "#09090b";
30
- function resolveChrome(theme) {
31
- if (theme === "light") return LIGHT_CHROME;
32
- if (theme === "dark") return DARK_CHROME;
33
- if (false) {}
34
- return window.matchMedia("(prefers-color-scheme: dark)").matches ? DARK_CHROME : LIGHT_CHROME;
35
- }
36
- // Keep the single <meta name="theme-color"> tag (installed by the pre-paint
37
- // script in app/layout.tsx) aligned with the active surface so the PWA's
38
- // desktop title bar and mobile address bar match the theme.
39
- function syncChrome(theme) {
40
- if (typeof document === "undefined") return;
41
- const meta = document.querySelector('meta[name="theme-color"]');
42
- if (meta) meta.setAttribute("content", resolveChrome(theme));
43
- }
44
- function apply(theme) {
45
- if (typeof document === "undefined") return;
46
- document.documentElement.setAttribute("data-theme", theme);
47
- syncChrome(theme);
48
- }
49
- const ThemeContext = /*#__PURE__*/ (0,react__WEBPACK_IMPORTED_MODULE_1__.createContext)(null);
50
- function ThemeProvider({ children }) {
51
- // Initial state stays "system" on the server to match the pre-paint script,
52
- // which writes data-theme before React hydrates. The effect below syncs the
53
- // React state to whatever the script (or localStorage) decided.
54
- const [theme, setThemeState] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)("system");
55
- (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
56
- const stored = readStored();
57
- setThemeState(stored);
58
- // When in "system" mode, mirror OS-level changes into the PWA chrome.
59
- if (false) {}
60
- const mq = window.matchMedia("(prefers-color-scheme: dark)");
61
- const onChange = ()=>{
62
- if (readStored() === "system") syncChrome("system");
63
- };
64
- mq.addEventListener?.("change", onChange);
65
- return ()=>mq.removeEventListener?.("change", onChange);
66
- }, []);
67
- const setTheme = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)((t)=>{
68
- setThemeState(t);
69
- apply(t);
70
- try {
71
- window.localStorage.setItem(STORAGE_KEY, t);
72
- } catch {
73
- /* ignore quota / private-mode errors */ }
74
- }, []);
75
- return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(ThemeContext.Provider, {
76
- value: {
77
- theme,
78
- setTheme
79
- },
80
- children: children
81
- });
82
- }
83
- function useTheme() {
84
- const ctx = (0,react__WEBPACK_IMPORTED_MODULE_1__.useContext)(ThemeContext);
85
- if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
86
- return ctx;
87
- }
88
-
89
-
90
- /***/ }),
91
-
92
- /***/ 39852:
93
- /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
94
-
95
- Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 96513));
96
- ;
97
- Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 12283));
98
-
99
-
100
- /***/ }),
101
-
102
- /***/ 96513:
3
+ /***/ 9024:
103
4
  /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
104
5
 
105
6
  "use strict";
@@ -826,40 +727,8 @@ function useUrlSync() {
826
727
  };
827
728
  }
828
729
 
829
- ;// ./lib/agents/effective-model.ts
830
- /**
831
- * Resolve which `ModelConfig` will actually be used for an agent at runtime.
832
- *
833
- * Mirrors what `lib/agents/llm.ts` does server-side: when an agent has
834
- * `model_config_name` set, that wins; otherwise the workspace-wide default
835
- * model is used. Both client (BridgeEditor, AgentEditor capability hints)
836
- * and server need the same logic — this is the single source of truth.
837
- *
838
- * Returns `null` only when neither the requested config nor the default
839
- * exists in the provided list (typically: no models configured yet).
840
- */ function resolveAgentModel(agent, models) {
841
- const defaultModel = models.find((m)=>m.is_default) ?? models[0] ?? null;
842
- if (!agent?.model_config_name) return defaultModel;
843
- return models.find((m)=>m.name === agent.model_config_name) ?? defaultModel;
844
- }
845
- /**
846
- * Diagnose whether an agent has a runnable model RIGHT NOW. Used by the
847
- * Scheduled Tasks and Watchers panels to surface model-availability issues
848
- * before an unattended run silently no-ops with `no_model`.
849
- */ function agentModelStatus(agent, models) {
850
- if (!agent) return {
851
- state: "no-agent"
852
- };
853
- const effectiveModel = resolveAgentModel(agent, models);
854
- if (effectiveModel) return {
855
- state: "ok",
856
- model: effectiveModel
857
- };
858
- return {
859
- state: "no-model"
860
- };
861
- }
862
-
730
+ // EXTERNAL MODULE: ./lib/agents/effective-model.ts
731
+ var effective_model = __webpack_require__(53036);
863
732
  ;// ./hooks/useConfigurationIssues.ts
864
733
  /* __next_internal_client_entry_do_not_use__ useConfigurationIssues auto */
865
734
 
@@ -915,7 +784,7 @@ function useUrlSync() {
915
784
  pushIssue("no-default-model", "info", "No default model set", "Agents without an explicit model will fail. Pick one default.", "?tab=models", "Open Models →");
916
785
  }
917
786
  for (const a of agents){
918
- const status = agentModelStatus(a, models);
787
+ const status = (0,effective_model/* agentModelStatus */.xu)(a, models);
919
788
  if (status.state === "no-model") {
920
789
  pushIssue(`agent-no-model:${a.id}`, "error", `Agent "${a.name}" has no model`, "Pick a model or set a workspace default.", `?tab=agents&item=${encodeURIComponent(a.id)}`, "Fix in Agents →");
921
790
  }
@@ -11315,6 +11184,8 @@ var plus = __webpack_require__(2125);
11315
11184
  var arrow_up_right = __webpack_require__(9751);
11316
11185
  // EXTERNAL MODULE: ./node_modules/lucide-react/dist/esm/icons/star.mjs
11317
11186
  var star = __webpack_require__(95697);
11187
+ // EXTERNAL MODULE: ./node_modules/lucide-react/dist/esm/icons/power-off.mjs
11188
+ var power_off = __webpack_require__(18402);
11318
11189
  // EXTERNAL MODULE: ./node_modules/lucide-react/dist/esm/icons/trash.mjs
11319
11190
  var trash = __webpack_require__(25879);
11320
11191
  ;// ./components/ui/PanelHeader.tsx
@@ -11396,6 +11267,24 @@ function useModels() {
11396
11267
  }, [
11397
11268
  refreshModels
11398
11269
  ]);
11270
+ const setDefault = (0,react.useCallback)(async (name)=>{
11271
+ const m = await client/* api */.FH.models.patch(name, {
11272
+ is_default: true
11273
+ });
11274
+ await refreshModels();
11275
+ return m;
11276
+ }, [
11277
+ refreshModels
11278
+ ]);
11279
+ const setEnabled = (0,react.useCallback)(async (name, enabled)=>{
11280
+ const m = await client/* api */.FH.models.patch(name, {
11281
+ enabled
11282
+ });
11283
+ await refreshModels();
11284
+ return m;
11285
+ }, [
11286
+ refreshModels
11287
+ ]);
11399
11288
  const remove = (0,react.useCallback)(async (name)=>{
11400
11289
  await client/* api */.FH.models.delete(name);
11401
11290
  await refreshModels();
@@ -11433,6 +11322,8 @@ function useModels() {
11433
11322
  refresh,
11434
11323
  create,
11435
11324
  update,
11325
+ setDefault,
11326
+ setEnabled,
11436
11327
  remove,
11437
11328
  assign,
11438
11329
  unassign
@@ -11447,6 +11338,8 @@ function useModels() {
11447
11338
  refresh,
11448
11339
  create,
11449
11340
  update,
11341
+ setDefault,
11342
+ setEnabled,
11450
11343
  remove,
11451
11344
  assign,
11452
11345
  unassign
@@ -11957,7 +11850,7 @@ const PROVIDER_COLORS = {
11957
11850
  "github-copilot": "bg-purple-900/40 text-purple-300 border-purple-700"
11958
11851
  };
11959
11852
  function ModelsPanel() {
11960
- const { models, assignments, loading, create, update, remove, refresh } = useModels();
11853
+ const { models, assignments, loading, create, update, setDefault, setEnabled, remove, refresh } = useModels();
11961
11854
  const { agents, loading: agentsLoading } = useAgents();
11962
11855
  const strategyOverrides = agents.filter((agent)=>agent.usage_strategy != null);
11963
11856
  const [editing, setEditing] = (0,react.useState)(null);
@@ -12005,12 +11898,20 @@ function ModelsPanel() {
12005
11898
  refresh();
12006
11899
  }
12007
11900
  async function handleSetDefault(m) {
12008
- await update(m.name, {
12009
- provider: m.provider,
12010
- model_id: m.model_id,
12011
- params: m.params,
12012
- is_default: true
12013
- });
11901
+ setDeleteError(null);
11902
+ try {
11903
+ await setDefault(m.name);
11904
+ } catch (e) {
11905
+ setDeleteError(`Could not set "${m.name}" as default: ${(0,utils_error/* errorMessage */.g)(e)}`);
11906
+ }
11907
+ }
11908
+ async function handleToggleEnabled(m) {
11909
+ setDeleteError(null);
11910
+ try {
11911
+ await setEnabled(m.name, !m.enabled);
11912
+ } catch (e) {
11913
+ setDeleteError(`Could not ${m.enabled ? "disable" : "enable"} "${m.name}": ${(0,utils_error/* errorMessage */.g)(e)}`);
11914
+ }
12014
11915
  }
12015
11916
  async function handleRemove(name) {
12016
11917
  const affectedAgents = assignments.filter((assignment)=>assignment.model_config_name === name).map((assignment)=>agents.find((agent)=>agent.id === assignment.agent_id)?.name ?? assignment.agent_id);
@@ -12286,7 +12187,7 @@ function ModelsPanel() {
12286
12187
  return /*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
12287
12188
  "data-deep-link-id": m.name,
12288
12189
  onClick: ()=>setEditing(m),
12289
- className: "flex items-center gap-3 py-2.5 border-b border-border/60 group cursor-pointer hover:bg-surface-3/30 transition-colors",
12190
+ className: `flex items-center gap-3 py-2.5 border-b border-border/60 group cursor-pointer hover:bg-surface-3/30 transition-colors ${m.enabled ? "" : "opacity-60"}`,
12290
12191
  children: [
12291
12192
  /*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
12292
12193
  className: "shrink-0 text-fg-subtle",
@@ -12302,13 +12203,17 @@ function ModelsPanel() {
12302
12203
  className: "flex items-center gap-2 mb-0.5",
12303
12204
  children: [
12304
12205
  /*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
12305
- className: "text-sm font-medium text-fg",
12206
+ className: `text-sm font-medium ${m.enabled ? "text-fg" : "text-fg-subtle line-through"}`,
12306
12207
  children: m.name
12307
12208
  }),
12308
12209
  m.is_default && /*#__PURE__*/ (0,jsx_runtime.jsx)(star/* default */.A, {
12309
12210
  size: 11,
12310
12211
  className: "text-yellow-700 dark:text-yellow-400 fill-yellow-400 shrink-0"
12311
12212
  }),
12213
+ !m.enabled && /*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
12214
+ className: "text-xs px-1.5 py-0.5 rounded border bg-surface-2 text-fg-muted border-border",
12215
+ children: "disabled"
12216
+ }),
12312
12217
  /*#__PURE__*/ (0,jsx_runtime.jsx)("span", {
12313
12218
  className: `text-xs px-1.5 py-0.5 rounded border ${PROVIDER_COLORS[m.provider] ?? "bg-surface-2 text-fg-muted border-border"}`,
12314
12219
  children: m.provider
@@ -12331,10 +12236,10 @@ function ModelsPanel() {
12331
12236
  /*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
12332
12237
  className: "flex gap-1 opacity-40 group-hover:opacity-100 pointer-coarse:opacity-100 transition-opacity shrink-0",
12333
12238
  children: [
12334
- !m.is_default && /*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
12239
+ !m.is_default && m.enabled && /*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
12335
12240
  onClick: (e)=>{
12336
12241
  e.stopPropagation();
12337
- handleSetDefault(m);
12242
+ void handleSetDefault(m);
12338
12243
  },
12339
12244
  className: "p-1 text-fg-subtle hover:text-yellow-700 dark:hover:text-yellow-400 transition-colors",
12340
12245
  title: "Set as default",
@@ -12342,6 +12247,21 @@ function ModelsPanel() {
12342
12247
  size: 13
12343
12248
  })
12344
12249
  }),
12250
+ /*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
12251
+ onClick: (e)=>{
12252
+ e.stopPropagation();
12253
+ void handleToggleEnabled(m);
12254
+ },
12255
+ className: "p-1 text-fg-subtle hover:text-fg transition-colors",
12256
+ title: m.enabled ? "Disable; hide from model selection" : "Enable; show in model selection",
12257
+ "aria-label": m.enabled ? `Disable ${m.name}` : `Enable ${m.name}`,
12258
+ "aria-pressed": !m.enabled,
12259
+ children: m.enabled ? /*#__PURE__*/ (0,jsx_runtime.jsx)(power/* default */.A, {
12260
+ size: 13
12261
+ }) : /*#__PURE__*/ (0,jsx_runtime.jsx)(power_off/* default */.A, {
12262
+ size: 13
12263
+ })
12264
+ }),
12345
12265
  /*#__PURE__*/ (0,jsx_runtime.jsx)("button", {
12346
12266
  onClick: (e)=>{
12347
12267
  e.stopPropagation();
@@ -19679,7 +19599,7 @@ function RouteTable({ bridge_id, module }) {
19679
19599
  client/* api */.FH.models.list().then(setModels).catch(()=>{});
19680
19600
  }, []);
19681
19601
  /** Vision capability of an agent (resolves via its model_config_name, with default fallback). */ function agentVisionState(a) {
19682
- const cfg = resolveAgentModel(a, models);
19602
+ const cfg = (0,effective_model/* resolveAgentModel */.Fu)(a, models);
19683
19603
  if (!cfg) return {
19684
19604
  supported: false,
19685
19605
  classified: false,
@@ -24780,7 +24700,7 @@ function WatcherCard({ watcher, agent, agents, models, onCancel, onRunNow, onCha
24780
24700
  const overdue = !watcher.last_error && new Date(watcher.next_run_at).getTime() < Date.now() - 60000;
24781
24701
  // Pre-flight model availability for the assigned agent; only relevant
24782
24702
  // when the watcher reacts via an agent prompt (not a pure script).
24783
- const modelStatus = watcher.reaction_kind === "agent_prompt" ? agentModelStatus(agent ?? null, models) : null;
24703
+ const modelStatus = watcher.reaction_kind === "agent_prompt" ? (0,effective_model/* agentModelStatus */.xu)(agent ?? null, models) : null;
24784
24704
  return /*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
24785
24705
  "data-deep-link-id": watcher.id,
24786
24706
  className: "mb-2 rounded-lg border border-border bg-surface-2 overflow-hidden",
@@ -25512,7 +25432,7 @@ function TaskCard({ task, agent, agents, models, onCancel, onRunNow, onChanged }
25512
25432
  collapseSeconds: true
25513
25433
  });
25514
25434
  const overdue = !task.last_error && new Date(task.next_run_at).getTime() < Date.now() - 60000;
25515
- const modelStatus = task.reaction_kind === "agent_prompt" ? agentModelStatus(agent ?? null, models) : null;
25435
+ const modelStatus = task.reaction_kind === "agent_prompt" ? (0,effective_model/* agentModelStatus */.xu)(agent ?? null, models) : null;
25516
25436
  return /*#__PURE__*/ (0,jsx_runtime.jsxs)("div", {
25517
25437
  "data-deep-link-id": task.id,
25518
25438
  className: "mb-2 rounded-lg border border-border bg-surface-2 overflow-hidden group",
@@ -33478,14 +33398,113 @@ function AppShell() {
33478
33398
  }
33479
33399
 
33480
33400
 
33401
+ /***/ }),
33402
+
33403
+ /***/ 30737:
33404
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
33405
+
33406
+ "use strict";
33407
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
33408
+ /* harmony export */ D: () => (/* binding */ useTheme),
33409
+ /* harmony export */ ThemeProvider: () => (/* binding */ ThemeProvider)
33410
+ /* harmony export */ });
33411
+ /* harmony import */ var react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(95155);
33412
+ /* harmony import */ var react__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12115);
33413
+ /* __next_internal_client_entry_do_not_use__ ThemeProvider,useTheme auto */
33414
+
33415
+ const STORAGE_KEY = "jarela-theme";
33416
+ function isTheme(v) {
33417
+ return v === "light" || v === "dark" || v === "system";
33418
+ }
33419
+ function readStored() {
33420
+ if (false) {}
33421
+ try {
33422
+ const v = window.localStorage.getItem(STORAGE_KEY);
33423
+ return isTheme(v) ? v : "system";
33424
+ } catch {
33425
+ return "system";
33426
+ }
33427
+ }
33428
+ const LIGHT_CHROME = "#ffffff";
33429
+ const DARK_CHROME = "#09090b";
33430
+ function resolveChrome(theme) {
33431
+ if (theme === "light") return LIGHT_CHROME;
33432
+ if (theme === "dark") return DARK_CHROME;
33433
+ if (false) {}
33434
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? DARK_CHROME : LIGHT_CHROME;
33435
+ }
33436
+ // Keep the single <meta name="theme-color"> tag (installed by the pre-paint
33437
+ // script in app/layout.tsx) aligned with the active surface so the PWA's
33438
+ // desktop title bar and mobile address bar match the theme.
33439
+ function syncChrome(theme) {
33440
+ if (typeof document === "undefined") return;
33441
+ const meta = document.querySelector('meta[name="theme-color"]');
33442
+ if (meta) meta.setAttribute("content", resolveChrome(theme));
33443
+ }
33444
+ function apply(theme) {
33445
+ if (typeof document === "undefined") return;
33446
+ document.documentElement.setAttribute("data-theme", theme);
33447
+ syncChrome(theme);
33448
+ }
33449
+ const ThemeContext = /*#__PURE__*/ (0,react__WEBPACK_IMPORTED_MODULE_1__.createContext)(null);
33450
+ function ThemeProvider({ children }) {
33451
+ // Initial state stays "system" on the server to match the pre-paint script,
33452
+ // which writes data-theme before React hydrates. The effect below syncs the
33453
+ // React state to whatever the script (or localStorage) decided.
33454
+ const [theme, setThemeState] = (0,react__WEBPACK_IMPORTED_MODULE_1__.useState)("system");
33455
+ (0,react__WEBPACK_IMPORTED_MODULE_1__.useEffect)(()=>{
33456
+ const stored = readStored();
33457
+ setThemeState(stored);
33458
+ // When in "system" mode, mirror OS-level changes into the PWA chrome.
33459
+ if (false) {}
33460
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
33461
+ const onChange = ()=>{
33462
+ if (readStored() === "system") syncChrome("system");
33463
+ };
33464
+ mq.addEventListener?.("change", onChange);
33465
+ return ()=>mq.removeEventListener?.("change", onChange);
33466
+ }, []);
33467
+ const setTheme = (0,react__WEBPACK_IMPORTED_MODULE_1__.useCallback)((t)=>{
33468
+ setThemeState(t);
33469
+ apply(t);
33470
+ try {
33471
+ window.localStorage.setItem(STORAGE_KEY, t);
33472
+ } catch {
33473
+ /* ignore quota / private-mode errors */ }
33474
+ }, []);
33475
+ return /*#__PURE__*/ (0,react_jsx_runtime__WEBPACK_IMPORTED_MODULE_0__.jsx)(ThemeContext.Provider, {
33476
+ value: {
33477
+ theme,
33478
+ setTheme
33479
+ },
33480
+ children: children
33481
+ });
33482
+ }
33483
+ function useTheme() {
33484
+ const ctx = (0,react__WEBPACK_IMPORTED_MODULE_1__.useContext)(ThemeContext);
33485
+ if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
33486
+ return ctx;
33487
+ }
33488
+
33489
+
33490
+ /***/ }),
33491
+
33492
+ /***/ 39852:
33493
+ /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
33494
+
33495
+ Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 9024));
33496
+ ;
33497
+ Promise.resolve(/* import() eager */).then(__webpack_require__.bind(__webpack_require__, 12283));
33498
+
33499
+
33481
33500
  /***/ })
33482
33501
 
33483
33502
  },
33484
33503
  /******/ __webpack_require__ => { // webpackRuntimeModules
33485
33504
  /******/ var __webpack_exec__ = (moduleId) => (__webpack_require__(__webpack_require__.s = moduleId))
33486
- /******/ __webpack_require__.O(0, [1973,8463,8095,4576,1395,8441,5254,7358], () => (__webpack_exec__(39852)));
33505
+ /******/ __webpack_require__.O(0, [1973,8463,8095,7045,1395,8441,5254,7358], () => (__webpack_exec__(39852)));
33487
33506
  /******/ var __webpack_exports__ = __webpack_require__.O();
33488
33507
  /******/ _N_E = __webpack_exports__;
33489
33508
  /******/ }
33490
33509
  ]);
33491
- //# sourceMappingURL=page-c6e972a7fd13dae8.js.map
33510
+ //# sourceMappingURL=page-7017f07f8a7a083c.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@circuitwall/jarela",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Jarela — local chat interface for LangGraph agents (multi-provider, single-process, SQLite-backed).",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Andrew Ge Wu",
package/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.1] - 2026-10-15
11
+
12
+ ### Fixed
13
+
14
+ - **Let models be disabled temporarily so they drop out of model selection and default/router resolution without being deleted, and set the default model in one atomic step that no longer rewrites model params or silently clears the default on edits.**
15
+ - **Show fresh agents, tasks, credentials, models, and other lists right after a save instead of a briefly stale browser-cached copy.**
16
+ - **Stop treating a `GITHUB_TOKEN` environment PAT as a GitHub Copilot credential and report Copilot catalog configuration errors as 400 instead of 500.**
17
+
10
18
  ## [2.2.0] - 2026-10-15
11
19
 
12
20
  ### Added
@@ -17,6 +17,7 @@ function model(modelId: string): ModelConfig {
17
17
  model_id: modelId,
18
18
  params: {},
19
19
  is_default: false,
20
+ enabled: true,
20
21
  created_at: "2026-01-01T00:00:00.000Z",
21
22
  updated_at: "2026-01-01T00:00:00.000Z",
22
23
  };
@@ -46,4 +47,34 @@ describe("api.models list cache", () => {
46
47
  await expect(Promise.all([oldList, latestList])).resolves.toEqual([[updated], [updated]]);
47
48
  expect(fetchMock).toHaveBeenCalledTimes(4);
48
49
  });
50
+ });
51
+
52
+ describe("api request HTTP cache mode", () => {
53
+ afterEach(() => {
54
+ vi.unstubAllGlobals();
55
+ });
56
+
57
+ it("bypasses the browser HTTP cache for mutable lists so reads after writes are fresh", async () => {
58
+ const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse([])));
59
+ vi.stubGlobal("fetch", fetchMock);
60
+
61
+ await api.models.list({ force: true });
62
+ await api.agents.list({ force: true });
63
+ await api.tasks.list({ force: true });
64
+ await api.credentials.list();
65
+
66
+ expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(4);
67
+ for (const [, init] of fetchMock.mock.calls) {
68
+ expect((init as RequestInit).cache).toBe("no-store");
69
+ }
70
+ });
71
+
72
+ it("lets read-only dashboard metrics keep the server's HTTP cache", async () => {
73
+ const fetchMock = vi.fn().mockResolvedValue(jsonResponse({}));
74
+ vi.stubGlobal("fetch", fetchMock);
75
+
76
+ await api.dashboard.metrics(7);
77
+
78
+ expect((fetchMock.mock.calls[0][1] as RequestInit).cache).toBe("default");
79
+ });
49
80
  });
package/api/client.ts CHANGED
@@ -51,6 +51,7 @@ import type {
51
51
  MemorySearchHit,
52
52
  ModelConfig,
53
53
  ModelConfigIn,
54
+ ModelConfigPatch,
54
55
  Credential,
55
56
  CredentialIn,
56
57
  StreamOptions,
@@ -388,6 +389,11 @@ async function request<T>(path: string, init?: RequestInit & { timeoutMs?: numbe
388
389
  try {
389
390
  const res = await fetch(`${BASE}${path}`, {
390
391
  headers: { "Content-Type": "application/json", ...fetchInit.headers },
392
+ // The in-memory list/value caches above are the client cache layer
393
+ // and are invalidated on writes. The browser HTTP cache would honor
394
+ // the server's short max-age and serve a stale list right after a
395
+ // save, so bypass it unless a caller opts in.
396
+ cache: "no-store",
391
397
  ...fetchInit,
392
398
  signal: timeoutCtrl.signal,
393
399
  });
@@ -811,6 +817,14 @@ export const api = {
811
817
  if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("jarela:models-changed"));
812
818
  return updated;
813
819
  },
820
+ patch: async (name: string, data: ModelConfigPatch) => {
821
+ const updated = await request<ModelConfig>(`/models/${encodeURIComponent(name)}`, { method: "PATCH", body: JSON.stringify(data) });
822
+ // Enabling/disabling or setting the default can move the default flag
823
+ // on other rows, so drop the cached list instead of patching one row.
824
+ invalidateModelListCache();
825
+ if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("jarela:models-changed"));
826
+ return updated;
827
+ },
814
828
  delete: async (name: string) => {
815
829
  const res = await request<{ deleted: boolean }>(`/models/${encodeURIComponent(name)}`, { method: "DELETE" });
816
830
  if (res.deleted) {
@@ -918,7 +932,8 @@ export const api = {
918
932
  },
919
933
 
920
934
  dashboard: {
921
- metrics: (days = 30) => request<DashboardMetrics>(`/dashboard/metrics?days=${encodeURIComponent(String(days))}`),
935
+ // Read-only aggregates: the server's 30s HTTP cache is intended here.
936
+ metrics: (days = 30) => request<DashboardMetrics>(`/dashboard/metrics?days=${encodeURIComponent(String(days))}`, { cache: "default" }),
922
937
  pricingTask: () => request<DashboardPricingTaskStatus>("/dashboard/pricing-task"),
923
938
  setupPricingTask: () => request<DashboardPricingTaskStatus>("/dashboard/pricing-task", {
924
939
  method: "POST",
package/api/types.ts CHANGED
@@ -482,10 +482,18 @@ export interface ModelConfig {
482
482
  // this for legacy rows that had inline secrets.
483
483
  credential_id?: string | null;
484
484
  is_default: boolean;
485
+ // Disabled configs are hidden from model pickers and skipped by runtime
486
+ // default/router resolution; the row and its credentials are kept.
487
+ enabled: boolean;
485
488
  created_at: string;
486
489
  updated_at: string;
487
490
  }
488
491
 
492
+ export interface ModelConfigPatch {
493
+ enabled?: boolean;
494
+ is_default?: true;
495
+ }
496
+
489
497
  export interface ModelConfigIn {
490
498
  provider: string;
491
499
  model_id: string;
@@ -0,0 +1,89 @@
1
+ import { afterAll, afterEach, describe, expect, it } from "vitest";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { NextRequest } from "next/server";
6
+
7
+ const tmpRoot = mkdtempSync(join(tmpdir(), "jarela-test-model-route-"));
8
+ process.env.JARELA_DB_DIR = tmpRoot;
9
+
10
+ const { upsertModelConfig, deleteModelConfig, listModelConfigs, getModelConfig } = await import("@/lib/stores/model-config");
11
+ const { PATCH, PUT } = await import("./route");
12
+
13
+ const ctx = (name: string) => ({ params: Promise.resolve({ name }) });
14
+ const json = (method: string, name: string, body: unknown) => new NextRequest(
15
+ `http://localhost/api/v1/models/${name}`,
16
+ { method, body: JSON.stringify(body), headers: { "Content-Type": "application/json" } },
17
+ );
18
+ const defaults = () => listModelConfigs().filter((r) => r.is_default === 1).map((r) => r.name);
19
+
20
+ afterEach(() => {
21
+ for (const r of listModelConfigs()) deleteModelConfig(r.name);
22
+ });
23
+
24
+ afterAll(() => {
25
+ try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* */ }
26
+ });
27
+
28
+ describe("PATCH /api/v1/models/[name]", () => {
29
+ it("switches the default without touching params", async () => {
30
+ upsertModelConfig("alpha", "anthropic", "claude-x", { temperature: 0.1 }, true);
31
+ upsertModelConfig("beta", "anthropic", "claude-y", { temperature: 0.7 }, false);
32
+ const before = getModelConfig("beta")!.params;
33
+
34
+ const res = await PATCH(json("PATCH", "beta", { is_default: true }), ctx("beta"));
35
+
36
+ expect(res.status).toBe(200);
37
+ await expect(res.json()).resolves.toMatchObject({ name: "beta", is_default: true, enabled: true });
38
+ expect(defaults()).toEqual(["beta"]);
39
+ expect(getModelConfig("beta")!.params).toBe(before);
40
+ });
41
+
42
+ it("disables a model and promotes another default atomically", async () => {
43
+ upsertModelConfig("alpha", "anthropic", "claude-x", {}, true);
44
+ upsertModelConfig("beta", "anthropic", "claude-y", {}, false);
45
+
46
+ const res = await PATCH(json("PATCH", "alpha", { enabled: false }), ctx("alpha"));
47
+
48
+ expect(res.status).toBe(200);
49
+ await expect(res.json()).resolves.toMatchObject({ enabled: false, is_default: false });
50
+ expect(defaults()).toEqual(["beta"]);
51
+ });
52
+
53
+ it("re-enables and sets default in one request", async () => {
54
+ upsertModelConfig("alpha", "anthropic", "claude-x", {}, true);
55
+ upsertModelConfig("beta", "anthropic", "claude-y", {}, false);
56
+ await PATCH(json("PATCH", "beta", { enabled: false }), ctx("beta"));
57
+
58
+ const res = await PATCH(json("PATCH", "beta", { enabled: true, is_default: true }), ctx("beta"));
59
+
60
+ expect(res.status).toBe(200);
61
+ expect(defaults()).toEqual(["beta"]);
62
+ });
63
+
64
+ it("returns 409 when disabling the last enabled model and 404 for unknown models", async () => {
65
+ upsertModelConfig("solo", "anthropic", "claude-x", {}, true);
66
+
67
+ expect((await PATCH(json("PATCH", "solo", { enabled: false }), ctx("solo"))).status).toBe(409);
68
+ expect(getModelConfig("solo")!.enabled).toBe(1);
69
+ expect((await PATCH(json("PATCH", "ghost", { is_default: true }), ctx("ghost"))).status).toBe(404);
70
+ });
71
+
72
+ it("rejects empty, contradictory, and unknown-field bodies", async () => {
73
+ upsertModelConfig("solo", "anthropic", "claude-x", {}, true);
74
+ for (const body of [{}, { enabled: false, is_default: true }, { is_default: false }, { params: {} }]) {
75
+ expect((await PATCH(json("PATCH", "solo", body), ctx("solo"))).status).toBe(400);
76
+ }
77
+ });
78
+ });
79
+
80
+ describe("PUT /api/v1/models/[name]", () => {
81
+ it("keeps the default flag when is_default is omitted", async () => {
82
+ upsertModelConfig("alpha", "anthropic", "claude-x", {}, true);
83
+
84
+ const res = await PUT(json("PUT", "alpha", { provider: "anthropic", model_id: "claude-x", params: { temperature: 0.3 } }), ctx("alpha"));
85
+
86
+ expect(res.status).toBe(200);
87
+ expect(defaults()).toEqual(["alpha"]);
88
+ });
89
+ });