@worca/app 1.5.0 → 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 +8 -2
- package/docker/compose.broker.yml +79 -0
- package/docker/compose.isolation.yml +40 -0
- package/package.json +2 -1
- package/src/broker/config.mjs +200 -0
- package/src/broker/copilot.mjs +124 -0
- package/src/broker/limits.mjs +88 -0
- package/src/broker/main.mjs +129 -0
- package/src/broker/scrub.mjs +45 -0
- package/src/broker/service.mjs +558 -0
- package/src/broker/slots.mjs +180 -0
- package/src/broker/store.mjs +171 -0
- package/src/broker/tokens.mjs +116 -0
- package/src/broker/ui/page.css +54 -0
- package/src/broker/ui/page.html +25 -0
- package/src/broker/ui/page.mjs +202 -0
- package/src/broker/ui-server.mjs +217 -0
- package/src/broker/usage.mjs +108 -0
- package/src/broker/vault.mjs +37 -0
- package/src/cli/models.mjs +46 -14
- package/src/cli/render.mjs +21 -0
- package/src/cli/runs.mjs +296 -0
- package/src/cli/worca-cc.mjs +14 -2
- package/src/core/agent-pool.mjs +76 -0
- package/src/core/artifacts.mjs +40 -8
- package/src/core/ask/events.mjs +11 -0
- package/src/core/ask/html-text.mjs +113 -0
- package/src/core/ask/limits.mjs +5 -0
- package/src/core/ask/mcp-stdio.mjs +74 -20
- package/src/core/ask/prompt.mjs +25 -6
- package/src/core/ask/spawn.mjs +55 -5
- package/src/core/ask/store.mjs +1 -1
- package/src/core/ask/tools.mjs +66 -1
- package/src/core/ask/turn.mjs +56 -3
- package/src/core/ask/web-access.mjs +26 -0
- package/src/core/ask/web-deps.mjs +56 -0
- package/src/core/ask/web-fetch.mjs +271 -0
- package/src/core/ask/web-proposal.mjs +76 -0
- package/src/core/auto/classify.mjs +20 -8
- package/src/core/auto/runnable.mjs +28 -0
- package/src/core/billing.mjs +53 -0
- package/src/core/bridge/errors.mjs +77 -5
- package/src/core/bridge/openrouter.mjs +59 -0
- package/src/core/bridge/provider-ops.mjs +202 -10
- package/src/core/bridge/providers/endpoint.mjs +112 -7
- package/src/core/bridge/registry.mjs +13 -0
- package/src/core/bridge/server.mjs +16 -3
- package/src/core/bridge/telemetry.mjs +44 -12
- package/src/core/bridge/translate/common.mjs +31 -0
- package/src/core/bridge/translate/request.mjs +4 -1
- package/src/core/bridge/translate/response.mjs +3 -0
- package/src/core/bridge/translate/schema-keywords.mjs +75 -0
- package/src/core/bridge/translate/stream.mjs +50 -4
- package/src/core/bridge/upstream.mjs +102 -17
- package/src/core/broker-boot.mjs +57 -0
- package/src/core/broker-client.mjs +206 -0
- package/src/core/broker-guard.mjs +112 -0
- package/src/core/broker-routing.mjs +138 -0
- package/src/core/claude-auth.mjs +29 -0
- package/src/core/claude-runner.mjs +200 -9
- package/src/core/config.mjs +9 -12
- package/src/core/failure-policy.mjs +4 -2
- package/src/core/git-info.mjs +49 -5
- package/src/core/github-credentials.mjs +44 -1
- package/src/core/graph/script-runner.mjs +3 -1
- package/src/core/list-prices.mjs +29 -0
- package/src/core/mcp-secrets.mjs +80 -0
- package/src/core/metrics/sync.mjs +2 -1
- package/src/core/model-env.mjs +72 -0
- package/src/core/model-test.mjs +17 -5
- package/src/core/onboarding.mjs +12 -6
- package/src/core/openrouter-free.mjs +159 -0
- package/src/core/orchestrator.mjs +100 -12
- package/src/core/policy/effective.mjs +18 -1
- package/src/core/policy/local.mjs +4 -1
- package/src/core/policy/registry.mjs +12 -2
- package/src/core/preflight.mjs +116 -0
- package/src/core/recoverable-error.mjs +95 -0
- package/src/core/recovery-backoff.mjs +84 -0
- package/src/core/redact.mjs +25 -0
- package/src/core/run-context.mjs +6 -0
- package/src/core/run-harness.mjs +112 -30
- package/src/core/run-report.mjs +2 -1
- package/src/core/settings.mjs +90 -5
- package/src/core/title.mjs +7 -2
- package/src/core/web-allowlist.mjs +95 -0
- package/ui/public/app.js +271 -54
- package/ui/public/ask-model.mjs +1 -0
- package/ui/public/ask-panel.mjs +160 -23
- package/ui/public/bridge-view.mjs +216 -9
- package/ui/public/chat-settings-view.mjs +52 -1
- package/ui/public/credential-badges.mjs +63 -0
- package/ui/public/credentials-view.mjs +57 -0
- package/ui/public/index.html +303 -244
- package/ui/public/models-view.mjs +19 -1
- package/ui/public/openrouter-free-view.mjs +118 -0
- package/ui/public/stats-view.mjs +56 -0
- package/ui/public/style.css +110 -54
- package/ui/public/team-policy-view.mjs +2 -1
- package/ui/public/ws-seq.mjs +24 -0
- package/ui/server.mjs +319 -23
package/ui/public/app.js
CHANGED
|
@@ -68,6 +68,7 @@ const state = {
|
|
|
68
68
|
|
|
69
69
|
import { logLineClass, logLineTime, serializeLog, cycleSeparatorBefore, newCycleState, projectLogRecord } from './log-line.mjs';
|
|
70
70
|
import { logLineVisible, logFacets, compileLogFilter } from './log-filter.mjs';
|
|
71
|
+
import { alreadyApplied, noteBoot } from './ws-seq.mjs';
|
|
71
72
|
import { decorFromState, applyDecor, isGraphManifest } from './graph/run-decor.mjs';
|
|
72
73
|
import { mountRunGraph } from './graph/run-hosts.mjs';
|
|
73
74
|
// Import list only — `statusChip`/`diffBadges`/`mergeFindings`/`reportResultControl`
|
|
@@ -106,7 +107,10 @@ import {
|
|
|
106
107
|
renderConfigForm, collectConfigForm, renderConnectResult, renderDoctorReport, renderReferences409,
|
|
107
108
|
renderOrphanList, channelBadge, renderAvailableList, renderMarketplaceList,
|
|
108
109
|
} from './plugins-view.mjs';
|
|
109
|
-
import { renderChatSettings, collectChatSettings, renderScriptToolsToggle, collectScriptToolsToggle } from './chat-settings-view.mjs';
|
|
110
|
+
import { renderChatSettings, collectChatSettings, renderScriptToolsToggle, collectScriptToolsToggle, renderAskWebFields, collectAskWebFields } from './chat-settings-view.mjs';
|
|
111
|
+
import { renderCredentials } from './credentials-view.mjs';
|
|
112
|
+
import { loadCredentials, credentialSuffix } from './credential-badges.mjs';
|
|
113
|
+
import { renderFreeDaily, freeRequestsSuffix, typicalFreeRun, newRunFreeWarning, providerFreeLine } from './openrouter-free-view.mjs';
|
|
110
114
|
import { PORT_ID_RE, MAX_PORTS_PER_SIDE, PORT_TYPES, FLOW_LABEL, KEYED_KINDS } from '../../src/shared/graph/constants.mjs';
|
|
111
115
|
import { FORM_ID_RE, validateFormDef, normalizeAskBlock } from '../../src/shared/forms/form-def.mjs';
|
|
112
116
|
import { ASK_LIMITS } from '../../src/shared/forms/catalog.mjs';
|
|
@@ -121,6 +125,7 @@ import {
|
|
|
121
125
|
} from './models-view.mjs';
|
|
122
126
|
import {
|
|
123
127
|
renderProvidersCard, collectProviderRow, renderImportSheet, renderEndpointSheet, collectImportSheet, applyImportSelectAll,
|
|
128
|
+
applyProviderPreset, endpointRowMatches,
|
|
124
129
|
setModelUpstream, COPILOT_TERMS,
|
|
125
130
|
} from './bridge-view.mjs';
|
|
126
131
|
import {
|
|
@@ -301,6 +306,7 @@ const el = {
|
|
|
301
306
|
// Wizard
|
|
302
307
|
wizName: $('#wiz-name'),
|
|
303
308
|
wizProjects: $('#wiz-projects'),
|
|
309
|
+
wizSelectAll: $('#wiz-select-all'),
|
|
304
310
|
wizStep1Hint: $('#wiz-step1-hint'),
|
|
305
311
|
wizStartScan: $('#wiz-start-scan'),
|
|
306
312
|
wizStatus: $('#wiz-status'),
|
|
@@ -600,6 +606,7 @@ function setSidebarCollapsed(v) {
|
|
|
600
606
|
updateNavCounts(); // Running's title/aria-label (both states)
|
|
601
607
|
renderPipelineTabs(); // child rows <-> initials tiles (phase 3)
|
|
602
608
|
paintBudget(); // spend block <-> budget ring (phase 4)
|
|
609
|
+
paintFreeDaily(); // the free-request line shows only in the full rail
|
|
603
610
|
}
|
|
604
611
|
|
|
605
612
|
$('#side-toggle')?.addEventListener('click', () => setSidebarCollapsed(!sidebarCollapsed));
|
|
@@ -672,7 +679,63 @@ let startSubmitInFlight = false;
|
|
|
672
679
|
// budgetState.budget until a reload. Record the request instead and run one
|
|
673
680
|
// trailing fetch per in-flight window — any number of requests made during a
|
|
674
681
|
// fetch collapse into that single trailing fetch.
|
|
682
|
+
// ── OpenRouter free-model requests (openrouter-free-view.mjs) ────────────────
|
|
683
|
+
// Refreshed with the budget (every spend-moving event and its tick), at most every 20s:
|
|
684
|
+
// the server keeps its own reading of OpenRouter and lowers it per :free call.
|
|
685
|
+
const freeDailyState = { status: null, lastFetchMs: 0, fetching: false };
|
|
686
|
+
|
|
687
|
+
async function refreshFreeDaily({ force = false } = {}) {
|
|
688
|
+
if (freeDailyState.fetching || (!force && Date.now() - freeDailyState.lastFetchMs < 20_000)) return;
|
|
689
|
+
freeDailyState.fetching = true;
|
|
690
|
+
freeDailyState.lastFetchMs = Date.now();
|
|
691
|
+
try {
|
|
692
|
+
const res = await fetch(`/api/openrouter/free-daily${force ? '?refresh=1' : ''}`);
|
|
693
|
+
if (res.ok) freeDailyState.status = await safeJson(res);
|
|
694
|
+
} catch { /* transient: keep the last one */ } finally { freeDailyState.fetching = false; }
|
|
695
|
+
paintFreeDaily();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/** The free-request line rides the spend mount, under the spend block (full rail only). */
|
|
699
|
+
function paintFreeDaily() {
|
|
700
|
+
const mount = document.getElementById('side-spend');
|
|
701
|
+
if (!mount) return;
|
|
702
|
+
mount.querySelector('.free-ind')?.remove();
|
|
703
|
+
const node = sidebarCollapsed ? null : renderFreeDaily(freeDailyState.status);
|
|
704
|
+
if (node) mount.appendChild(node);
|
|
705
|
+
applyFreeDailyToNewView();
|
|
706
|
+
if (currentView() === 'settings' && currentSettingsTab === 'providers') paintProviderFreeDaily();
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** The new-run form: warn when a node's model is :free and fewer requests are left than a run takes. */
|
|
710
|
+
function applyFreeDailyToNewView() {
|
|
711
|
+
const note = document.getElementById('newFreeNote');
|
|
712
|
+
if (!note) return;
|
|
713
|
+
const s = freeDailyState.status;
|
|
714
|
+
const free = new Set((s && Array.isArray(s.models) ? s.models : []).map((m) => String(m).toLowerCase()));
|
|
715
|
+
const usesFree = Object.values(agentRowsById || {}).some((r) => r && r.model && free.has(String(r.model).toLowerCase()));
|
|
716
|
+
const msg = newRunFreeWarning(s, { typical: typicalFreeRun(recentRunsForFree()), usesFree });
|
|
717
|
+
note.hidden = !msg;
|
|
718
|
+
note.textContent = msg || '';
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** The runs this page holds, newest first, for the typical-run figure. */
|
|
722
|
+
function recentRunsForFree() {
|
|
723
|
+
return [...runs.values()].sort((a, b) => String(b.startedAt || '').localeCompare(String(a.startedAt || '')));
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** The Providers card: the allowance under the OpenAI-compatible row, without pressing Test. */
|
|
727
|
+
function paintProviderFreeDaily() {
|
|
728
|
+
const line = providerFreeLine(freeDailyState.status);
|
|
729
|
+
const root = providerRoot();
|
|
730
|
+
const row = root && root.querySelector('.mv-pv-row[data-provider="openai"] .mv-pv-msg');
|
|
731
|
+
if (!row || !line || (row.textContent && !row.dataset.freeDaily)) return;
|
|
732
|
+
row.textContent = line;
|
|
733
|
+
row.dataset.freeDaily = '1';
|
|
734
|
+
row.className = 'hint mv-pv-msg';
|
|
735
|
+
}
|
|
736
|
+
|
|
675
737
|
async function refreshBudget() {
|
|
738
|
+
refreshFreeDaily();
|
|
676
739
|
if (budgetState.fetching) { budgetState.pending = true; return; }
|
|
677
740
|
budgetState.fetching = true;
|
|
678
741
|
try {
|
|
@@ -700,6 +763,7 @@ function paintBudget() {
|
|
|
700
763
|
const render = sidebarCollapsed ? renderBudgetRing : renderBudgetIndicator;
|
|
701
764
|
mount.replaceChildren(render(b,
|
|
702
765
|
{ fmt: { usd: fmtUsd, usd4: fmtUsd4, duration: fmtDuration, estTitle } }));
|
|
766
|
+
paintFreeDaily(); // the free-request line sits under the block this just replaced
|
|
703
767
|
}
|
|
704
768
|
if (topAmt) {
|
|
705
769
|
topAmt.hidden = false;
|
|
@@ -709,7 +773,7 @@ function paintBudget() {
|
|
|
709
773
|
topAmt.classList.toggle('over', !!b.blocked);
|
|
710
774
|
}
|
|
711
775
|
applyBudgetToNewView();
|
|
712
|
-
if (currentView() === 'settings') paintBudgetReadout();
|
|
776
|
+
if (currentView() === 'settings' && currentSettingsTab === 'runs') paintBudgetReadout();
|
|
713
777
|
repaintCostBanners();
|
|
714
778
|
}
|
|
715
779
|
|
|
@@ -855,7 +919,9 @@ function repaintPeople() {
|
|
|
855
919
|
// <nav> that navLinks snapshots at boot — so route it from a container listener
|
|
856
920
|
// rather than the [data-nav] delegation.
|
|
857
921
|
document.getElementById('side-spend').addEventListener('click', (e) => {
|
|
858
|
-
|
|
922
|
+
// The OpenRouter free-request line (same mount) opens the Providers tab, where its key lives.
|
|
923
|
+
if (e.target.closest('.free-ind')) location.hash = 'settings/providers';
|
|
924
|
+
else if (e.target.closest('.spend-ind')) location.hash = 'stats';
|
|
859
925
|
});
|
|
860
926
|
|
|
861
927
|
// ---------------------------------------------------------------------------
|
|
@@ -1000,7 +1066,7 @@ function handleServerMessage(msg) {
|
|
|
1000
1066
|
if (currentView() === 'projects') paintProjectPolicyCells(true);
|
|
1001
1067
|
if (currentView() === 'workspaces') paintWsPolicyLines(true);
|
|
1002
1068
|
if (currentView() === 'team-policy' && !tpState.editing) loadTeamPolicyView();
|
|
1003
|
-
if (currentView() === 'settings') paintTeamCapsReadout(true);
|
|
1069
|
+
if (currentView() === 'settings' && currentSettingsTab === 'runs') paintTeamCapsReadout(true);
|
|
1004
1070
|
if (currentView() === 'settings' && currentSettingsTab === 'plugins') paintPluginsPolicy(true);
|
|
1005
1071
|
if (currentView() === 'new') schedulePolicyLine();
|
|
1006
1072
|
return;
|
|
@@ -1084,6 +1150,9 @@ function handleServerMessage(msg) {
|
|
|
1084
1150
|
// resurrect the phantom.)
|
|
1085
1151
|
if ((msg.type === 'subagent' || msg.type === 'stepskills' || msg.type === 'stepgraphify' || msg.type === 'question-resolved') && !runs.has(msg.runId)) return;
|
|
1086
1152
|
const r = upsertRun({ runId: msg.runId });
|
|
1153
|
+
// A reconnect re-subscribes and the server replays the run's buffer: skip what this page
|
|
1154
|
+
// already applied, or every earlier log line shows twice (ws-seq.mjs).
|
|
1155
|
+
if (alreadyApplied(r, msg)) return;
|
|
1087
1156
|
|
|
1088
1157
|
switch (msg.type) {
|
|
1089
1158
|
case 'log':
|
|
@@ -1147,6 +1216,7 @@ function handleServerMessage(msg) {
|
|
|
1147
1216
|
function onHello(msg) {
|
|
1148
1217
|
const ws = state.ws;
|
|
1149
1218
|
const list = Array.isArray(msg.runs) ? msg.runs : [];
|
|
1219
|
+
noteBoot(state, msg.bootId, runs); // a restarted server numbers run events from 1 again
|
|
1150
1220
|
|
|
1151
1221
|
if (!helloSeeded) {
|
|
1152
1222
|
helloSeeded = true;
|
|
@@ -2785,7 +2855,7 @@ function renderModelEffortPair(modelSel, effortSel, caption, sel = {}) {
|
|
|
2785
2855
|
const ambiguous = m.custom === 'plugin' && labelCounts.get((m.label || m.id).toLowerCase()) > 1;
|
|
2786
2856
|
const viaSuffix = m.bridged && !(m.label || m.id).toLowerCase().includes(m.bridged) ? ` · ${m.bridged}` : '';
|
|
2787
2857
|
og.appendChild(option(m.id,
|
|
2788
|
-
m.label + (ambiguous ? ` (${m.plugin})` : '') + viaSuffix + (m.costUnreliable ? ' ⚠cost' : '') + (m.needsSignIn ? ' (needs sign-in)' : '')));
|
|
2858
|
+
m.label + (ambiguous ? ` (${m.plugin})` : '') + viaSuffix + (m.costUnreliable ? ' ⚠cost' : '') + (m.needsSignIn ? ' (needs sign-in)' : '') + credentialSuffix(m.id)));
|
|
2789
2859
|
}
|
|
2790
2860
|
modelSel.appendChild(og);
|
|
2791
2861
|
};
|
|
@@ -3346,6 +3416,7 @@ function renderAgentRows(rows) {
|
|
|
3346
3416
|
if (row.pinned) lockPinnedPair(modelSel, effortSel);
|
|
3347
3417
|
host.appendChild(card);
|
|
3348
3418
|
});
|
|
3419
|
+
applyFreeDailyToNewView(); // a :free model among the rows may need the allowance warning
|
|
3349
3420
|
}
|
|
3350
3421
|
|
|
3351
3422
|
// Fill a sub-agent model dropdown. '' is "unset" — the run resolves it to the
|
|
@@ -7368,11 +7439,7 @@ function renderWizardProjects() {
|
|
|
7368
7439
|
const projects = Array.isArray(state.projects) ? state.projects : [];
|
|
7369
7440
|
const usable = projects.filter((p) => p && p.exists);
|
|
7370
7441
|
|
|
7371
|
-
|
|
7372
|
-
el.wizStep1Hint.textContent = usable.length < 2
|
|
7373
|
-
? 'Onboard at least two projects (in New Pipeline) to create a workspace.'
|
|
7374
|
-
: 'Select two or more projects to scan their interconnections.';
|
|
7375
|
-
}
|
|
7442
|
+
resetWizStep1Hint();
|
|
7376
7443
|
|
|
7377
7444
|
projects.forEach((p) => {
|
|
7378
7445
|
if (!p || !p.path) return;
|
|
@@ -7417,8 +7484,37 @@ function renderWizardProjects() {
|
|
|
7417
7484
|
function syncWizardStartEnabled() {
|
|
7418
7485
|
const next = document.getElementById('wiz-start-scan');
|
|
7419
7486
|
if (next) next.disabled = state.wizard.selectedPaths.length < 2;
|
|
7487
|
+
syncWizardSelectAll();
|
|
7488
|
+
}
|
|
7489
|
+
|
|
7490
|
+
// The enabled (existing) project checkboxes: the only rows Select all may touch.
|
|
7491
|
+
function wizardUsableBoxes() {
|
|
7492
|
+
return el.wizProjects ? [...el.wizProjects.querySelectorAll('.wiz-proj-cb:not(:disabled)')] : [];
|
|
7493
|
+
}
|
|
7494
|
+
|
|
7495
|
+
// Select all reads checked when every usable project is picked, indeterminate when some are.
|
|
7496
|
+
function syncWizardSelectAll() {
|
|
7497
|
+
const all = el.wizSelectAll;
|
|
7498
|
+
if (!all) return;
|
|
7499
|
+
const boxes = wizardUsableBoxes();
|
|
7500
|
+
const picked = boxes.filter((b) => b.checked).length;
|
|
7501
|
+
all.disabled = boxes.length < 2;
|
|
7502
|
+
all.checked = boxes.length > 0 && picked === boxes.length;
|
|
7503
|
+
all.indeterminate = picked > 0 && picked < boxes.length;
|
|
7420
7504
|
}
|
|
7421
7505
|
|
|
7506
|
+
// Check or clear every usable project at once; a missing project's selection is left as is.
|
|
7507
|
+
if (el.wizSelectAll) el.wizSelectAll.addEventListener('change', () => {
|
|
7508
|
+
const on = el.wizSelectAll.checked;
|
|
7509
|
+
const set = new Set(state.wizard.selectedPaths);
|
|
7510
|
+
for (const cb of wizardUsableBoxes()) {
|
|
7511
|
+
cb.checked = on;
|
|
7512
|
+
if (on) set.add(cb.value); else set.delete(cb.value);
|
|
7513
|
+
}
|
|
7514
|
+
state.wizard.selectedPaths = [...set];
|
|
7515
|
+
syncWizardStartEnabled();
|
|
7516
|
+
});
|
|
7517
|
+
|
|
7422
7518
|
// The team-metrics home is no longer a wizard step: POST /api/workspaces adopts the one
|
|
7423
7519
|
// member that already records (autoMetricsHome), and every other case is "Choose…" on the
|
|
7424
7520
|
// workspace card — so creating a workspace never blocks on a feature most users have not
|
|
@@ -7465,7 +7561,7 @@ async function startWizardScan() {
|
|
|
7465
7561
|
state.wizard.abort = null;
|
|
7466
7562
|
setStatusText('');
|
|
7467
7563
|
showWizardStep(1);
|
|
7468
|
-
setWizStep1Error(data.error || `Scan failed (${res.status})
|
|
7564
|
+
setWizStep1Error(data.error || `Scan failed (${res.status})`, data.code);
|
|
7469
7565
|
return;
|
|
7470
7566
|
}
|
|
7471
7567
|
state.wizard.scanId = data.scanId;
|
|
@@ -7479,8 +7575,39 @@ async function startWizardScan() {
|
|
|
7479
7575
|
}
|
|
7480
7576
|
}
|
|
7481
7577
|
|
|
7482
|
-
function
|
|
7483
|
-
|
|
7578
|
+
function resetWizStep1Hint() {
|
|
7579
|
+
const hint = el.wizStep1Hint;
|
|
7580
|
+
if (!hint) return;
|
|
7581
|
+
const usable = (Array.isArray(state.projects) ? state.projects : []).filter((p) => p && p.exists);
|
|
7582
|
+
hint.classList.remove('err');
|
|
7583
|
+
claudeSignedOutHints.delete(hint);
|
|
7584
|
+
hint.textContent = usable.length < 2
|
|
7585
|
+
? 'Onboard at least two projects (in New Pipeline) to create a workspace.'
|
|
7586
|
+
: 'Select two or more projects to scan their interconnections.';
|
|
7587
|
+
}
|
|
7588
|
+
|
|
7589
|
+
// A signed-out Claude CLI (409 code 'claude-signed-out') gets one short red line
|
|
7590
|
+
// whose link opens the Connect Claude Code dialog. `restore` repaints the hint's
|
|
7591
|
+
// normal text once Check again there finds the CLI signed in (paintClaudeSetupStatus).
|
|
7592
|
+
const claudeSignedOutHints = new Map(); // hint element -> restore()
|
|
7593
|
+
function showClaudeSignedOut(hint, restore) {
|
|
7594
|
+
const link = document.createElement('a');
|
|
7595
|
+
link.href = '#';
|
|
7596
|
+
link.textContent = 'Sign in…';
|
|
7597
|
+
link.addEventListener('click', (e) => { e.preventDefault(); openClaudeSetup(); });
|
|
7598
|
+
hint.classList.add('err');
|
|
7599
|
+
hint.replaceChildren("Claude Code isn't signed in. ", link);
|
|
7600
|
+
claudeSignedOutHints.set(hint, restore);
|
|
7601
|
+
}
|
|
7602
|
+
|
|
7603
|
+
// Any other refusal keeps the "Scan error: …" text.
|
|
7604
|
+
function setWizStep1Error(message, code) {
|
|
7605
|
+
const hint = el.wizStep1Hint;
|
|
7606
|
+
if (!hint) return;
|
|
7607
|
+
if (code === 'claude-signed-out') { showClaudeSignedOut(hint, resetWizStep1Hint); return; }
|
|
7608
|
+
claudeSignedOutHints.delete(hint);
|
|
7609
|
+
hint.classList.add('err');
|
|
7610
|
+
hint.textContent = `Scan error: ${message}`;
|
|
7484
7611
|
}
|
|
7485
7612
|
|
|
7486
7613
|
// Persist at Step 3 Save: new → POST /api/workspaces; re-scan → PATCH :id.
|
|
@@ -9825,7 +9952,7 @@ async function startAgentGenerate() {
|
|
|
9825
9952
|
if (!res.ok || !data.genId) {
|
|
9826
9953
|
state.agentWizard.abort = null;
|
|
9827
9954
|
showAgentWizardStep(1);
|
|
9828
|
-
|
|
9955
|
+
setAgwStep1Error(data.error || res.status, data.code);
|
|
9829
9956
|
return;
|
|
9830
9957
|
}
|
|
9831
9958
|
state.agentWizard.genId = data.genId;
|
|
@@ -9835,10 +9962,27 @@ async function startAgentGenerate() {
|
|
|
9835
9962
|
if (err && err.name === 'AbortError') return;
|
|
9836
9963
|
state.agentWizard.abort = null;
|
|
9837
9964
|
showAgentWizardStep(1);
|
|
9838
|
-
|
|
9965
|
+
setAgwStep1Error(err.message);
|
|
9839
9966
|
}
|
|
9840
9967
|
}
|
|
9841
9968
|
|
|
9969
|
+
const AGW_STEP1_HINT = 'Name + purpose are required (or paste your own markdown).';
|
|
9970
|
+
function resetAgwStep1Hint() {
|
|
9971
|
+
const hint = el.agwStep1Hint;
|
|
9972
|
+
if (!hint) return;
|
|
9973
|
+
claudeSignedOutHints.delete(hint);
|
|
9974
|
+
hint.classList.remove('err');
|
|
9975
|
+
hint.textContent = AGW_STEP1_HINT;
|
|
9976
|
+
}
|
|
9977
|
+
function setAgwStep1Error(message, code) {
|
|
9978
|
+
const hint = el.agwStep1Hint;
|
|
9979
|
+
if (!hint) return;
|
|
9980
|
+
if (code === 'claude-signed-out') { showClaudeSignedOut(hint, resetAgwStep1Hint); return; }
|
|
9981
|
+
claudeSignedOutHints.delete(hint);
|
|
9982
|
+
hint.classList.add('err');
|
|
9983
|
+
hint.textContent = `Generation error: ${message}`;
|
|
9984
|
+
}
|
|
9985
|
+
|
|
9842
9986
|
function onAgentGenEvent(msg) {
|
|
9843
9987
|
if (!msg || !msg.genId || msg.genId !== state.agentWizard.genId) return; // stale/aborted gen
|
|
9844
9988
|
if (msg.type === 'agentgen-progress') {
|
|
@@ -9863,7 +10007,7 @@ function onAgentGenEvent(msg) {
|
|
|
9863
10007
|
state.agentWizard.abort = null;
|
|
9864
10008
|
state.agentWizard.genId = '';
|
|
9865
10009
|
showAgentWizardStep(1);
|
|
9866
|
-
|
|
10010
|
+
setAgwStep1Error(msg.message || 'failed');
|
|
9867
10011
|
}
|
|
9868
10012
|
}
|
|
9869
10013
|
|
|
@@ -10414,6 +10558,7 @@ async function loadSettings() {
|
|
|
10414
10558
|
paintTeamCapsReadout(); // team policy (design board 7): each home's caps, read-only
|
|
10415
10559
|
refreshBudget();
|
|
10416
10560
|
paintChatSettings(data.chat);
|
|
10561
|
+
paintCredentials();
|
|
10417
10562
|
loadAskHistory();
|
|
10418
10563
|
setSettingsMsg('');
|
|
10419
10564
|
} catch (e) { setSettingsMsg(e.message, 'err'); }
|
|
@@ -10427,6 +10572,19 @@ function setChatSettingsMsg(text, cls) {
|
|
|
10427
10572
|
el.chatSettingsMsg.className = `hint${cls ? ` ${cls}` : ''}`;
|
|
10428
10573
|
}
|
|
10429
10574
|
|
|
10575
|
+
// Settings › My model credentials (credential broker, docs/credential-broker.md): the card
|
|
10576
|
+
// stays hidden unless worca runs with a broker. Status only; keys live on the key page.
|
|
10577
|
+
async function paintCredentials() {
|
|
10578
|
+
const card = document.getElementById('credentials-card');
|
|
10579
|
+
const host = document.getElementById('credentialsHost');
|
|
10580
|
+
if (!card || !host) return;
|
|
10581
|
+
let data;
|
|
10582
|
+
try { data = await safeJson(await fetch('/api/credentials')); } catch { data = { enabled: false }; }
|
|
10583
|
+
card.hidden = !data || data.enabled !== true;
|
|
10584
|
+
if (card.hidden) return;
|
|
10585
|
+
host.replaceChildren(renderCredentials(data));
|
|
10586
|
+
}
|
|
10587
|
+
|
|
10430
10588
|
async function paintChatSettings(prefs) {
|
|
10431
10589
|
if (!el.chatSettingsHost) return;
|
|
10432
10590
|
let channels = [];
|
|
@@ -10795,7 +10953,7 @@ try {
|
|
|
10795
10953
|
if (mq && typeof mq.addEventListener === 'function') mq.addEventListener('change', () => applyTheme(document.documentElement.dataset.theme));
|
|
10796
10954
|
} catch { /* no media queries here */ }
|
|
10797
10955
|
|
|
10798
|
-
// Settings ›
|
|
10956
|
+
// Settings › Runs › Scheduled runs: the defaults a new schedule inherits.
|
|
10799
10957
|
function setSchedDefaultsMsg(text, kind) { setHintMsg('schedDefaultsMsg', text, kind); }
|
|
10800
10958
|
function paintScheduleSettings(data) {
|
|
10801
10959
|
const d = data && data.schedule;
|
|
@@ -10839,6 +10997,9 @@ function paintAskSettings(data) {
|
|
|
10839
10997
|
// the GET and from every save response without a second fetch.
|
|
10840
10998
|
const scriptHost = document.getElementById('ask-script-tools-host');
|
|
10841
10999
|
if (scriptHost) scriptHost.replaceChildren(renderScriptToolsToggle({ prefs: data.chat || {} }, { doc: document }));
|
|
11000
|
+
// Web access: repainted from every GET and save response, which also resets its dirty flag.
|
|
11001
|
+
const webHost = document.getElementById('ask-web-host');
|
|
11002
|
+
if (webHost) webHost.replaceChildren(renderAskWebFields({ askWeb: data.askWeb }, { doc: document }));
|
|
10842
11003
|
}
|
|
10843
11004
|
function postAskLimits(body) {
|
|
10844
11005
|
return postSettingsCard(body, { setMsg: setAskLimitsMsg, paint: paintAskSettings });
|
|
@@ -10861,7 +11022,11 @@ function saveAskLimits() {
|
|
|
10861
11022
|
askMaxBudgetUsd = b;
|
|
10862
11023
|
}
|
|
10863
11024
|
const scriptHost = document.getElementById('ask-script-tools-host');
|
|
10864
|
-
|
|
11025
|
+
const webHost = document.getElementById('ask-web-host');
|
|
11026
|
+
const askWebBody = webHost ? collectAskWebFields(webHost) : null; // null = the web fields were not touched
|
|
11027
|
+
postAskLimits({ askMaxTurns, askMaxBudgetUsd,
|
|
11028
|
+
...(scriptHost ? { chat: collectScriptToolsToggle(scriptHost) } : {}),
|
|
11029
|
+
...(askWebBody ? { askWeb: askWebBody } : {}) });
|
|
10865
11030
|
}
|
|
10866
11031
|
document.getElementById('askLimitsSave')?.addEventListener('click', saveAskLimits);
|
|
10867
11032
|
document.getElementById('askLimitsReset')?.addEventListener('click', () => postAskLimits({ askMaxTurns: '', askMaxBudgetUsd: '' }));
|
|
@@ -12528,7 +12693,7 @@ async function loadModelsView() {
|
|
|
12528
12693
|
if (!el.modelsList) return;
|
|
12529
12694
|
setModelsMsg('');
|
|
12530
12695
|
try {
|
|
12531
|
-
const [res, pres] = await Promise.all([fetch('/api/models'), fetch('/api/providers')]);
|
|
12696
|
+
const [res, pres] = await Promise.all([fetch('/api/models'), fetch('/api/providers'), loadCredentials()]);
|
|
12532
12697
|
const data = await safeJson(res);
|
|
12533
12698
|
if (!res.ok) return setModelsMsg(data.error || `HTTP ${res.status}`, 'err');
|
|
12534
12699
|
mvState.data = data;
|
|
@@ -12537,6 +12702,7 @@ async function loadModelsView() {
|
|
|
12537
12702
|
const pdata = await safeJson(pres);
|
|
12538
12703
|
mvState.providers = pres.ok ? pdata : null;
|
|
12539
12704
|
renderModelsViewBody();
|
|
12705
|
+
void paintHelperModelCards();
|
|
12540
12706
|
// "+ Add model…" from a picker on another page: the dialog opens once the catalog is here.
|
|
12541
12707
|
if (mvState.openEditorOnLoad) { mvState.openEditorOnLoad = false; openModelEditorDialog(); }
|
|
12542
12708
|
// Copilot's models feed the editor's upstream-id datalist; fetched in the
|
|
@@ -12551,6 +12717,18 @@ async function loadModelsView() {
|
|
|
12551
12717
|
}
|
|
12552
12718
|
}
|
|
12553
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
|
+
|
|
12554
12732
|
/** The Providers tab: the same card, on a page of its own (§8.1). */
|
|
12555
12733
|
async function loadProvidersView() {
|
|
12556
12734
|
if (!el.providersList) return;
|
|
@@ -12575,6 +12753,8 @@ function setProvidersMsg(text, kind) {
|
|
|
12575
12753
|
function renderProvidersViewBody() {
|
|
12576
12754
|
if (!el.providersList) return;
|
|
12577
12755
|
el.providersList.replaceChildren(renderProvidersCard(mvState.providers, { signIn: mvState.signIn, split: true }));
|
|
12756
|
+
// OpenRouter's free-model allowance, read afresh each time the card opens.
|
|
12757
|
+
refreshFreeDaily({ force: true });
|
|
12578
12758
|
}
|
|
12579
12759
|
|
|
12580
12760
|
/** A page-level message, on whichever of the two tabs is showing. */
|
|
@@ -12727,7 +12907,9 @@ async function testProviderFlow(btn) {
|
|
|
12727
12907
|
const unsaved = hasUnsavedProviderEdits(name, typed);
|
|
12728
12908
|
if (data.ok) {
|
|
12729
12909
|
setProviderResult(name, 'ok', `Reachable${data.models != null ? ` — ${data.models} model${data.models === 1 ? '' : 's'}` : ''}`);
|
|
12730
|
-
|
|
12910
|
+
// `detail` is what the endpoint says about the key itself — OpenRouter's credit, free-model
|
|
12911
|
+
// allowance and rate limit (provider-ops formatOpenRouterKeyInfo); never the key.
|
|
12912
|
+
setProviderMsg(name, `${where}${data.models != null ? ` answered with ${data.models} model${data.models === 1 ? '' : 's'}` : ' answered'}.${data.detail ? ` Key: ${data.detail}.` : ''}${unsaved ? ' Press Save to keep these settings.' : ''}`);
|
|
12731
12913
|
} else {
|
|
12732
12914
|
const why = data.message || data.error || `HTTP ${res.status}`;
|
|
12733
12915
|
setProviderResult(name, 'err', 'Failed');
|
|
@@ -12838,20 +13020,21 @@ function applyImportFilter() {
|
|
|
12838
13020
|
const sheet = el.mimpBody && el.mimpBody.querySelector('.mvi');
|
|
12839
13021
|
if (!sheet) return;
|
|
12840
13022
|
const q = (el.mimpFilter?.value || '').trim().toLowerCase();
|
|
12841
|
-
let shown = 0;
|
|
13023
|
+
let shown = 0; let total = 0;
|
|
12842
13024
|
for (const tr of sheet.querySelectorAll('tbody tr')) {
|
|
12843
|
-
const hit =
|
|
13025
|
+
const hit = endpointRowMatches(tr, sheet, q); // text, plus a hosted sheet's Free / Tools / window
|
|
12844
13026
|
tr.classList.toggle('is-filtered', !hit);
|
|
13027
|
+
total += 1;
|
|
12845
13028
|
if (hit) shown += 1;
|
|
12846
13029
|
}
|
|
12847
|
-
|
|
12848
|
-
if (!shown &&
|
|
13030
|
+
let none = sheet.querySelector('.mimp-nohits');
|
|
13031
|
+
if (!shown && total) {
|
|
12849
13032
|
if (!none) {
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
sheet.appendChild(d);
|
|
13033
|
+
none = document.createElement('div');
|
|
13034
|
+
none.className = 'hist-empty mimp-nohits';
|
|
13035
|
+
sheet.appendChild(none);
|
|
12854
13036
|
}
|
|
13037
|
+
none.textContent = q ? `Nothing here matches “${el.mimpFilter.value.trim()}”.` : 'Nothing here matches these filters.';
|
|
12855
13038
|
} else if (none) none.remove();
|
|
12856
13039
|
}
|
|
12857
13040
|
|
|
@@ -13131,7 +13314,11 @@ if (el.providersList) {
|
|
|
13131
13314
|
const body = collectProviderRow(providerRoot(), t.dataset.provider);
|
|
13132
13315
|
if (body) patchProviderFlow(t.dataset.provider, body);
|
|
13133
13316
|
} else if (t.classList.contains('mv-pv-test')) testProviderFlow(t);
|
|
13134
|
-
else if (t.classList.contains('mv-pv-
|
|
13317
|
+
else if (t.classList.contains('mv-pv-preset')) {
|
|
13318
|
+
// Fills the fields only; the row's Test / Save do the rest, exactly as for a typed URL.
|
|
13319
|
+
applyProviderPreset(providerRoot(), t.dataset.provider, t.dataset.preset);
|
|
13320
|
+
setProviderMsg(t.dataset.provider, 'OpenRouter filled in. Set the key (or keep ${OPENROUTER_KEY} and export it where Worca starts), Test connection, then Save.');
|
|
13321
|
+
} else if (t.classList.contains('mv-pv-browse')) {
|
|
13135
13322
|
// The import lands in the CATALOG, so it opens there — with this row's endpoint filled in.
|
|
13136
13323
|
const row = t.closest('.mv-pv-row');
|
|
13137
13324
|
const input = row && row.querySelector('.mv-pv-baseurl');
|
|
@@ -13290,7 +13477,7 @@ if (el.modelImportModal) {
|
|
|
13290
13477
|
const sheet = t.closest('.mvi');
|
|
13291
13478
|
// Select-all means what is ON SCREEN: ticking a row the filter hides would import a surprise.
|
|
13292
13479
|
if (sheet) for (const c of sheet.querySelectorAll('tbody tr:not(.is-filtered) .mvi-cb')) { if (!c.disabled) c.checked = t.checked; }
|
|
13293
|
-
}
|
|
13480
|
+
} else if (t && t.closest && t.closest('.mvi-or-filters')) applyImportFilter(); // Free / Tools / window
|
|
13294
13481
|
});
|
|
13295
13482
|
// Backdrop click and Escape close it, like every other overlay in this file.
|
|
13296
13483
|
el.modelImportModal.addEventListener('mousedown', (ev) => { if (ev.target === el.modelImportModal) closeImportDialog(); });
|
|
@@ -13712,7 +13899,7 @@ if (runListEl) {
|
|
|
13712
13899
|
if (runId) confirmCostOverride(runId, overrideBtn);
|
|
13713
13900
|
return;
|
|
13714
13901
|
}
|
|
13715
|
-
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; }
|
|
13716
13903
|
// Team-cap banner (team-policy design board 9): continue past, or open the page.
|
|
13717
13904
|
const pastBtn = e.target.closest && e.target.closest('.cb-past-team-cap');
|
|
13718
13905
|
if (pastBtn) {
|
|
@@ -14401,7 +14588,7 @@ if (tpSection) tpSection.addEventListener('click', async (e) => {
|
|
|
14401
14588
|
await handlePolicyPluginClick(e);
|
|
14402
14589
|
});
|
|
14403
14590
|
|
|
14404
|
-
// 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.
|
|
14405
14592
|
async function paintTeamCapsReadout(force = false) {
|
|
14406
14593
|
if (!el.teamCapsReadout) return;
|
|
14407
14594
|
const data = await loadTpScopes({ force });
|
|
@@ -15223,8 +15410,10 @@ function cssEscape(s) {
|
|
|
15223
15410
|
// inserted BEFORE `.hist-open` so the chevron stays last in the aside.
|
|
15224
15411
|
function resetPrCluster(card) {
|
|
15225
15412
|
const aside = card.querySelector('.hist-aside');
|
|
15226
|
-
|
|
15227
|
-
const
|
|
15413
|
+
// A PR batch can land after its document is gone (a test harness swapping pages): nothing to patch.
|
|
15414
|
+
const tpl = $('#hist-card-tpl');
|
|
15415
|
+
if (!aside || !tpl) return;
|
|
15416
|
+
const freshPr = tpl.content.querySelector('.hist-pr').cloneNode(true);
|
|
15228
15417
|
const curPr = aside.querySelector('.hist-pr, .hist-pr-link'); // button OR the swapped-in link
|
|
15229
15418
|
if (curPr) curPr.replaceWith(freshPr);
|
|
15230
15419
|
else aside.insertBefore(freshPr, aside.querySelector('.hist-open'));
|
|
@@ -15248,8 +15437,8 @@ function patchHistoryPr({ projectKey, id, pr }) {
|
|
|
15248
15437
|
if (!card) return; // off-screen (filtered out) — model is enough
|
|
15249
15438
|
resetPrCluster(card);
|
|
15250
15439
|
setupPrButton(card, row?.projectDir || null, row || { id, projectKey, pr }, state.ghAvailable);
|
|
15251
|
-
// A MERGED enrichment
|
|
15252
|
-
//
|
|
15440
|
+
// A MERGED enrichment hides only a LIVE diff pill (the merge emptied its
|
|
15441
|
+
// source...feature diff); a row with frozen counts (`diffFrozen`) keeps showing them.
|
|
15253
15442
|
renderHistDiffPill(card.querySelector('.hist-diff-pill'), row || { id, projectKey, pr });
|
|
15254
15443
|
// No setMergePill: clarification B — merged-or-not is shown by the link swap inside
|
|
15255
15444
|
// setupPrButton (OPEN->"View PR", MERGED->"Merged"); the pill is detail-only now.
|
|
@@ -15991,14 +16180,18 @@ function renderHistCommentPill(pill, p) {
|
|
|
15991
16180
|
pill.title = `${n} unresolved diff comment${n === 1 ? '' : 's'}`;
|
|
15992
16181
|
}
|
|
15993
16182
|
|
|
15994
|
-
// Diff pill:
|
|
15995
|
-
//
|
|
16183
|
+
// Diff pill: frozen counts (`diffFrozen`, the run's own results.json summary) always
|
|
16184
|
+
// show — merged PR and branch gone alike — as +A −R, or "no diff" when the run
|
|
16185
|
+
// really changed nothing. Without them (a live or legacy run) the counts are the
|
|
16186
|
+
// live source...feature diff: survived with changes -> +A −R; survived with none ->
|
|
16187
|
+
// "no diff"; merged PR or branch gone -> hidden (that diff is empty or unknowable).
|
|
15996
16188
|
// NOTE: the minus glyph is U+2212 (−), not an ASCII hyphen; the jsdom test
|
|
15997
16189
|
// asserts it byte-for-byte, so keep this exact character.
|
|
15998
16190
|
function renderHistDiffPill(pill, p) {
|
|
15999
16191
|
if (!pill) return;
|
|
16192
|
+
const frozen = !!(p && p.diffFrozen);
|
|
16000
16193
|
const merged = p && p.pr && typeof p.pr === 'object' && String(p.pr.state || '').toUpperCase() === 'MERGED';
|
|
16001
|
-
if (!p || !p.survived || merged) { pill.hidden = true; return; }
|
|
16194
|
+
if (!p || (!frozen && (!p.survived || merged))) { pill.hidden = true; return; }
|
|
16002
16195
|
pill.hidden = false;
|
|
16003
16196
|
const added = Number.isFinite(+p.added) ? +p.added : 0;
|
|
16004
16197
|
const removed = Number.isFinite(+p.removed) ? +p.removed : 0;
|
|
@@ -16012,7 +16205,9 @@ function renderHistDiffPill(pill, p) {
|
|
|
16012
16205
|
const add = document.createElement('span'); add.className = 'diff-add'; add.textContent = `+${added}`;
|
|
16013
16206
|
const del = document.createElement('span'); del.className = 'diff-del'; del.textContent = `−${removed}`; // U+2212
|
|
16014
16207
|
diffEl.append(add, ' ', del);
|
|
16015
|
-
pill.title =
|
|
16208
|
+
pill.title = frozen
|
|
16209
|
+
? `${added} added, ${removed} removed by this run`
|
|
16210
|
+
: `${added} added, ${removed} removed vs ${p.sourceBranch || 'source'}`;
|
|
16016
16211
|
}
|
|
16017
16212
|
}
|
|
16018
16213
|
|
|
@@ -17195,7 +17390,7 @@ function paintHdBanners(screen, record, data) {
|
|
|
17195
17390
|
{ pauseReason, pauseDetail, pipelineId: record.id, totalCostUsd: st.totalCostUsd },
|
|
17196
17391
|
{ budget: budgetState.budget || {}, fmt: { usd: fmtUsd, usd4: fmtUsd4, duration: fmtDuration, estTitle } });
|
|
17197
17392
|
const settingsBtn = banner.querySelector('.cb-settings');
|
|
17198
|
-
if (settingsBtn) settingsBtn.addEventListener('click', () => { location.hash = 'settings'; });
|
|
17393
|
+
if (settingsBtn) settingsBtn.addEventListener('click', () => { location.hash = 'settings/runs'; });
|
|
17199
17394
|
const overrideBtn = banner.querySelector('.cb-override');
|
|
17200
17395
|
if (overrideBtn) {
|
|
17201
17396
|
overrideBtn.addEventListener('click', () => {
|
|
@@ -19757,6 +19952,8 @@ function rdStateCopy(r, stepName) {
|
|
|
19757
19952
|
return `Paused on a recoverable error${why}. Once it clears, Resume retries the step — the worktree and progress are kept.`;
|
|
19758
19953
|
}
|
|
19759
19954
|
if (r.pauseReason === 'usage_limit') {
|
|
19955
|
+
// OpenRouter's daily free requests: the detail already says when they come back and what to do.
|
|
19956
|
+
if (/^OpenRouter's free-model requests/.test(r.pauseDetail || '')) return `Paused — ${r.pauseDetail}.`;
|
|
19760
19957
|
return `Paused — session/usage limit reached${r.pauseDetail ? ` (${r.pauseDetail})` : ''}. Resume after the reset.`;
|
|
19761
19958
|
}
|
|
19762
19959
|
if (r.pauseReason && (r.status === 'paused' || r.status === 'pausing' || r.status === 'interrupted')) {
|
|
@@ -20894,6 +21091,7 @@ function paintAutoBadge(el, stepper) {
|
|
|
20894
21091
|
el.hidden = false;
|
|
20895
21092
|
el.textContent = decided ? `Auto → ${name}` : 'Auto';
|
|
20896
21093
|
el.title = !decided ? 'Auto is deciding the workflow'
|
|
21094
|
+
: auto.via === 'fallback' ? `The Auto classifier was unreachable${auto.reason ? ` (${auto.reason})` : ''} — running the default workflow "${name}"`
|
|
20897
21095
|
: auto.via === 'reused' ? `Auto reused the saved workflow "${name}"` : `Auto created the workflow "${name}"`;
|
|
20898
21096
|
el.classList.toggle('is-deciding', !decided);
|
|
20899
21097
|
}
|
|
@@ -22352,7 +22550,7 @@ el.runDetail?.addEventListener('click', (e) => {
|
|
|
22352
22550
|
if (!r) return;
|
|
22353
22551
|
const override = e.target.closest && e.target.closest('.cb-override');
|
|
22354
22552
|
if (override) { confirmCostOverride(r.runId, override); return; } // async, fire-and-forget
|
|
22355
|
-
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; }
|
|
22356
22554
|
const past = e.target.closest && e.target.closest('.cb-past-team-cap');
|
|
22357
22555
|
if (past) { confirmPastTeamCap(r.runId, past); return; } // team-policy board 9
|
|
22358
22556
|
if (e.target.closest && e.target.closest('.cb-policy-open')) { location.hash = 'team-policy'; return; }
|
|
@@ -22429,7 +22627,7 @@ function paintRdHeader(screen, r) {
|
|
|
22429
22627
|
// Model bridge (model-bridge-design.md §8.6): a run that went through the
|
|
22430
22628
|
// bridge shows its request count beside the dollars, so a "$0" reads as a
|
|
22431
22629
|
// unit mismatch (Copilot bills requests), not as free.
|
|
22432
|
-
['rd-cost', fmtUsd(r.totalCostUsd || 0) + bridgeRequestsSuffix(r.steps), true],
|
|
22630
|
+
['rd-cost', fmtUsd(r.totalCostUsd || 0) + (freeRequestsSuffix(r.steps) || bridgeRequestsSuffix(r.steps)), true],
|
|
22433
22631
|
['rd-step', stepText, false],
|
|
22434
22632
|
];
|
|
22435
22633
|
segs.forEach(([cls, txt, strong]) => {
|
|
@@ -22438,7 +22636,8 @@ function paintRdHeader(screen, r) {
|
|
|
22438
22636
|
const seg = document.createElement('span');
|
|
22439
22637
|
seg.className = cls + (strong ? ' strong' : '');
|
|
22440
22638
|
seg.textContent = txt;
|
|
22441
|
-
if (cls === 'rd-cost') seg.title = estTitle(r.totalCostUsd || 0) +
|
|
22639
|
+
if (cls === 'rd-cost' && freeRequestsSuffix(r.steps)) seg.title = estTitle(r.totalCostUsd || 0) + ' Free requests: calls this run made to OpenRouter :free models (every call, tool-loop continuations too), out of the day\'s free allowance.';
|
|
22640
|
+
else if (cls === 'rd-cost') seg.title = estTitle(r.totalCostUsd || 0) + (bridgeRequestsSuffix(r.steps) ? ' Requests: calls this run initiated through the model bridge (Copilot bills premium requests, not tokens); tool-loop continuations are not counted.' : '');
|
|
22442
22641
|
meta.appendChild(seg);
|
|
22443
22642
|
});
|
|
22444
22643
|
|
|
@@ -22816,10 +23015,10 @@ function gsEnsurePillHost() {
|
|
|
22816
23015
|
return gsPillHost;
|
|
22817
23016
|
}
|
|
22818
23017
|
|
|
22819
|
-
async function loadOnboarding() {
|
|
23018
|
+
async function loadOnboarding({ recheck = false } = {}) {
|
|
22820
23019
|
let data;
|
|
22821
23020
|
try {
|
|
22822
|
-
const res = await fetch('/api/onboarding');
|
|
23021
|
+
const res = await fetch(recheck ? '/api/onboarding?recheck=1' : '/api/onboarding');
|
|
22823
23022
|
data = await safeJson(res);
|
|
22824
23023
|
if (!res.ok) return;
|
|
22825
23024
|
} catch { return; }
|
|
@@ -22926,10 +23125,13 @@ function paintClaudeSetupStatus() {
|
|
|
22926
23125
|
if (!box || !gs.status) return;
|
|
22927
23126
|
const c = gs.status.claude || {};
|
|
22928
23127
|
const ok = !!gs.status.steps.claude;
|
|
23128
|
+
const bin = c.bin || 'claude';
|
|
22929
23129
|
box.className = `ob-claude-status ${ok ? 'ok' : 'err'}`;
|
|
22930
|
-
box.textContent =
|
|
22931
|
-
|
|
22932
|
-
|
|
23130
|
+
if (ok) box.textContent = c.auth === 'signed-in' ? `Found ${bin}, installed and signed in — you're set.` : `Found ${bin} — you're set.`;
|
|
23131
|
+
else if (c.auth === 'signed-out') box.textContent = `Found ${bin}, installed but not signed in yet. Run claude in a terminal, type /login, then check again.`;
|
|
23132
|
+
else box.textContent = c.hint || `"${bin}" is not on the PATH of the Worca server. Install it, then check again (restart the UI if PATH changed).`;
|
|
23133
|
+
// The wizards' signed-out lines are stale once the CLI checks out.
|
|
23134
|
+
if (ok) for (const restore of [...claudeSignedOutHints.values()]) restore();
|
|
22933
23135
|
}
|
|
22934
23136
|
function openClaudeSetup() {
|
|
22935
23137
|
const modal = document.getElementById('claude-setup-modal');
|
|
@@ -22944,7 +23146,7 @@ document.getElementById('claude-setup-modal')?.addEventListener('click', (e) =>
|
|
|
22944
23146
|
document.getElementById('claude-setup-check')?.addEventListener('click', async () => {
|
|
22945
23147
|
const btn = document.getElementById('claude-setup-check');
|
|
22946
23148
|
btn.disabled = true;
|
|
22947
|
-
try { await loadOnboarding(); } finally { btn.disabled = false; }
|
|
23149
|
+
try { await loadOnboarding({ recheck: true }); } finally { btn.disabled = false; }
|
|
22948
23150
|
paintClaudeSetupStatus();
|
|
22949
23151
|
});
|
|
22950
23152
|
document.addEventListener('keydown', (e) => {
|
|
@@ -23465,11 +23667,11 @@ const VIEW_MIN_LEVEL = Object.freeze({
|
|
|
23465
23667
|
'team-metrics': 'expert', 'team-policy': 'expert', agents: 'expert', scripts: 'expert',
|
|
23466
23668
|
schedules: 'advanced',
|
|
23467
23669
|
});
|
|
23468
|
-
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' });
|
|
23469
23671
|
const VIEW_TITLES = Object.freeze({
|
|
23470
23672
|
stats: 'Statistics', composer: 'Workflow Composer', workspaces: 'Workspaces', 'workspace-create': 'Workspaces',
|
|
23471
23673
|
'agent-create': 'Create agent', 'team-metrics': 'Team metrics', 'team-policy': 'Team policy', agents: 'Agents', scripts: 'Scripts',
|
|
23472
|
-
guardrails: 'Guardrails', plugins: 'Plugins', memory: 'Memory', models: 'Models', providers: 'Providers',
|
|
23674
|
+
guardrails: 'Guardrails', plugins: 'Plugins', memory: 'Memory', models: 'Models', providers: 'Providers', ask: 'Ask Worca',
|
|
23473
23675
|
schedules: 'Schedules',
|
|
23474
23676
|
});
|
|
23475
23677
|
function pageMinLevel() {
|
|
@@ -23526,7 +23728,10 @@ document.addEventListener('worca:level', () => {
|
|
|
23526
23728
|
// The tab is the Settings view's hash param; a guardrail deep link nests its id
|
|
23527
23729
|
// behind it (#settings/guardrails/<id>). parseHash splits on the FIRST '/' only,
|
|
23528
23730
|
// so that is view 'settings', param 'guardrails/<id>' — no parseHash change.
|
|
23529
|
-
const SETTINGS_TABS = ['general', '
|
|
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'];
|
|
23530
23735
|
const settingsPanes = $$('[data-view="settings"] .settings-pane');
|
|
23531
23736
|
// Old top-level hashes keep working. The hashchange listener DROPS any view it
|
|
23532
23737
|
// does not know, so without this map a bookmark or an old in-app link would
|
|
@@ -23776,7 +23981,7 @@ function showSettingsTab(param = '') {
|
|
|
23776
23981
|
// request, and re-entry refetches (which is what lets grvExitWizard's
|
|
23777
23982
|
// '#settings/guardrails/<id>' -> '#settings/guardrails' hop reset the wizard).
|
|
23778
23983
|
paintLevelBanner();
|
|
23779
|
-
if (tab
|
|
23984
|
+
if (SETTINGS_FORM_TABS.includes(tab)) loadSettings();
|
|
23780
23985
|
if (tab === 'guardrails') loadGuardrailsView(sub);
|
|
23781
23986
|
if (tab === 'models') loadModelsView(sub);
|
|
23782
23987
|
if (tab === 'providers') loadProvidersView();
|
|
@@ -24094,6 +24299,17 @@ refreshAllCounts();
|
|
|
24094
24299
|
refreshBudget();
|
|
24095
24300
|
startBudgetTick();
|
|
24096
24301
|
loadWhoami();
|
|
24302
|
+
// Credential broker badges ("your key / no key" on model pickers): fetched at boot and
|
|
24303
|
+
// again when the tab regains focus — people add keys on the key page in another tab.
|
|
24304
|
+
// Pickers already on screen repaint once the answer changes what they show.
|
|
24305
|
+
{
|
|
24306
|
+
const repaintPickers = () => {
|
|
24307
|
+
if (currentView() === 'new') refreshNewPipelinePickers();
|
|
24308
|
+
if (currentView() === 'settings' && currentSettingsTab === 'models') loadModelsView();
|
|
24309
|
+
};
|
|
24310
|
+
loadCredentials().then((d) => { if (d) repaintPickers(); });
|
|
24311
|
+
window.addEventListener('focus', () => { loadCredentials().then((d) => { if (d) repaintPickers(); }); });
|
|
24312
|
+
}
|
|
24097
24313
|
|
|
24098
24314
|
// Ask Worca mount (§10.2 seam 1): a JS-built body-level overlay — index.html is
|
|
24099
24315
|
// untouched so ui-shell's routed-view census stays at 11. No network happens here;
|
|
@@ -24112,6 +24328,7 @@ askPanel = createAskPanel({
|
|
|
24112
24328
|
getPageContext,
|
|
24113
24329
|
openNewPipeline,
|
|
24114
24330
|
openComposer: (id) => { openComposerFromAsk(id); },
|
|
24331
|
+
openClaudeSetup: () => { openClaudeSetup(); },
|
|
24115
24332
|
loadMarkdown: loadAskMarkdown,
|
|
24116
24333
|
hljsLoader: diffHljsLoader,
|
|
24117
24334
|
storage: window.localStorage,
|