@worca/app 1.5.0 → 1.6.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- 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 +52 -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 +165 -9
- 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 +244 -43
- package/ui/public/ask-model.mjs +1 -0
- package/ui/public/ask-panel.mjs +160 -23
- package/ui/public/bridge-view.mjs +175 -6
- 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 +33 -6
- 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 +99 -50
- 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;
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -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();
|
|
7420
7488
|
}
|
|
7421
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;
|
|
7504
|
+
}
|
|
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 = [];
|
|
@@ -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;
|
|
@@ -12575,6 +12740,8 @@ function setProvidersMsg(text, kind) {
|
|
|
12575
12740
|
function renderProvidersViewBody() {
|
|
12576
12741
|
if (!el.providersList) return;
|
|
12577
12742
|
el.providersList.replaceChildren(renderProvidersCard(mvState.providers, { signIn: mvState.signIn, split: true }));
|
|
12743
|
+
// OpenRouter's free-model allowance, read afresh each time the card opens.
|
|
12744
|
+
refreshFreeDaily({ force: true });
|
|
12578
12745
|
}
|
|
12579
12746
|
|
|
12580
12747
|
/** A page-level message, on whichever of the two tabs is showing. */
|
|
@@ -12727,7 +12894,9 @@ async function testProviderFlow(btn) {
|
|
|
12727
12894
|
const unsaved = hasUnsavedProviderEdits(name, typed);
|
|
12728
12895
|
if (data.ok) {
|
|
12729
12896
|
setProviderResult(name, 'ok', `Reachable${data.models != null ? ` — ${data.models} model${data.models === 1 ? '' : 's'}` : ''}`);
|
|
12730
|
-
|
|
12897
|
+
// `detail` is what the endpoint says about the key itself — OpenRouter's credit, free-model
|
|
12898
|
+
// allowance and rate limit (provider-ops formatOpenRouterKeyInfo); never the key.
|
|
12899
|
+
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
12900
|
} else {
|
|
12732
12901
|
const why = data.message || data.error || `HTTP ${res.status}`;
|
|
12733
12902
|
setProviderResult(name, 'err', 'Failed');
|
|
@@ -12838,20 +13007,21 @@ function applyImportFilter() {
|
|
|
12838
13007
|
const sheet = el.mimpBody && el.mimpBody.querySelector('.mvi');
|
|
12839
13008
|
if (!sheet) return;
|
|
12840
13009
|
const q = (el.mimpFilter?.value || '').trim().toLowerCase();
|
|
12841
|
-
let shown = 0;
|
|
13010
|
+
let shown = 0; let total = 0;
|
|
12842
13011
|
for (const tr of sheet.querySelectorAll('tbody tr')) {
|
|
12843
|
-
const hit =
|
|
13012
|
+
const hit = endpointRowMatches(tr, sheet, q); // text, plus a hosted sheet's Free / Tools / window
|
|
12844
13013
|
tr.classList.toggle('is-filtered', !hit);
|
|
13014
|
+
total += 1;
|
|
12845
13015
|
if (hit) shown += 1;
|
|
12846
13016
|
}
|
|
12847
|
-
|
|
12848
|
-
if (!shown &&
|
|
13017
|
+
let none = sheet.querySelector('.mimp-nohits');
|
|
13018
|
+
if (!shown && total) {
|
|
12849
13019
|
if (!none) {
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
sheet.appendChild(d);
|
|
13020
|
+
none = document.createElement('div');
|
|
13021
|
+
none.className = 'hist-empty mimp-nohits';
|
|
13022
|
+
sheet.appendChild(none);
|
|
12854
13023
|
}
|
|
13024
|
+
none.textContent = q ? `Nothing here matches “${el.mimpFilter.value.trim()}”.` : 'Nothing here matches these filters.';
|
|
12855
13025
|
} else if (none) none.remove();
|
|
12856
13026
|
}
|
|
12857
13027
|
|
|
@@ -13131,7 +13301,11 @@ if (el.providersList) {
|
|
|
13131
13301
|
const body = collectProviderRow(providerRoot(), t.dataset.provider);
|
|
13132
13302
|
if (body) patchProviderFlow(t.dataset.provider, body);
|
|
13133
13303
|
} else if (t.classList.contains('mv-pv-test')) testProviderFlow(t);
|
|
13134
|
-
else if (t.classList.contains('mv-pv-
|
|
13304
|
+
else if (t.classList.contains('mv-pv-preset')) {
|
|
13305
|
+
// Fills the fields only; the row's Test / Save do the rest, exactly as for a typed URL.
|
|
13306
|
+
applyProviderPreset(providerRoot(), t.dataset.provider, t.dataset.preset);
|
|
13307
|
+
setProviderMsg(t.dataset.provider, 'OpenRouter filled in. Set the key (or keep ${OPENROUTER_KEY} and export it where Worca starts), Test connection, then Save.');
|
|
13308
|
+
} else if (t.classList.contains('mv-pv-browse')) {
|
|
13135
13309
|
// The import lands in the CATALOG, so it opens there — with this row's endpoint filled in.
|
|
13136
13310
|
const row = t.closest('.mv-pv-row');
|
|
13137
13311
|
const input = row && row.querySelector('.mv-pv-baseurl');
|
|
@@ -13290,7 +13464,7 @@ if (el.modelImportModal) {
|
|
|
13290
13464
|
const sheet = t.closest('.mvi');
|
|
13291
13465
|
// Select-all means what is ON SCREEN: ticking a row the filter hides would import a surprise.
|
|
13292
13466
|
if (sheet) for (const c of sheet.querySelectorAll('tbody tr:not(.is-filtered) .mvi-cb')) { if (!c.disabled) c.checked = t.checked; }
|
|
13293
|
-
}
|
|
13467
|
+
} else if (t && t.closest && t.closest('.mvi-or-filters')) applyImportFilter(); // Free / Tools / window
|
|
13294
13468
|
});
|
|
13295
13469
|
// Backdrop click and Escape close it, like every other overlay in this file.
|
|
13296
13470
|
el.modelImportModal.addEventListener('mousedown', (ev) => { if (ev.target === el.modelImportModal) closeImportDialog(); });
|
|
@@ -15223,8 +15397,10 @@ function cssEscape(s) {
|
|
|
15223
15397
|
// inserted BEFORE `.hist-open` so the chevron stays last in the aside.
|
|
15224
15398
|
function resetPrCluster(card) {
|
|
15225
15399
|
const aside = card.querySelector('.hist-aside');
|
|
15226
|
-
|
|
15227
|
-
const
|
|
15400
|
+
// A PR batch can land after its document is gone (a test harness swapping pages): nothing to patch.
|
|
15401
|
+
const tpl = $('#hist-card-tpl');
|
|
15402
|
+
if (!aside || !tpl) return;
|
|
15403
|
+
const freshPr = tpl.content.querySelector('.hist-pr').cloneNode(true);
|
|
15228
15404
|
const curPr = aside.querySelector('.hist-pr, .hist-pr-link'); // button OR the swapped-in link
|
|
15229
15405
|
if (curPr) curPr.replaceWith(freshPr);
|
|
15230
15406
|
else aside.insertBefore(freshPr, aside.querySelector('.hist-open'));
|
|
@@ -15248,8 +15424,8 @@ function patchHistoryPr({ projectKey, id, pr }) {
|
|
|
15248
15424
|
if (!card) return; // off-screen (filtered out) — model is enough
|
|
15249
15425
|
resetPrCluster(card);
|
|
15250
15426
|
setupPrButton(card, row?.projectDir || null, row || { id, projectKey, pr }, state.ghAvailable);
|
|
15251
|
-
// A MERGED enrichment
|
|
15252
|
-
//
|
|
15427
|
+
// A MERGED enrichment hides only a LIVE diff pill (the merge emptied its
|
|
15428
|
+
// source...feature diff); a row with frozen counts (`diffFrozen`) keeps showing them.
|
|
15253
15429
|
renderHistDiffPill(card.querySelector('.hist-diff-pill'), row || { id, projectKey, pr });
|
|
15254
15430
|
// No setMergePill: clarification B — merged-or-not is shown by the link swap inside
|
|
15255
15431
|
// setupPrButton (OPEN->"View PR", MERGED->"Merged"); the pill is detail-only now.
|
|
@@ -15991,14 +16167,18 @@ function renderHistCommentPill(pill, p) {
|
|
|
15991
16167
|
pill.title = `${n} unresolved diff comment${n === 1 ? '' : 's'}`;
|
|
15992
16168
|
}
|
|
15993
16169
|
|
|
15994
|
-
// Diff pill:
|
|
15995
|
-
//
|
|
16170
|
+
// Diff pill: frozen counts (`diffFrozen`, the run's own results.json summary) always
|
|
16171
|
+
// show — merged PR and branch gone alike — as +A −R, or "no diff" when the run
|
|
16172
|
+
// really changed nothing. Without them (a live or legacy run) the counts are the
|
|
16173
|
+
// live source...feature diff: survived with changes -> +A −R; survived with none ->
|
|
16174
|
+
// "no diff"; merged PR or branch gone -> hidden (that diff is empty or unknowable).
|
|
15996
16175
|
// NOTE: the minus glyph is U+2212 (−), not an ASCII hyphen; the jsdom test
|
|
15997
16176
|
// asserts it byte-for-byte, so keep this exact character.
|
|
15998
16177
|
function renderHistDiffPill(pill, p) {
|
|
15999
16178
|
if (!pill) return;
|
|
16179
|
+
const frozen = !!(p && p.diffFrozen);
|
|
16000
16180
|
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; }
|
|
16181
|
+
if (!p || (!frozen && (!p.survived || merged))) { pill.hidden = true; return; }
|
|
16002
16182
|
pill.hidden = false;
|
|
16003
16183
|
const added = Number.isFinite(+p.added) ? +p.added : 0;
|
|
16004
16184
|
const removed = Number.isFinite(+p.removed) ? +p.removed : 0;
|
|
@@ -16012,7 +16192,9 @@ function renderHistDiffPill(pill, p) {
|
|
|
16012
16192
|
const add = document.createElement('span'); add.className = 'diff-add'; add.textContent = `+${added}`;
|
|
16013
16193
|
const del = document.createElement('span'); del.className = 'diff-del'; del.textContent = `−${removed}`; // U+2212
|
|
16014
16194
|
diffEl.append(add, ' ', del);
|
|
16015
|
-
pill.title =
|
|
16195
|
+
pill.title = frozen
|
|
16196
|
+
? `${added} added, ${removed} removed by this run`
|
|
16197
|
+
: `${added} added, ${removed} removed vs ${p.sourceBranch || 'source'}`;
|
|
16016
16198
|
}
|
|
16017
16199
|
}
|
|
16018
16200
|
|
|
@@ -19757,6 +19939,8 @@ function rdStateCopy(r, stepName) {
|
|
|
19757
19939
|
return `Paused on a recoverable error${why}. Once it clears, Resume retries the step — the worktree and progress are kept.`;
|
|
19758
19940
|
}
|
|
19759
19941
|
if (r.pauseReason === 'usage_limit') {
|
|
19942
|
+
// OpenRouter's daily free requests: the detail already says when they come back and what to do.
|
|
19943
|
+
if (/^OpenRouter's free-model requests/.test(r.pauseDetail || '')) return `Paused — ${r.pauseDetail}.`;
|
|
19760
19944
|
return `Paused — session/usage limit reached${r.pauseDetail ? ` (${r.pauseDetail})` : ''}. Resume after the reset.`;
|
|
19761
19945
|
}
|
|
19762
19946
|
if (r.pauseReason && (r.status === 'paused' || r.status === 'pausing' || r.status === 'interrupted')) {
|
|
@@ -20894,6 +21078,7 @@ function paintAutoBadge(el, stepper) {
|
|
|
20894
21078
|
el.hidden = false;
|
|
20895
21079
|
el.textContent = decided ? `Auto → ${name}` : 'Auto';
|
|
20896
21080
|
el.title = !decided ? 'Auto is deciding the workflow'
|
|
21081
|
+
: auto.via === 'fallback' ? `The Auto classifier was unreachable${auto.reason ? ` (${auto.reason})` : ''} — running the default workflow "${name}"`
|
|
20897
21082
|
: auto.via === 'reused' ? `Auto reused the saved workflow "${name}"` : `Auto created the workflow "${name}"`;
|
|
20898
21083
|
el.classList.toggle('is-deciding', !decided);
|
|
20899
21084
|
}
|
|
@@ -22429,7 +22614,7 @@ function paintRdHeader(screen, r) {
|
|
|
22429
22614
|
// Model bridge (model-bridge-design.md §8.6): a run that went through the
|
|
22430
22615
|
// bridge shows its request count beside the dollars, so a "$0" reads as a
|
|
22431
22616
|
// unit mismatch (Copilot bills requests), not as free.
|
|
22432
|
-
['rd-cost', fmtUsd(r.totalCostUsd || 0) + bridgeRequestsSuffix(r.steps), true],
|
|
22617
|
+
['rd-cost', fmtUsd(r.totalCostUsd || 0) + (freeRequestsSuffix(r.steps) || bridgeRequestsSuffix(r.steps)), true],
|
|
22433
22618
|
['rd-step', stepText, false],
|
|
22434
22619
|
];
|
|
22435
22620
|
segs.forEach(([cls, txt, strong]) => {
|
|
@@ -22438,7 +22623,8 @@ function paintRdHeader(screen, r) {
|
|
|
22438
22623
|
const seg = document.createElement('span');
|
|
22439
22624
|
seg.className = cls + (strong ? ' strong' : '');
|
|
22440
22625
|
seg.textContent = txt;
|
|
22441
|
-
if (cls === 'rd-cost') seg.title = estTitle(r.totalCostUsd || 0) +
|
|
22626
|
+
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.';
|
|
22627
|
+
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
22628
|
meta.appendChild(seg);
|
|
22443
22629
|
});
|
|
22444
22630
|
|
|
@@ -22816,10 +23002,10 @@ function gsEnsurePillHost() {
|
|
|
22816
23002
|
return gsPillHost;
|
|
22817
23003
|
}
|
|
22818
23004
|
|
|
22819
|
-
async function loadOnboarding() {
|
|
23005
|
+
async function loadOnboarding({ recheck = false } = {}) {
|
|
22820
23006
|
let data;
|
|
22821
23007
|
try {
|
|
22822
|
-
const res = await fetch('/api/onboarding');
|
|
23008
|
+
const res = await fetch(recheck ? '/api/onboarding?recheck=1' : '/api/onboarding');
|
|
22823
23009
|
data = await safeJson(res);
|
|
22824
23010
|
if (!res.ok) return;
|
|
22825
23011
|
} catch { return; }
|
|
@@ -22926,10 +23112,13 @@ function paintClaudeSetupStatus() {
|
|
|
22926
23112
|
if (!box || !gs.status) return;
|
|
22927
23113
|
const c = gs.status.claude || {};
|
|
22928
23114
|
const ok = !!gs.status.steps.claude;
|
|
23115
|
+
const bin = c.bin || 'claude';
|
|
22929
23116
|
box.className = `ob-claude-status ${ok ? 'ok' : 'err'}`;
|
|
22930
|
-
box.textContent =
|
|
22931
|
-
|
|
22932
|
-
|
|
23117
|
+
if (ok) box.textContent = c.auth === 'signed-in' ? `Found ${bin}, installed and signed in — you're set.` : `Found ${bin} — you're set.`;
|
|
23118
|
+
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.`;
|
|
23119
|
+
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).`;
|
|
23120
|
+
// The wizards' signed-out lines are stale once the CLI checks out.
|
|
23121
|
+
if (ok) for (const restore of [...claudeSignedOutHints.values()]) restore();
|
|
22933
23122
|
}
|
|
22934
23123
|
function openClaudeSetup() {
|
|
22935
23124
|
const modal = document.getElementById('claude-setup-modal');
|
|
@@ -22944,7 +23133,7 @@ document.getElementById('claude-setup-modal')?.addEventListener('click', (e) =>
|
|
|
22944
23133
|
document.getElementById('claude-setup-check')?.addEventListener('click', async () => {
|
|
22945
23134
|
const btn = document.getElementById('claude-setup-check');
|
|
22946
23135
|
btn.disabled = true;
|
|
22947
|
-
try { await loadOnboarding(); } finally { btn.disabled = false; }
|
|
23136
|
+
try { await loadOnboarding({ recheck: true }); } finally { btn.disabled = false; }
|
|
22948
23137
|
paintClaudeSetupStatus();
|
|
22949
23138
|
});
|
|
22950
23139
|
document.addEventListener('keydown', (e) => {
|
|
@@ -24094,6 +24283,17 @@ refreshAllCounts();
|
|
|
24094
24283
|
refreshBudget();
|
|
24095
24284
|
startBudgetTick();
|
|
24096
24285
|
loadWhoami();
|
|
24286
|
+
// Credential broker badges ("your key / no key" on model pickers): fetched at boot and
|
|
24287
|
+
// again when the tab regains focus — people add keys on the key page in another tab.
|
|
24288
|
+
// Pickers already on screen repaint once the answer changes what they show.
|
|
24289
|
+
{
|
|
24290
|
+
const repaintPickers = () => {
|
|
24291
|
+
if (currentView() === 'new') refreshNewPipelinePickers();
|
|
24292
|
+
if (currentView() === 'settings' && currentSettingsTab === 'models') loadModelsView();
|
|
24293
|
+
};
|
|
24294
|
+
loadCredentials().then((d) => { if (d) repaintPickers(); });
|
|
24295
|
+
window.addEventListener('focus', () => { loadCredentials().then((d) => { if (d) repaintPickers(); }); });
|
|
24296
|
+
}
|
|
24097
24297
|
|
|
24098
24298
|
// Ask Worca mount (§10.2 seam 1): a JS-built body-level overlay — index.html is
|
|
24099
24299
|
// untouched so ui-shell's routed-view census stays at 11. No network happens here;
|
|
@@ -24112,6 +24312,7 @@ askPanel = createAskPanel({
|
|
|
24112
24312
|
getPageContext,
|
|
24113
24313
|
openNewPipeline,
|
|
24114
24314
|
openComposer: (id) => { openComposerFromAsk(id); },
|
|
24315
|
+
openClaudeSetup: () => { openClaudeSetup(); },
|
|
24115
24316
|
loadMarkdown: loadAskMarkdown,
|
|
24116
24317
|
hljsLoader: diffHljsLoader,
|
|
24117
24318
|
storage: window.localStorage,
|
package/ui/public/ask-model.mjs
CHANGED
|
@@ -190,6 +190,7 @@ export function createThreadModel({ threadId }) {
|
|
|
190
190
|
row.text = live.text;
|
|
191
191
|
row.status = 'error';
|
|
192
192
|
row.errorMessage = frame.message || 'unknown error';
|
|
193
|
+
row.errorCode = frame.code || null;
|
|
193
194
|
live = null;
|
|
194
195
|
inFlight = null;
|
|
195
196
|
dirty.structure = true;
|