@worca/app 1.6.0-rc.1 → 1.6.0-rc.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/README.md CHANGED
@@ -234,7 +234,7 @@ See [`docs/team-policy.md`](docs/team-policy.md).
234
234
  into that model's agent spawns. Share a model catalog as a plugin, with
235
235
  secrets required at install time.
236
236
  - **No first-party account needed** — run and chat titles are written by the
237
- model the run or chat itself uses (Settings › General › Title generation picks
237
+ model the run or chat itself uses (Settings › Models › Title generation picks
238
238
  a fixed one instead), endpoint-routed models carry Claude Code's internal
239
239
  haiku/sonnet/opus/fable tier keys so nothing falls back to the Anthropic API,
240
240
  and *Hide built-in models* (Settings › Models) drops the built-ins from every
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@worca/app",
3
- "version": "1.6.0-rc.1",
3
+ "version": "1.6.0-rc.2",
4
4
  "description": "Worca — deterministic multi-agent pipeline that drives Claude Code (headless) through Plan -> Refine -> Implement -> Review, with a CLI, an installable /worca skill, and a web UI.",
5
5
  "license": "MIT",
6
6
  "author": "Sinisha Djukic",
@@ -658,7 +658,11 @@ class AskTurn extends EventEmitter {
658
658
  await d.fs.writeFile(
659
659
  mcpConfigPath,
660
660
  JSON.stringify(d.buildMcpConfig({ homeBase, threadId: this.threadId, serverPath: d.serverPath, ...(this.reader ? { reader: this.reader } : {}), ...(this.relay ? { relay: this.relay } : {}), ...(this.web ? { web: this.web } : {}) }), null, 2),
661
- { encoding: 'utf8', mode: 0o600 }, // never a key value (webKeyVar: the key rides the process env)
661
+ // Never a key value (webKeyVar: the key rides the process env). A relayed turn runs as
662
+ // the person's agent user (agent-pool.mjs), which reads this file through its group: the
663
+ // scratch dir is setgid worca-share (2770), so 0640 reaches the agent users and nobody
664
+ // else. It carries this turn's relay token, which that agent must present anyway.
665
+ { encoding: 'utf8', mode: this.relay ? 0o640 : 0o600 },
662
666
  );
663
667
  // One 30-minute budget for the whole turn, retry included. The timedOut
664
668
  // flag and abort() run in ONE synchronous callback, so R-C always reads
@@ -19,7 +19,7 @@ import {
19
19
  } from './providers/copilot.mjs';
20
20
  import { keyOptional } from './registry.mjs';
21
21
  import { listEndpointModels, catalogEntryForEndpointModel, importableModel } from './providers/endpoint.mjs';
22
- import { brokerEnabled, brokerInfo, cachedBrokerInfo, mintSpawnToken, revokeSpawnToken, slotBaseUrl } from '../broker-client.mjs';
22
+ import { brokerEnabled, brokerInfo, cachedBrokerInfo, mintSpawnToken, revokeSpawnToken, slotBaseUrl, personSlots } from '../broker-client.mjs';
23
23
  import { routeUpstream } from '../broker-routing.mjs';
24
24
  import { resolveBillTo } from '../billing.mjs';
25
25
 
@@ -129,7 +129,7 @@ const secretSource = (v) => (!v ? null : modelEnvRef(v) ? 'env' : 'stored');
129
129
  * a network call to GitHub — and is null when the account exposes none.
130
130
  * @param {{quota?:boolean, fetch?:typeof fetch}} [opts]
131
131
  */
132
- export async function providersState({ quota = false, fetch: f } = {}) {
132
+ export async function providersState({ quota = false, fetch: f, person = resolveBillTo(null) } = {}) {
133
133
  const all = allProviders();
134
134
  const c = all.copilot;
135
135
  const token = resolveProviderSecret(c.githubToken);
@@ -161,11 +161,47 @@ export async function providersState({ quota = false, fetch: f } = {}) {
161
161
  maxConcurrent: p.maxConcurrent,
162
162
  };
163
163
  };
164
- // With the credential broker on, keys and the Copilot sign-in are per person on its key page.
165
- const broker = brokerEnabled() ? { enabled: true, keyPage: cachedBrokerInfo()?.publicUrl || null, mode: cachedBrokerInfo()?.mode || null } : { enabled: false };
164
+ // With the credential broker on, keys and the Copilot sign-in are per person on its key page:
165
+ // each provider's state is the viewer's own slot there, not worca's (empty) settings.
166
+ const broker = brokerEnabled() ? await brokerProviders(all, person) : { enabled: false };
166
167
  return { copilot, openai: keyed('openai'), anthropic: keyed('anthropic'), broker };
167
168
  }
168
169
 
170
+ /**
171
+ * The Providers card's view of the broker: which slot each provider spends from and the viewer's
172
+ * state in it — {slot, state:'set'|'missing'|'invalid'|'operator'|'keyless'|'none', suffix?, error?}.
173
+ * Never throws: an unreachable broker leaves the states out and says why.
174
+ */
175
+ async function brokerProviders(all, person) {
176
+ const info = cachedBrokerInfo() || await brokerInfo().catch(() => null);
177
+ const out = { enabled: true, keyPage: info?.publicUrl || null, mode: info?.mode || null, providers: {} };
178
+ const slots = info?.slots || [];
179
+ const route = (name) => {
180
+ if (name === 'copilot') {
181
+ const s = slots.find((x) => x.auth === 'copilot');
182
+ return s ? { slot: s.id } : { state: 'none', error: 'the credential broker has no GitHub Copilot slot' };
183
+ }
184
+ const r = routeUpstream({ provider: name, baseUrl: all[name].baseUrl || null }, { slots });
185
+ return r.slot ? { slot: r.slot } : r.keyless ? { state: 'keyless' } : { state: 'none', error: r.error };
186
+ };
187
+ for (const name of ['copilot', 'openai', 'anthropic']) out.providers[name] = route(name);
188
+ const who = out.mode === 'multi' ? person : 'local';
189
+ if (!who || (out.mode === 'multi' && who === 'local')) { out.signInNeeded = true; return out; }
190
+ try {
191
+ const mine = new Map(((await personSlots(who)).slots || []).map((x) => [x.id, x]));
192
+ for (const p of Object.values(out.providers)) {
193
+ if (!p.slot) continue;
194
+ const s = mine.get(p.slot);
195
+ p.state = s ? s.state : 'missing';
196
+ if (s && s.suffix) p.suffix = s.suffix;
197
+ if (s && s.label) p.label = s.label;
198
+ }
199
+ } catch (err) {
200
+ out.error = err.message;
201
+ }
202
+ return out;
203
+ }
204
+
169
205
  /** Patch a provider from the UI/CLI; masked key echoes are dropped ("keep"). */
170
206
  export async function patchProvider(name, patch = {}) {
171
207
  if (!UPSTREAM_PROVIDERS.includes(name)) throw new Error(`unknown provider ${JSON.stringify(name)}`);
package/ui/public/app.js CHANGED
@@ -773,7 +773,7 @@ function paintBudget() {
773
773
  topAmt.classList.toggle('over', !!b.blocked);
774
774
  }
775
775
  applyBudgetToNewView();
776
- if (currentView() === 'settings') paintBudgetReadout();
776
+ if (currentView() === 'settings' && currentSettingsTab === 'runs') paintBudgetReadout();
777
777
  repaintCostBanners();
778
778
  }
779
779
 
@@ -1066,7 +1066,7 @@ function handleServerMessage(msg) {
1066
1066
  if (currentView() === 'projects') paintProjectPolicyCells(true);
1067
1067
  if (currentView() === 'workspaces') paintWsPolicyLines(true);
1068
1068
  if (currentView() === 'team-policy' && !tpState.editing) loadTeamPolicyView();
1069
- if (currentView() === 'settings') paintTeamCapsReadout(true);
1069
+ if (currentView() === 'settings' && currentSettingsTab === 'runs') paintTeamCapsReadout(true);
1070
1070
  if (currentView() === 'settings' && currentSettingsTab === 'plugins') paintPluginsPolicy(true);
1071
1071
  if (currentView() === 'new') schedulePolicyLine();
1072
1072
  return;
@@ -10953,7 +10953,7 @@ try {
10953
10953
  if (mq && typeof mq.addEventListener === 'function') mq.addEventListener('change', () => applyTheme(document.documentElement.dataset.theme));
10954
10954
  } catch { /* no media queries here */ }
10955
10955
 
10956
- // Settings › General › Scheduled runs: the defaults a new schedule inherits.
10956
+ // Settings › Runs › Scheduled runs: the defaults a new schedule inherits.
10957
10957
  function setSchedDefaultsMsg(text, kind) { setHintMsg('schedDefaultsMsg', text, kind); }
10958
10958
  function paintScheduleSettings(data) {
10959
10959
  const d = data && data.schedule;
@@ -12702,6 +12702,7 @@ async function loadModelsView() {
12702
12702
  const pdata = await safeJson(pres);
12703
12703
  mvState.providers = pres.ok ? pdata : null;
12704
12704
  renderModelsViewBody();
12705
+ void paintHelperModelCards();
12705
12706
  // "+ Add model…" from a picker on another page: the dialog opens once the catalog is here.
12706
12707
  if (mvState.openEditorOnLoad) { mvState.openEditorOnLoad = false; openModelEditorDialog(); }
12707
12708
  // Copilot's models feed the editor's upstream-id datalist; fetched in the
@@ -12716,6 +12717,18 @@ async function loadModelsView() {
12716
12717
  }
12717
12718
  }
12718
12719
 
12720
+ // Settings › Models › Title generation + Auto workflow model: both pickers list the catalog, so they
12721
+ // repaint whenever it loads — a model added a moment ago is selectable without leaving the tab.
12722
+ async function paintHelperModelCards() {
12723
+ try {
12724
+ const res = await fetch('/api/settings');
12725
+ if (!res.ok) return;
12726
+ const data = await safeJson(res);
12727
+ await paintTitleModelSettings(data);
12728
+ await paintAutoModelSettings(data);
12729
+ } catch { /* the cards keep their last paint */ }
12730
+ }
12731
+
12719
12732
  /** The Providers tab: the same card, on a page of its own (§8.1). */
12720
12733
  async function loadProvidersView() {
12721
12734
  if (!el.providersList) return;
@@ -13886,7 +13899,7 @@ if (runListEl) {
13886
13899
  if (runId) confirmCostOverride(runId, overrideBtn);
13887
13900
  return;
13888
13901
  }
13889
- if (e.target.closest && e.target.closest('.cb-settings')) { location.hash = 'settings'; return; }
13902
+ if (e.target.closest && e.target.closest('.cb-settings')) { location.hash = 'settings/runs'; return; }
13890
13903
  // Team-cap banner (team-policy design board 9): continue past, or open the page.
13891
13904
  const pastBtn = e.target.closest && e.target.closest('.cb-past-team-cap');
13892
13905
  if (pastBtn) {
@@ -14575,7 +14588,7 @@ if (tpSection) tpSection.addEventListener('click', async (e) => {
14575
14588
  await handlePolicyPluginClick(e);
14576
14589
  });
14577
14590
 
14578
- // Settings › Budget (board 7): each home's caps as a readout, the tightest as a chip on the labels.
14591
+ // Settings › Runs › Budget (board 7): each home's caps as a readout, the tightest as a chip on the labels.
14579
14592
  async function paintTeamCapsReadout(force = false) {
14580
14593
  if (!el.teamCapsReadout) return;
14581
14594
  const data = await loadTpScopes({ force });
@@ -17377,7 +17390,7 @@ function paintHdBanners(screen, record, data) {
17377
17390
  { pauseReason, pauseDetail, pipelineId: record.id, totalCostUsd: st.totalCostUsd },
17378
17391
  { budget: budgetState.budget || {}, fmt: { usd: fmtUsd, usd4: fmtUsd4, duration: fmtDuration, estTitle } });
17379
17392
  const settingsBtn = banner.querySelector('.cb-settings');
17380
- if (settingsBtn) settingsBtn.addEventListener('click', () => { location.hash = 'settings'; });
17393
+ if (settingsBtn) settingsBtn.addEventListener('click', () => { location.hash = 'settings/runs'; });
17381
17394
  const overrideBtn = banner.querySelector('.cb-override');
17382
17395
  if (overrideBtn) {
17383
17396
  overrideBtn.addEventListener('click', () => {
@@ -22537,7 +22550,7 @@ el.runDetail?.addEventListener('click', (e) => {
22537
22550
  if (!r) return;
22538
22551
  const override = e.target.closest && e.target.closest('.cb-override');
22539
22552
  if (override) { confirmCostOverride(r.runId, override); return; } // async, fire-and-forget
22540
- if (e.target.closest && e.target.closest('.cb-settings')) { location.hash = 'settings'; return; }
22553
+ if (e.target.closest && e.target.closest('.cb-settings')) { location.hash = 'settings/runs'; return; }
22541
22554
  const past = e.target.closest && e.target.closest('.cb-past-team-cap');
22542
22555
  if (past) { confirmPastTeamCap(r.runId, past); return; } // team-policy board 9
22543
22556
  if (e.target.closest && e.target.closest('.cb-policy-open')) { location.hash = 'team-policy'; return; }
@@ -23654,11 +23667,11 @@ const VIEW_MIN_LEVEL = Object.freeze({
23654
23667
  'team-metrics': 'expert', 'team-policy': 'expert', agents: 'expert', scripts: 'expert',
23655
23668
  schedules: 'advanced',
23656
23669
  });
23657
- const SETTINGS_TAB_MIN_LEVEL = Object.freeze({ guardrails: 'advanced', plugins: 'advanced', memory: 'advanced', models: 'expert', providers: 'expert' });
23670
+ const SETTINGS_TAB_MIN_LEVEL = Object.freeze({ ask: 'advanced', guardrails: 'advanced', plugins: 'advanced', memory: 'advanced', models: 'expert', providers: 'expert' });
23658
23671
  const VIEW_TITLES = Object.freeze({
23659
23672
  stats: 'Statistics', composer: 'Workflow Composer', workspaces: 'Workspaces', 'workspace-create': 'Workspaces',
23660
23673
  'agent-create': 'Create agent', 'team-metrics': 'Team metrics', 'team-policy': 'Team policy', agents: 'Agents', scripts: 'Scripts',
23661
- guardrails: 'Guardrails', plugins: 'Plugins', memory: 'Memory', models: 'Models', providers: 'Providers',
23674
+ guardrails: 'Guardrails', plugins: 'Plugins', memory: 'Memory', models: 'Models', providers: 'Providers', ask: 'Ask Worca',
23662
23675
  schedules: 'Schedules',
23663
23676
  });
23664
23677
  function pageMinLevel() {
@@ -23715,7 +23728,10 @@ document.addEventListener('worca:level', () => {
23715
23728
  // The tab is the Settings view's hash param; a guardrail deep link nests its id
23716
23729
  // behind it (#settings/guardrails/<id>). parseHash splits on the FIRST '/' only,
23717
23730
  // so that is view 'settings', param 'guardrails/<id>' — no parseHash change.
23718
- const SETTINGS_TABS = ['general', 'guardrails', 'models', 'providers', 'plugins', 'memory'];
23731
+ const SETTINGS_TABS = ['general', 'runs', 'ask', 'guardrails', 'memory', 'plugins', 'models', 'providers'];
23732
+ // The tabs whose cards GET /api/settings paints (loadSettings paints every card, wherever it sits).
23733
+ // Models is not one: loadModelsView repaints its two helper-model cards with the catalog.
23734
+ const SETTINGS_FORM_TABS = ['general', 'runs', 'ask'];
23719
23735
  const settingsPanes = $$('[data-view="settings"] .settings-pane');
23720
23736
  // Old top-level hashes keep working. The hashchange listener DROPS any view it
23721
23737
  // does not know, so without this map a bookmark or an old in-app link would
@@ -23965,7 +23981,7 @@ function showSettingsTab(param = '') {
23965
23981
  // request, and re-entry refetches (which is what lets grvExitWizard's
23966
23982
  // '#settings/guardrails/<id>' -> '#settings/guardrails' hop reset the wizard).
23967
23983
  paintLevelBanner();
23968
- if (tab === 'general') loadSettings();
23984
+ if (SETTINGS_FORM_TABS.includes(tab)) loadSettings();
23969
23985
  if (tab === 'guardrails') loadGuardrailsView(sub);
23970
23986
  if (tab === 'models') loadModelsView(sub);
23971
23987
  if (tab === 'providers') loadProvidersView();
@@ -106,11 +106,48 @@ export function needsSignInPill(m, { doc = globalThis.document } = {}) {
106
106
  // ── Providers card (§8.1) ────────────────────────────────────────────────────
107
107
 
108
108
  function providerStatePill(doc, c) {
109
- if (!c.termsCurrent) return h(doc, 'span', 'badge grey', 'sign-in blocked until acknowledged');
109
+ if (!c.termsCurrent) {
110
+ // What unblocks sign-in, in the "Read notice" button's own words — and it opens that notice.
111
+ const pill = h(doc, 'button', 'badge grey mv-cp-terms mv-cp-terms-pill',
112
+ c.acknowledgedTerms ? 'please re-read the updated notice' : 'please read the notice first');
113
+ pill.type = 'button';
114
+ pill.title = 'Sign-in is blocked until you acknowledge the GitHub Copilot notice. Click to read it.';
115
+ return pill;
116
+ }
110
117
  if (!c.connected) return h(doc, 'span', 'badge grey', 'not connected');
111
118
  return h(doc, 'span', 'badge green', `connected${c.login ? ` as @${c.login}` : ''}`);
112
119
  }
113
120
 
121
+ /**
122
+ * With the credential broker: the viewer's own state in the slot `name` spends from (key page),
123
+ * not worca's settings, which hold no key. A missing key links to the key page.
124
+ */
125
+ function brokerPill(doc, b, name) {
126
+ const p = (b.providers || {})[name] || {};
127
+ const what = name === 'copilot' ? 'sign-in' : 'key';
128
+ if (b.signInNeeded) return h(doc, 'span', 'badge grey', `sign in to see your ${what}`);
129
+ if (p.state === 'none') {
130
+ const x = h(doc, 'span', 'badge red', 'no broker slot for this URL');
131
+ x.title = p.error || 'The credential broker has no slot for this base URL.';
132
+ return x;
133
+ }
134
+ if (p.state === 'keyless') return h(doc, 'span', 'badge green', 'local — no key needed');
135
+ if (b.error) {
136
+ const x = h(doc, 'span', 'badge grey', 'key page unreachable');
137
+ x.title = b.error;
138
+ return x;
139
+ }
140
+ if (p.state === 'set') return h(doc, 'span', 'badge green', `your ${what}${p.suffix ? ` ••••${p.suffix}` : ''}`);
141
+ if (p.state === 'operator') return h(doc, 'span', 'badge green', 'team key');
142
+ const label = p.state === 'invalid' ? `your ${what} was rejected` : `add your ${what} on the key page`;
143
+ const cls = p.state === 'invalid' ? 'badge red' : 'badge grey';
144
+ if (!b.keyPage) return h(doc, 'span', cls, label);
145
+ const a = h(doc, 'a', `${cls} mv-broker-pill`, label);
146
+ a.href = b.keyPage; a.target = '_blank'; a.rel = 'noopener';
147
+ a.title = 'Opens the key page in a new tab. Reopen this tab afterwards to see the change.';
148
+ return a;
149
+ }
150
+
114
151
  function quotaLine(q) {
115
152
  if (!q) return '';
116
153
  if (q.unlimited) return 'Premium requests: unlimited on this plan';
@@ -181,7 +218,9 @@ export function renderProvidersCard(providers, { doc = globalThis.document, sign
181
218
  const cpMain = h(doc, 'div', 'mv-pv-main');
182
219
  const cpHead = h(doc, 'div', 'mv-head');
183
220
  cpHead.appendChild(h(doc, 'b', 'mv-name', 'GitHub Copilot'));
184
- cpHead.appendChild(providerStatePill(doc, c));
221
+ const brokered = !!(p.broker && p.broker.enabled);
222
+ // The notice gates Copilot models either way; past it, the broker's view of your sign-in.
223
+ cpHead.appendChild(brokered && c.termsCurrent ? brokerPill(doc, p.broker, 'copilot') : providerStatePill(doc, c));
185
224
  cpMain.appendChild(cpHead);
186
225
  cpMain.appendChild(h(doc, 'small', 'hint',
187
226
  c.acknowledgedTerms
@@ -215,7 +254,6 @@ export function renderProvidersCard(providers, { doc = globalThis.document, sign
215
254
  const cpBtns = h(doc, 'div', 'mv-pv-btns');
216
255
  const imp = h(doc, 'button', 'btn-ghost mv-cp-fetch-models', 'Import models…');
217
256
  // With the broker, the import runs with the viewer's own Copilot sign-in (on the key page).
218
- const brokered = !!(p.broker && p.broker.enabled);
219
257
  imp.type = 'button'; imp.disabled = !c.connected && !brokered;
220
258
  if (imp.disabled) imp.title = 'Sign in first';
221
259
  cpBtns.appendChild(imp);
@@ -245,7 +283,7 @@ export function renderProvidersCard(providers, { doc = globalThis.document, sign
245
283
  const main = h(doc, 'div', 'mv-pv-main');
246
284
  const rh = h(doc, 'div', 'mv-head');
247
285
  rh.appendChild(h(doc, 'b', 'mv-name', PROVIDER_LABELS[name]));
248
- rh.appendChild(k.configured ? h(doc, 'span', 'badge green', 'key set')
286
+ rh.appendChild(p.broker && p.broker.enabled ? brokerPill(doc, p.broker, name) : k.configured ? h(doc, 'span', 'badge green', 'key set')
249
287
  : k.keySet ? h(doc, 'span', 'badge red', 'key ${VAR} not set')
250
288
  : k.keyOptional ? h(doc, 'span', 'badge green', 'local — no key needed') : h(doc, 'span', 'badge grey', 'no key'));
251
289
  main.appendChild(rh);
@@ -1350,12 +1350,16 @@
1350
1350
  literal routing attribute name, so never spell it out in a comment. -->
1351
1351
  <section class="view hidden" data-view="settings">
1352
1352
  <div class="seg settings-tabs" id="settings-tabs" role="group" aria-label="Settings section">
1353
+ <!-- Ordered by mode (Simple, Advanced, Expert) so each mode sees a gap-free prefix of the
1354
+ strip; Models sits next to Providers. -->
1353
1355
  <button type="button" class="on" data-tab="general" data-min-level="simple" aria-pressed="true">General</button>
1356
+ <button type="button" data-tab="runs" data-min-level="simple" aria-pressed="false">Runs</button>
1357
+ <button type="button" data-tab="ask" data-min-level="advanced" aria-pressed="false">Ask Worca</button>
1354
1358
  <button type="button" data-tab="guardrails" data-min-level="advanced" aria-pressed="false">Guardrails</button>
1359
+ <button type="button" data-tab="memory" data-min-level="advanced" aria-pressed="false">Memory</button>
1360
+ <button type="button" data-tab="plugins" data-min-level="advanced" aria-pressed="false">Plugins</button>
1355
1361
  <button type="button" data-tab="models" data-min-level="expert" aria-pressed="false">Models</button>
1356
1362
  <button type="button" data-tab="providers" data-min-level="expert" aria-pressed="false">Providers</button>
1357
- <button type="button" data-tab="plugins" data-min-level="advanced" aria-pressed="false">Plugins</button>
1358
- <button type="button" data-tab="memory" data-min-level="advanced" aria-pressed="false">Memory</button>
1359
1363
  </div>
1360
1364
 
1361
1365
  <section class="settings-pane" data-tab="general">
@@ -1452,6 +1456,97 @@
1452
1456
  <small class="hint" id="settingsMsg"></small>
1453
1457
  </section>
1454
1458
 
1459
+ <!-- Spawn-debug diagnostics: the stored side of WORCA_DEBUG_SPAWN.
1460
+ claude-runner.mjs reads the setting per spawn (UI server AND CLI);
1461
+ a non-empty env var overrides it. This card only flips the gate —
1462
+ the runner decides what is printable (routing keys readable,
1463
+ credentials as "<set, N chars>"). -->
1464
+ <section class="card settings-card" id="debug-spawn-settings-card" data-min-level="expert">
1465
+ <div class="card-head">
1466
+ <div class="label-row">
1467
+ <h2>Spawn diagnostics</h2>
1468
+ <button type="button" class="info-tip" aria-label="About spawn diagnostics">i<span class="tip-content hidden">
1469
+ Logs the exact claude binary, argv and routing env handed to every spawn, in the run
1470
+ log. Credentials never appear: routing keys (endpoint, wire model) print readable,
1471
+ every other value as <code>&lt;set, N chars&gt;</code>. Applies to runs started here
1472
+ and from the CLI, with no restart. A non-empty <code>WORCA_DEBUG_SPAWN</code> in the
1473
+ environment overrides this setting.
1474
+ </span></button>
1475
+ </div>
1476
+ </div>
1477
+ <div class="field">
1478
+ <label class="check-row" for="debugSpawnEnabled"><input id="debugSpawnEnabled" type="checkbox" /> Log claude spawn details (binary, argv, routing env)</label>
1479
+ </div>
1480
+ <small class="hint" id="debugSpawnEnvNote"></small>
1481
+ <div class="add-project-actions" style="margin-top:14px">
1482
+ <button type="button" id="debugSpawnReset" class="btn btn-ghost btn-mini">Reset to default</button>
1483
+ <button type="button" id="debugSpawnSave" class="btn btn-primary btn-mini">Save</button>
1484
+ </div>
1485
+ <small class="hint" id="debugSpawnMsg"></small>
1486
+ </section>
1487
+
1488
+ <!-- Getting started: the checklist's Hide is undone here. Only the checklist —
1489
+ the welcome dialog stays seen. No info-tip (the row explains itself);
1490
+ the .hint is painted by app.js like every other settings status line. -->
1491
+ <section class="card settings-card" id="getting-started-card" data-min-level="simple">
1492
+ <div class="label-row">
1493
+ <h2>Getting started</h2>
1494
+ </div>
1495
+ <div class="gs-settings-row">
1496
+ <p class="gs-settings-text">The checklist on New pipeline walks through the first nine things to do with Worca. Hidden it, or want the guided tour back?</p>
1497
+ <button type="button" id="gsShowAgain" class="btn btn-ghost btn-mini">Show again</button>
1498
+ </div>
1499
+ <small class="hint" id="gsSettingsMsg"></small>
1500
+ </section>
1501
+
1502
+ <!-- About: intentionally no info-tip and no .hint (test/ui-settings-tooltips.test.mjs pins both). -->
1503
+ <section class="card settings-card" id="about-card" data-min-level="simple">
1504
+ <div class="label-row">
1505
+ <h2>About</h2>
1506
+ </div>
1507
+ <div class="about-row">
1508
+ <span class="about-key">Version</span>
1509
+ <!-- href is painted from app.releaseUrl (the worca-app-v<version> tag);
1510
+ without one the anchor is plain text, so the placeholder never links. -->
1511
+ <a class="about-val about-version" id="aboutVersion"
1512
+ target="_blank" rel="noopener noreferrer">&mdash;</a>
1513
+ </div>
1514
+ <div class="about-row">
1515
+ <span class="about-key">Repository</span>
1516
+ <a class="about-val about-link" id="aboutRepoLink"
1517
+ href="https://github.com/SinishaDjukic/worca-cc"
1518
+ target="_blank" rel="noopener noreferrer">github.com/SinishaDjukic/worca-cc</a>
1519
+ </div>
1520
+ <div class="about-row">
1521
+ <span class="about-key">Feedback</span>
1522
+ <!-- hrefs are repainted from app.bugsUrl (package.json bugs.url); the
1523
+ static fallback keeps the links usable before /api/settings lands.
1524
+ Anchors only: ui-settings-about.test.mjs forbids a control here,
1525
+ and neither the label nor the href carries a version string. -->
1526
+ <a class="about-val about-link" id="aboutBugLink"
1527
+ href="https://github.com/SinishaDjukic/worca-cc/issues/new?labels=bug"
1528
+ target="_blank" rel="noopener noreferrer">Report a bug</a>
1529
+ </div>
1530
+ <div class="about-row">
1531
+ <span class="about-key"></span>
1532
+ <a class="about-val about-link" id="aboutIdeaLink"
1533
+ href="https://github.com/SinishaDjukic/worca-cc/issues/new?labels=enhancement"
1534
+ target="_blank" rel="noopener noreferrer">Suggest an improvement</a>
1535
+ </div>
1536
+ </section>
1537
+ </section>
1538
+ <!-- /pane general -->
1539
+
1540
+ <!-- ===== TAB: RUNS — how pipeline runs are capped, scheduled and reported.
1541
+ Simple, for Budget; the two Advanced cards keep their own level. ===== -->
1542
+ <section class="settings-pane hidden" data-tab="runs">
1543
+ <div class="topbar">
1544
+ <div>
1545
+ <h1>Runs</h1>
1546
+ <div class="sub">Cost limits and defaults for your pipeline runs</div>
1547
+ </div>
1548
+ </div>
1549
+
1455
1550
  <section class="card settings-card" id="budget-settings-card" data-min-level="simple">
1456
1551
  <div class="card-head">
1457
1552
  <div class="label-row">
@@ -1510,63 +1605,6 @@
1510
1605
  <small class="hint" id="budgetMsg"></small>
1511
1606
  </section>
1512
1607
 
1513
- <!-- Ask Worca (ask-worca-design §6.9): per-turn guards for the
1514
- assistant chat. Read fresh on every turn (D12); a stored null
1515
- budget means "no cap". Posted ALONE — never with root/budget keys. -->
1516
- <section class="card settings-card" id="ask-settings-card" data-min-level="advanced">
1517
- <div class="card-head">
1518
- <div class="label-row">
1519
- <h2>Ask Worca</h2>
1520
- <button type="button" class="info-tip" aria-label="About Ask Worca limits">i<span class="tip-content hidden">
1521
- Per-turn guards for the Ask Worca assistant chat. A chat turn stops when it reaches the turn limit or the per-turn cost cap; both are read fresh on every turn.
1522
- </span></button>
1523
- </div>
1524
- </div>
1525
- <div class="field">
1526
- <div class="label-row">
1527
- <label for="askMaxTurns">Turn limit</label>
1528
- <button type="button" class="info-tip" aria-label="About Turn limit">i<span class="tip-content hidden">
1529
- Maximum assistant/tool turns one chat message may use (1&ndash;500). Leave empty to restore the default of 400.
1530
- </span></button>
1531
- </div>
1532
- <input id="askMaxTurns" class="input" type="number" min="1" max="500" step="1" inputmode="numeric" placeholder="400" autocomplete="off" />
1533
- </div>
1534
- <div class="field">
1535
- <div class="label-row">
1536
- <label for="askMaxBudgetUsd">Per-turn cost cap (USD)</label>
1537
- <button type="button" class="info-tip" aria-label="About Per-turn cost cap">i<span class="tip-content hidden">
1538
- There is no cap by default. Untick No cap and enter an amount (0.1&ndash;100) to turn the guard on: a chat turn then stops when its estimated cost reaches it. Leaving the amount empty, or pressing Use defaults, means no cap.
1539
- </span></button>
1540
- </div>
1541
- <input id="askMaxBudgetUsd" class="input" type="number" min="0.1" max="100" step="0.1" inputmode="decimal" placeholder="No cap" autocomplete="off" />
1542
- <label class="check-row" for="askNoCap"><input id="askNoCap" type="checkbox" /> No cap</label>
1543
- </div>
1544
- <!-- Scripts workbench W20: the chat's save_script / test_script switch. Body
1545
- rendered by chat-settings-view.mjs; saved with this card's Save button. -->
1546
- <div class="field" id="ask-script-tools-host"></div>
1547
- <!-- Ask web access (off by default): rendered by chat-settings-view.mjs; saved with this
1548
- card's Save button, and only when one of its fields was touched. -->
1549
- <div class="field" id="ask-web-host"></div>
1550
- <div class="add-project-actions" style="margin-top:14px">
1551
- <button type="button" id="askLimitsReset" class="btn btn-ghost btn-mini">Use defaults</button>
1552
- <button type="button" id="askLimitsSave" class="btn btn-primary btn-mini">Save</button>
1553
- </div>
1554
- <small class="hint" id="askLimitsMsg"></small>
1555
- <!-- Chat history: the counts paint with the view; the button opens a
1556
- danger confirm quoting FRESH counts, then DELETE /api/ask/threads. -->
1557
- <div class="field" style="margin-top:14px">
1558
- <div class="label-row">
1559
- <label for="askHistoryDelete">Chat history</label>
1560
- <button type="button" class="info-tip" aria-label="About Chat history">i<span class="tip-content hidden">
1561
- Every saved Ask Worca chat: transcripts, attachments and the git worktrees checked out for them. Runs started from a chat are not affected.
1562
- </span></button>
1563
- </div>
1564
- <button type="button" id="askHistoryDelete" class="ask-history-delete" disabled>Delete all chat history</button>
1565
- <small class="hint" id="askHistoryCounts"></small>
1566
- <small class="hint" id="askHistoryMsg"></small>
1567
- </div>
1568
- </section>
1569
-
1570
1608
  <!-- Scheduled runs: the defaults a NEW schedule inherits. Each schedule keeps
1571
1609
  its own copy, so a change here never rewrites an existing one. -->
1572
1610
  <section class="card settings-card" id="schedule-settings-card" data-min-level="advanced">
@@ -1618,95 +1656,6 @@
1618
1656
  <small class="hint" id="schedDefaultsMsg"></small>
1619
1657
  </section>
1620
1658
 
1621
- <!-- Title generation (#422): which model writes run/chat titles.
1622
- Empty = the model of the run or chat itself (title.mjs
1623
- resolveTitleModel), so a first-party-free install needs no setup.
1624
- The select is built from the project-less catalog; a stored id
1625
- that left the catalog paints disabled + "not installed". -->
1626
- <section class="card settings-card" id="title-model-settings-card" data-min-level="expert">
1627
- <div class="card-head">
1628
- <div class="label-row">
1629
- <h2>Title generation</h2>
1630
- <button type="button" class="info-tip" aria-label="About title generation">i<span class="tip-content hidden">
1631
- Every run and chat gets a short LLM-written title. By default it is written by the
1632
- model the run or chat itself uses, so an install without a first-party Anthropic
1633
- account needs no setup here. Pick a model to write every title with it instead;
1634
- the change applies to the next title, no restart. A non-empty
1635
- <code>WORCA_TITLE_MODEL</code> in the environment overrides this setting.
1636
- </span></button>
1637
- </div>
1638
- </div>
1639
- <div class="field">
1640
- <div class="label-row">
1641
- <label for="titleModel">Model</label>
1642
- </div>
1643
- <select id="titleModel" class="select" aria-label="Title model"></select>
1644
- </div>
1645
- <small class="hint" id="titleModelEnvNote"></small>
1646
- <div class="add-project-actions" style="margin-top:14px">
1647
- <button type="button" id="titleModelTest" class="btn btn-ghost btn-mini" title="Send one tiny prompt to the selected model">Test</button>
1648
- <button type="button" id="titleModelReset" class="btn btn-ghost btn-mini">Use default</button>
1649
- <button type="button" id="titleModelSave" class="btn btn-primary btn-mini">Save</button>
1650
- </div>
1651
- <small class="hint" id="titleModelMsg"></small>
1652
- </section>
1653
-
1654
- <!-- Auto workflow model (spec D14 / §7.7): which model classifies a task into a workflow.
1655
- Empty = the Sonnet-class default; WORCA_AUTO_MODEL in the environment wins. -->
1656
- <section class="card settings-card" id="auto-model-settings-card" data-min-level="expert">
1657
- <div class="card-head">
1658
- <div class="label-row">
1659
- <h2>Auto workflow model</h2>
1660
- <button type="button" class="info-tip" aria-label="About the Auto workflow model">i<span class="tip-content hidden">
1661
- When a run starts on <b>Auto</b>, one short model call reads the task and picks the workflow.
1662
- Pick the model that makes that call; empty means the Sonnet-class default. A non-empty
1663
- <code>WORCA_AUTO_MODEL</code> in the environment overrides this setting. Each call is billed to the
1664
- run as a sub-agent row named "Auto workflow (round n)".
1665
- </span></button>
1666
- </div>
1667
- </div>
1668
- <div class="field">
1669
- <div class="label-row"><label for="autoModel">Model</label></div>
1670
- <select id="autoModel" class="select" aria-label="Auto workflow model"></select>
1671
- </div>
1672
- <small class="hint" id="autoModelEnvNote"></small>
1673
- <div class="add-project-actions" style="margin-top:14px">
1674
- <button type="button" id="autoModelTest" class="btn btn-ghost btn-mini" title="Send one tiny prompt to the selected model">Test</button>
1675
- <button type="button" id="autoModelReset" class="btn btn-ghost btn-mini">Use default</button>
1676
- <button type="button" id="autoModelSave" class="btn btn-primary btn-mini">Save</button>
1677
- </div>
1678
- <small class="hint" id="autoModelMsg"></small>
1679
- </section>
1680
-
1681
- <!-- Spawn-debug diagnostics: the stored side of WORCA_DEBUG_SPAWN.
1682
- claude-runner.mjs reads the setting per spawn (UI server AND CLI);
1683
- a non-empty env var overrides it. This card only flips the gate —
1684
- the runner decides what is printable (routing keys readable,
1685
- credentials as "<set, N chars>"). -->
1686
- <section class="card settings-card" id="debug-spawn-settings-card" data-min-level="expert">
1687
- <div class="card-head">
1688
- <div class="label-row">
1689
- <h2>Spawn diagnostics</h2>
1690
- <button type="button" class="info-tip" aria-label="About spawn diagnostics">i<span class="tip-content hidden">
1691
- Logs the exact claude binary, argv and routing env handed to every spawn, in the run
1692
- log. Credentials never appear: routing keys (endpoint, wire model) print readable,
1693
- every other value as <code>&lt;set, N chars&gt;</code>. Applies to runs started here
1694
- and from the CLI, with no restart. A non-empty <code>WORCA_DEBUG_SPAWN</code> in the
1695
- environment overrides this setting.
1696
- </span></button>
1697
- </div>
1698
- </div>
1699
- <div class="field">
1700
- <label class="check-row" for="debugSpawnEnabled"><input id="debugSpawnEnabled" type="checkbox" /> Log claude spawn details (binary, argv, routing env)</label>
1701
- </div>
1702
- <small class="hint" id="debugSpawnEnvNote"></small>
1703
- <div class="add-project-actions" style="margin-top:14px">
1704
- <button type="button" id="debugSpawnReset" class="btn btn-ghost btn-mini">Reset to default</button>
1705
- <button type="button" id="debugSpawnSave" class="btn btn-primary btn-mini">Save</button>
1706
- </div>
1707
- <small class="hint" id="debugSpawnMsg"></small>
1708
- </section>
1709
-
1710
1659
  <!-- Chat notifications (chat-connectivity-design.md §4.8): which run
1711
1660
  events ping which chat channels. Body rendered by
1712
1661
  chat-settings-view.mjs; app.js owns fetch/save/Test wiring. -->
@@ -1720,58 +1669,78 @@
1720
1669
  </div>
1721
1670
  <small class="hint" id="chatSettingsMsg"></small>
1722
1671
  </section>
1672
+ </section>
1673
+ <!-- /pane runs -->
1723
1674
 
1724
- <!-- Getting started: the checklist's Hide is undone here. Only the checklist —
1725
- the welcome dialog stays seen. No info-tip (the row explains itself);
1726
- the .hint is painted by app.js like every other settings status line. -->
1727
- <section class="card settings-card" id="getting-started-card" data-min-level="simple">
1728
- <div class="label-row">
1729
- <h2>Getting started</h2>
1730
- </div>
1731
- <div class="gs-settings-row">
1732
- <p class="gs-settings-text">The checklist on New pipeline walks through the first nine things to do with Worca. Hidden it, or want the guided tour back?</p>
1733
- <button type="button" id="gsShowAgain" class="btn btn-ghost btn-mini">Show again</button>
1675
+ <!-- ===== TAB: ASK WORCA — the assistant chat's limits, tools, web access and history ===== -->
1676
+ <section class="settings-pane hidden" data-tab="ask">
1677
+ <div class="topbar">
1678
+ <div>
1679
+ <h1>Ask Worca</h1>
1680
+ <div class="sub">The assistant chat: how far one turn may go, which tools and websites it may use, and its saved history</div>
1734
1681
  </div>
1735
- <small class="hint" id="gsSettingsMsg"></small>
1736
- </section>
1682
+ </div>
1737
1683
 
1738
- <!-- About: intentionally no info-tip and no .hint (test/ui-settings-tooltips.test.mjs pins both). -->
1739
- <section class="card settings-card" id="about-card" data-min-level="simple">
1740
- <div class="label-row">
1741
- <h2>About</h2>
1684
+ <!-- Ask Worca (ask-worca-design §6.9): per-turn guards for the
1685
+ assistant chat. Read fresh on every turn (D12); a stored null
1686
+ budget means "no cap". Posted ALONE — never with root/budget keys. -->
1687
+ <!-- Simple on purpose: the Ask Worca tab (Advanced) already gates it, and a deep link at a lower mode
1688
+ must still show the card under the level banner (docs/ui-levels.md rule 3). -->
1689
+ <section class="card settings-card" id="ask-settings-card" data-min-level="simple">
1690
+ <div class="card-head">
1691
+ <div class="label-row">
1692
+ <h2>Limits &amp; access</h2>
1693
+ <button type="button" class="info-tip" aria-label="About Ask Worca limits">i<span class="tip-content hidden">
1694
+ Per-turn guards for the Ask Worca assistant chat. A chat turn stops when it reaches the turn limit or the per-turn cost cap; both are read fresh on every turn.
1695
+ </span></button>
1696
+ </div>
1742
1697
  </div>
1743
- <div class="about-row">
1744
- <span class="about-key">Version</span>
1745
- <!-- href is painted from app.releaseUrl (the worca-app-v<version> tag);
1746
- without one the anchor is plain text, so the placeholder never links. -->
1747
- <a class="about-val about-version" id="aboutVersion"
1748
- target="_blank" rel="noopener noreferrer">&mdash;</a>
1698
+ <div class="field">
1699
+ <div class="label-row">
1700
+ <label for="askMaxTurns">Turn limit</label>
1701
+ <button type="button" class="info-tip" aria-label="About Turn limit">i<span class="tip-content hidden">
1702
+ Maximum assistant/tool turns one chat message may use (1&ndash;500). Leave empty to restore the default of 400.
1703
+ </span></button>
1704
+ </div>
1705
+ <input id="askMaxTurns" class="input" type="number" min="1" max="500" step="1" inputmode="numeric" placeholder="400" autocomplete="off" />
1749
1706
  </div>
1750
- <div class="about-row">
1751
- <span class="about-key">Repository</span>
1752
- <a class="about-val about-link" id="aboutRepoLink"
1753
- href="https://github.com/SinishaDjukic/worca-cc"
1754
- target="_blank" rel="noopener noreferrer">github.com/SinishaDjukic/worca-cc</a>
1707
+ <div class="field">
1708
+ <div class="label-row">
1709
+ <label for="askMaxBudgetUsd">Per-turn cost cap (USD)</label>
1710
+ <button type="button" class="info-tip" aria-label="About Per-turn cost cap">i<span class="tip-content hidden">
1711
+ There is no cap by default. Untick No cap and enter an amount (0.1&ndash;100) to turn the guard on: a chat turn then stops when its estimated cost reaches it. Leaving the amount empty, or pressing Use defaults, means no cap.
1712
+ </span></button>
1713
+ </div>
1714
+ <input id="askMaxBudgetUsd" class="input" type="number" min="0.1" max="100" step="0.1" inputmode="decimal" placeholder="No cap" autocomplete="off" />
1715
+ <label class="check-row" for="askNoCap"><input id="askNoCap" type="checkbox" /> No cap</label>
1755
1716
  </div>
1756
- <div class="about-row">
1757
- <span class="about-key">Feedback</span>
1758
- <!-- hrefs are repainted from app.bugsUrl (package.json bugs.url); the
1759
- static fallback keeps the links usable before /api/settings lands.
1760
- Anchors only: ui-settings-about.test.mjs forbids a control here,
1761
- and neither the label nor the href carries a version string. -->
1762
- <a class="about-val about-link" id="aboutBugLink"
1763
- href="https://github.com/SinishaDjukic/worca-cc/issues/new?labels=bug"
1764
- target="_blank" rel="noopener noreferrer">Report a bug</a>
1717
+ <!-- Scripts workbench W20: the chat's save_script / test_script switch. Body
1718
+ rendered by chat-settings-view.mjs; saved with this card's Save button. -->
1719
+ <div class="field" id="ask-script-tools-host"></div>
1720
+ <!-- Ask web access (off by default): rendered by chat-settings-view.mjs; saved with this
1721
+ card's Save button, and only when one of its fields was touched. -->
1722
+ <div class="field" id="ask-web-host"></div>
1723
+ <div class="add-project-actions" style="margin-top:14px">
1724
+ <button type="button" id="askLimitsReset" class="btn btn-ghost btn-mini">Use defaults</button>
1725
+ <button type="button" id="askLimitsSave" class="btn btn-primary btn-mini">Save</button>
1765
1726
  </div>
1766
- <div class="about-row">
1767
- <span class="about-key"></span>
1768
- <a class="about-val about-link" id="aboutIdeaLink"
1769
- href="https://github.com/SinishaDjukic/worca-cc/issues/new?labels=enhancement"
1770
- target="_blank" rel="noopener noreferrer">Suggest an improvement</a>
1727
+ <small class="hint" id="askLimitsMsg"></small>
1728
+ <!-- Chat history: the counts paint with the view; the button opens a
1729
+ danger confirm quoting FRESH counts, then DELETE /api/ask/threads. -->
1730
+ <div class="field" style="margin-top:14px">
1731
+ <div class="label-row">
1732
+ <label for="askHistoryDelete">Chat history</label>
1733
+ <button type="button" class="info-tip" aria-label="About Chat history">i<span class="tip-content hidden">
1734
+ Every saved Ask Worca chat: transcripts, attachments and the git worktrees checked out for them. Runs started from a chat are not affected.
1735
+ </span></button>
1736
+ </div>
1737
+ <button type="button" id="askHistoryDelete" class="ask-history-delete" disabled>Delete all chat history</button>
1738
+ <small class="hint" id="askHistoryCounts"></small>
1739
+ <small class="hint" id="askHistoryMsg"></small>
1771
1740
  </div>
1772
1741
  </section>
1773
1742
  </section>
1774
- <!-- /pane general -->
1743
+ <!-- /pane ask -->
1775
1744
 
1776
1745
  <!-- ===== TAB: GUARDRAILS (was the top-level Guardrails view) ===== -->
1777
1746
  <section class="settings-pane hidden" data-tab="guardrails">
@@ -1790,41 +1759,42 @@
1790
1759
  </section>
1791
1760
  <!-- /pane guardrails -->
1792
1761
 
1793
- <!-- ===== TAB: MODELS (was the top-level Models view) ===== -->
1794
- <section class="settings-pane hidden" data-tab="models">
1762
+ <!-- ===== TAB: MEMORY (agent-memory-design.md §10) ===== -->
1763
+ <section class="settings-pane hidden" data-tab="memory">
1795
1764
  <div class="topbar">
1796
1765
  <div>
1797
- <h1>Models</h1>
1798
- <div class="sub">The global model catalog — available in every project; entries can override built-ins, route through a provider, or carry per-model routing env. Sign-ins and endpoints live on the Providers tab.</div>
1799
- </div>
1800
- <div style="display:flex;gap:8px;flex:0 0 auto">
1801
- <button type="button" id="model-share-btn" class="btn-ghost" style="padding:11px 20px;white-space:nowrap">Share as plugin…</button>
1802
- <button type="button" id="model-import-btn" class="btn-ghost" style="padding:11px 20px;white-space:nowrap">Import models…</button>
1803
- <button type="button" id="model-create-btn" class="btn-go" style="padding:11px 20px;white-space:nowrap">
1804
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
1805
- Add model
1806
- </button>
1766
+ <h1>Memory</h1>
1767
+ <div class="sub">What worca remembers across runs and chats — the global rules and preferences every agent sees. A project's own memory lives on its project page (Projects → open a project → Memory).</div>
1807
1768
  </div>
1808
1769
  </div>
1809
- <p id="models-msg" class="form-msg" aria-live="polite"></p>
1810
- <div class="run-list" id="models-list"></div>
1811
- </section>
1812
- <!-- /pane models -->
1813
-
1814
- <!-- ===== TAB: PROVIDERS (model-bridge-design.md §8.1) =====
1815
- Split out of Models: the card is account-level setup you touch rarely, and at
1816
- 1000+px it buried the catalog it unlocks. Expert only, like Models. -->
1817
- <section class="settings-pane hidden" data-tab="providers">
1818
- <div class="topbar">
1819
- <div>
1820
- <h1>Providers</h1>
1821
- <div class="sub">Sign-ins, endpoints and keys for the models Worca bridges — GitHub Copilot, any OpenAI-compatible server (llama.cpp, Ollama, LM Studio, vLLM), an Anthropic-compatible gateway. The models themselves live on the Models tab.</div>
1770
+ <!-- Settings › Memory: the model + effort EVERY Memory defragment run uses (memory-defrag-model.mjs).
1771
+ Expert, like the health card's Defragment. No ⓘ and empty hints: the settings-view census
1772
+ (test/ui-settings-tooltips.test.mjs) pins both. A plain .card like the health card, NOT a
1773
+ .settings-card: that census (About last) counts the Settings cards. -->
1774
+ <section class="card mem-model-card" id="mem-defrag-model-card" data-min-level="expert">
1775
+ <div class="label-row"><h2>Defragment model</h2></div>
1776
+ <p class="mem-model-text">The model every Memory defragment run uses — from the Defragment button, New pipeline, an Ask Worca card or the CLI. A model named when the run starts (<code>worca --model</code>) still wins.</p>
1777
+ <div class="mem-model-row">
1778
+ <div class="field">
1779
+ <div class="label-row"><label for="memDefragModel">Model</label></div>
1780
+ <select id="memDefragModel" class="select" aria-label="Defragment model"></select>
1781
+ </div>
1782
+ <div class="field">
1783
+ <div class="label-row"><label for="memDefragEffort">Effort</label></div>
1784
+ <select id="memDefragEffort" class="select" aria-label="Defragment effort"></select>
1785
+ </div>
1822
1786
  </div>
1823
- </div>
1824
- <p id="providers-msg" class="form-msg" aria-live="polite"></p>
1825
- <div class="run-list" id="providers-list"></div>
1787
+ <small class="hint" id="memDefragModelNote"></small>
1788
+ <div class="add-project-actions" style="margin-top:14px">
1789
+ <button type="button" id="memDefragModelReset" class="btn btn-ghost btn-mini">Use default</button>
1790
+ <button type="button" id="memDefragModelSave" class="btn btn-primary btn-mini">Save</button>
1791
+ </div>
1792
+ <small class="hint" id="memDefragModelMsg"></small>
1793
+ </section>
1794
+ <p id="memory-msg" class="form-msg" aria-live="polite"></p>
1795
+ <div id="memory-host" class="mem-host"></div>
1826
1796
  </section>
1827
- <!-- /pane providers -->
1797
+ <!-- /pane memory -->
1828
1798
 
1829
1799
  <!-- ===== TAB: PLUGINS (was the top-level Plugins view) ===== -->
1830
1800
  <section class="settings-pane hidden" data-tab="plugins">
@@ -1856,42 +1826,104 @@
1856
1826
  </section>
1857
1827
  <!-- /pane plugins -->
1858
1828
 
1859
- <!-- ===== TAB: MEMORY (agent-memory-design.md §10) ===== -->
1860
- <section class="settings-pane hidden" data-tab="memory">
1829
+ <!-- ===== TAB: MODELS (was the top-level Models view) ===== -->
1830
+ <section class="settings-pane hidden" data-tab="models">
1861
1831
  <div class="topbar">
1862
1832
  <div>
1863
- <h1>Memory</h1>
1864
- <div class="sub">What worca remembers across runs and chats — the global rules and preferences every agent sees. A project's own memory lives on its project page (Projects → open a project → Memory).</div>
1833
+ <h1>Models</h1>
1834
+ <div class="sub">The global model catalog — available in every project; entries can override built-ins, route through a provider, or carry per-model routing env. The two cards on top pick the models Worca's own titles and Auto classifier use. Sign-ins and endpoints live on the Providers tab.</div>
1835
+ </div>
1836
+ <div style="display:flex;gap:8px;flex:0 0 auto">
1837
+ <button type="button" id="model-share-btn" class="btn-ghost" style="padding:11px 20px;white-space:nowrap">Share as plugin…</button>
1838
+ <button type="button" id="model-import-btn" class="btn-ghost" style="padding:11px 20px;white-space:nowrap">Import models…</button>
1839
+ <button type="button" id="model-create-btn" class="btn-go" style="padding:11px 20px;white-space:nowrap">
1840
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
1841
+ Add model
1842
+ </button>
1865
1843
  </div>
1866
1844
  </div>
1867
- <!-- Settings › Memory: the model + effort EVERY Memory defragment run uses (memory-defrag-model.mjs).
1868
- Expert, like the health card's Defragment. No ⓘ and empty hints: the settings-view census
1869
- (test/ui-settings-tooltips.test.mjs) pins both. A plain .card like the health card, NOT a
1870
- .settings-card: that census (About last, twelve cards) counts the General tab's cards. -->
1871
- <section class="card mem-model-card" id="mem-defrag-model-card" data-min-level="expert">
1872
- <div class="label-row"><h2>Defragment model</h2></div>
1873
- <p class="mem-model-text">The model every Memory defragment run uses — from the Defragment button, New pipeline, an Ask Worca card or the CLI. A model named when the run starts (<code>worca --model</code>) still wins.</p>
1874
- <div class="mem-model-row">
1845
+ <!-- The models Worca's own helper calls use (titles, the Auto classifier). Two columns on a
1846
+ wide screen so the catalog below is not pushed a screen down. -->
1847
+ <div class="models-helpers" data-min-level="expert">
1848
+ <!-- Title generation (#422): which model writes run/chat titles.
1849
+ Empty = the model of the run or chat itself (title.mjs
1850
+ resolveTitleModel), so a first-party-free install needs no setup.
1851
+ The select is built from the project-less catalog; a stored id
1852
+ that left the catalog paints disabled + "not installed". -->
1853
+ <section class="card settings-card" id="title-model-settings-card" data-min-level="expert">
1854
+ <div class="card-head">
1855
+ <div class="label-row">
1856
+ <h2>Title generation</h2>
1857
+ <button type="button" class="info-tip" aria-label="About title generation">i<span class="tip-content hidden">
1858
+ Every run and chat gets a short LLM-written title. By default it is written by the
1859
+ model the run or chat itself uses, so an install without a first-party Anthropic
1860
+ account needs no setup here. Pick a model to write every title with it instead;
1861
+ the change applies to the next title, no restart. A non-empty
1862
+ <code>WORCA_TITLE_MODEL</code> in the environment overrides this setting.
1863
+ </span></button>
1864
+ </div>
1865
+ </div>
1875
1866
  <div class="field">
1876
- <div class="label-row"><label for="memDefragModel">Model</label></div>
1877
- <select id="memDefragModel" class="select" aria-label="Defragment model"></select>
1867
+ <div class="label-row">
1868
+ <label for="titleModel">Model</label>
1869
+ </div>
1870
+ <select id="titleModel" class="select" aria-label="Title model"></select>
1871
+ </div>
1872
+ <small class="hint" id="titleModelEnvNote"></small>
1873
+ <div class="add-project-actions" style="margin-top:14px">
1874
+ <button type="button" id="titleModelTest" class="btn btn-ghost btn-mini" title="Send one tiny prompt to the selected model">Test</button>
1875
+ <button type="button" id="titleModelReset" class="btn btn-ghost btn-mini">Use default</button>
1876
+ <button type="button" id="titleModelSave" class="btn btn-primary btn-mini">Save</button>
1877
+ </div>
1878
+ <small class="hint" id="titleModelMsg"></small>
1879
+ </section>
1880
+
1881
+ <!-- Auto workflow model (spec D14 / §7.7): which model classifies a task into a workflow.
1882
+ Empty = the Sonnet-class default; WORCA_AUTO_MODEL in the environment wins. -->
1883
+ <section class="card settings-card" id="auto-model-settings-card" data-min-level="expert">
1884
+ <div class="card-head">
1885
+ <div class="label-row">
1886
+ <h2>Auto workflow model</h2>
1887
+ <button type="button" class="info-tip" aria-label="About the Auto workflow model">i<span class="tip-content hidden">
1888
+ When a run starts on <b>Auto</b>, one short model call reads the task and picks the workflow.
1889
+ Pick the model that makes that call; empty means the Sonnet-class default. A non-empty
1890
+ <code>WORCA_AUTO_MODEL</code> in the environment overrides this setting. Each call is billed to the
1891
+ run as a sub-agent row named "Auto workflow (round n)".
1892
+ </span></button>
1893
+ </div>
1878
1894
  </div>
1879
1895
  <div class="field">
1880
- <div class="label-row"><label for="memDefragEffort">Effort</label></div>
1881
- <select id="memDefragEffort" class="select" aria-label="Defragment effort"></select>
1896
+ <div class="label-row"><label for="autoModel">Model</label></div>
1897
+ <select id="autoModel" class="select" aria-label="Auto workflow model"></select>
1882
1898
  </div>
1899
+ <small class="hint" id="autoModelEnvNote"></small>
1900
+ <div class="add-project-actions" style="margin-top:14px">
1901
+ <button type="button" id="autoModelTest" class="btn btn-ghost btn-mini" title="Send one tiny prompt to the selected model">Test</button>
1902
+ <button type="button" id="autoModelReset" class="btn btn-ghost btn-mini">Use default</button>
1903
+ <button type="button" id="autoModelSave" class="btn btn-primary btn-mini">Save</button>
1904
+ </div>
1905
+ <small class="hint" id="autoModelMsg"></small>
1906
+ </section>
1907
+ </div>
1908
+ <p id="models-msg" class="form-msg" aria-live="polite"></p>
1909
+ <div class="run-list" id="models-list"></div>
1910
+ </section>
1911
+ <!-- /pane models -->
1912
+
1913
+ <!-- ===== TAB: PROVIDERS (model-bridge-design.md §8.1) =====
1914
+ Split out of Models: the card is account-level setup you touch rarely, and at
1915
+ 1000+px it buried the catalog it unlocks. Expert only, like Models. -->
1916
+ <section class="settings-pane hidden" data-tab="providers">
1917
+ <div class="topbar">
1918
+ <div>
1919
+ <h1>Providers</h1>
1920
+ <div class="sub">Sign-ins, endpoints and keys for the models Worca bridges — GitHub Copilot, any OpenAI-compatible server (llama.cpp, Ollama, LM Studio, vLLM), an Anthropic-compatible gateway. The models themselves live on the Models tab.</div>
1883
1921
  </div>
1884
- <small class="hint" id="memDefragModelNote"></small>
1885
- <div class="add-project-actions" style="margin-top:14px">
1886
- <button type="button" id="memDefragModelReset" class="btn btn-ghost btn-mini">Use default</button>
1887
- <button type="button" id="memDefragModelSave" class="btn btn-primary btn-mini">Save</button>
1888
- </div>
1889
- <small class="hint" id="memDefragModelMsg"></small>
1890
- </section>
1891
- <p id="memory-msg" class="form-msg" aria-live="polite"></p>
1892
- <div id="memory-host" class="mem-host"></div>
1922
+ </div>
1923
+ <p id="providers-msg" class="form-msg" aria-live="polite"></p>
1924
+ <div class="run-list" id="providers-list"></div>
1893
1925
  </section>
1894
- <!-- /pane memory -->
1926
+ <!-- /pane providers -->
1895
1927
  </section>
1896
1928
  <!-- /view settings -->
1897
1929
  </main>
@@ -2357,6 +2357,13 @@ section[data-view="team-metrics"] .topbar{align-items:flex-start;}
2357
2357
  /* Actor and Started never wrap: with the Saved column in, Title (ellipsis) and Workflow give way first. */
2358
2358
  .tm-run-tbl td:nth-last-child(-n+2){white-space:nowrap;}
2359
2359
  .badge.grey{background:var(--field);color:var(--ink-2);}
2360
+ /* The Copilot card's "read the notice first" pill is a button: it opens the notice (bridge-view.mjs). */
2361
+ .mv-cp-terms-pill{border:0;font-family:inherit;cursor:pointer;}
2362
+ .mv-cp-terms-pill:hover{background:var(--line);color:var(--ink);}
2363
+ .mv-cp-terms-pill:focus-visible{outline:2px solid var(--ink);outline-offset:2px;}
2364
+ /* With the credential broker, a provider pill for a missing key links to the key page. */
2365
+ .mv-broker-pill{text-decoration:none;cursor:pointer;}
2366
+ .mv-broker-pill:hover{text-decoration:underline;}
2360
2367
  .tm-empty{display:grid;grid-template-columns:1fr 1fr;gap:14px;} .tm-empty .card{padding:22px;}
2361
2368
  .tm-step{font-size:11px;letter-spacing:.08em;font-weight:700;color:var(--ink-2);}
2362
2369
  .tm-empty-foot{display:block;margin-top:14px;}
@@ -2622,6 +2629,10 @@ button.tl-tile[aria-pressed="true"]{border-color:var(--ink);}
2622
2629
  .settings-card{max-width:760px;padding:22px;margin-left:auto;margin-right:auto;}
2623
2630
  .settings-card + .settings-card{margin-top:18px;}
2624
2631
  .settings-card[hidden]{display:none;}
2632
+ /* Settings › Models: Title generation + Auto workflow model side by side above the catalog, so two
2633
+ short cards do not push it a screen down; one column once they would squeeze. */
2634
+ .models-helpers{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:18px;margin-bottom:18px;}
2635
+ .models-helpers > .settings-card{max-width:none;margin:0;}
2625
2636
 
2626
2637
  /* My model credentials (credential broker): one row per slot, a state chip on the right. */
2627
2638
  .cred-list{list-style:none;margin:0;padding:0;display:grid;gap:8px;}
@@ -5537,10 +5548,6 @@ a.ask-card-task-title:hover{text-decoration-color:currentColor;}
5537
5548
  :root[data-level="expert"] [data-max-level="simple"],
5538
5549
  :root[data-level="expert"] [data-max-level="advanced"]{display:none !important;}
5539
5550
 
5540
- /* Simple shows General only: a one-button segmented control is noise, so the bar goes — unless a
5541
- deep link parked us on a higher tab, whose button is then kept (paintLevelBanner). */
5542
- :root[data-level="simple"] #settings-tabs:not(:has([data-level-keep])){display:none !important;}
5543
-
5544
5551
  /* The icon: a stack of layers, unlit ones ghosted. currentColor, so hover, the
5545
5552
  dark active fill and both themes come for free. */
5546
5553
  /* An <i>, not a <span>: the collapsed rail visually hides every direct span child of a nav button