@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/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;
|
package/ui/public/ask-panel.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { openScheduleSheet, browserTimeZone } from './schedule-sheet.mjs';
|
|
8
8
|
import { formatInstant, describeRule } from '../../src/shared/schedule/recurrence.mjs';
|
|
9
9
|
import { createThreadModel } from './ask-model.mjs';
|
|
10
|
+
import { credentialBadge } from './credential-badges.mjs';
|
|
10
11
|
import { createMarkdownRenderer } from './ask-markdown.mjs';
|
|
11
12
|
import { createThinkingOrb } from './thinking-orb.mjs';
|
|
12
13
|
import { workflowPickerLabel } from './results-view.mjs';
|
|
@@ -164,21 +165,22 @@ const PILL_MORPH_IN_MS = 520;
|
|
|
164
165
|
const PILL_MORPH_OUT_MS = 800;
|
|
165
166
|
const PILL_SETTLE_FALLBACK_MS = PILL_MORPH_OUT_MS + 150;
|
|
166
167
|
|
|
167
|
-
export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContext, openNewPipeline, openComposer = null, loadMarkdown, hljsLoader, storage, raf, now, runStore = null }) {
|
|
168
|
-
const
|
|
168
|
+
export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContext, openNewPipeline, openComposer = null, openClaudeSetup = null, loadMarkdown, hljsLoader, storage, raf, now, runStore = null }) {
|
|
169
|
+
const homePick = browserPick(); // hoisted declaration (defined below)
|
|
169
170
|
const st = {
|
|
170
171
|
open: false,
|
|
171
172
|
threadId: null,
|
|
172
173
|
model: null, // createThreadModel for the active thread (Task 4+)
|
|
173
|
-
picker:
|
|
174
|
-
model: storedPick && storedPick.model ? storedPick.model : FALLBACK_PICK.model,
|
|
175
|
-
effort: storedPick ? storedPick.effort : FALLBACK_PICK.effort,
|
|
176
|
-
},
|
|
174
|
+
picker: homePick.picker,
|
|
177
175
|
// D11 provenance, tracked per slot: only a MODEL the user actually picked
|
|
178
176
|
// outranks the backend default. An effort-only record leaves the model slot
|
|
179
177
|
// unclaimed, so a later change to ASK_LIMITS.defaultModel still reaches here.
|
|
180
|
-
pickerFromStore:
|
|
181
|
-
effortFromStore:
|
|
178
|
+
pickerFromStore: homePick.pickerFromStore,
|
|
179
|
+
effortFromStore: homePick.effortFromStore,
|
|
180
|
+
// The picker shows the open thread's own last pick (its row's model/effort).
|
|
181
|
+
// It outranks the backend default like a stored pick, but it is the chat's,
|
|
182
|
+
// not the browser's: nothing sourced from a thread reaches worca-cc.ask.model.
|
|
183
|
+
pickerFromThread: false,
|
|
182
184
|
catalog: null,
|
|
183
185
|
// #397: the thread's project/workspace scope. pinned:false = Auto (follow the
|
|
184
186
|
// page — today's behaviour). label caches the display name once resolved.
|
|
@@ -237,11 +239,28 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
237
239
|
} catch { /* storage unavailable */ }
|
|
238
240
|
return null; // no stored pick — the catalog decides (D5/D6/D11)
|
|
239
241
|
}
|
|
242
|
+
/** The browser-level pick — what a new chat starts with: the stored record over the cold-start literal, with each slot's provenance. */
|
|
243
|
+
function browserPick() {
|
|
244
|
+
const stored = readStoredModel();
|
|
245
|
+
return {
|
|
246
|
+
picker: {
|
|
247
|
+
model: stored && stored.model ? stored.model : FALLBACK_PICK.model,
|
|
248
|
+
effort: stored ? stored.effort : FALLBACK_PICK.effort,
|
|
249
|
+
},
|
|
250
|
+
pickerFromStore: !!(stored && stored.model),
|
|
251
|
+
effortFromStore: stored !== null,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
240
254
|
function storeModel() {
|
|
241
255
|
// Provenance travels with the record: writing st.picker.model when the user never
|
|
242
256
|
// chose one would pin the cold-start literal (or a default they merely saw), and
|
|
243
|
-
// the backend would be authoritative exactly once per browser.
|
|
244
|
-
|
|
257
|
+
// the backend would be authoritative exactly once per browser. A thread's model
|
|
258
|
+
// is not the user's browser-level choice either: an effort picked on it leaves
|
|
259
|
+
// the record's model slot as it was.
|
|
260
|
+
const model = st.pickerFromThread
|
|
261
|
+
? (readStoredModel() || { model: null }).model
|
|
262
|
+
: (st.pickerFromStore ? st.picker.model : null);
|
|
263
|
+
const rec = { model, effort: st.picker.effort };
|
|
245
264
|
try { storage.setItem('worca-cc.ask.model', JSON.stringify(rec)); } catch { /* ignore */ }
|
|
246
265
|
}
|
|
247
266
|
function readStoredThread() { try { return storage.getItem('worca-cc.ask.thread') || null; } catch { return null; } }
|
|
@@ -1231,15 +1250,20 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1231
1250
|
}
|
|
1232
1251
|
|
|
1233
1252
|
function applyCatalogToPicker() {
|
|
1253
|
+
const list = st.catalog && Array.isArray(st.catalog.models) ? st.catalog.models : [];
|
|
1254
|
+
// A thread's model the catalog no longer has (a removed user model) falls back
|
|
1255
|
+
// to the browser-level pick, then the backend default. The thread is not
|
|
1256
|
+
// patched: its next send stores whatever is used.
|
|
1257
|
+
if (st.pickerFromThread && list.length && !catalogEntry(st.picker.model)) { restoreBrowserPick(); return; }
|
|
1234
1258
|
const fallback = catalogDefault();
|
|
1235
1259
|
// Each slot is decided by its own provenance: a stored MODEL outranks the backend
|
|
1236
1260
|
// default, and a stored EFFORT survives even when the model comes from the default.
|
|
1237
1261
|
// (effortFromStore ⊇ pickerFromStore — a stored model always carries its effort.)
|
|
1262
|
+
// A thread's pick claims both slots.
|
|
1238
1263
|
const wanted = {
|
|
1239
|
-
model: st.pickerFromStore ? st.picker.model : fallback.model,
|
|
1240
|
-
effort: st.effortFromStore ? st.picker.effort : fallback.effort,
|
|
1264
|
+
model: st.pickerFromThread || st.pickerFromStore ? st.picker.model : fallback.model,
|
|
1265
|
+
effort: st.pickerFromThread || st.effortFromStore ? st.picker.effort : fallback.effort,
|
|
1241
1266
|
};
|
|
1242
|
-
const list = st.catalog && Array.isArray(st.catalog.models) ? st.catalog.models : [];
|
|
1243
1267
|
const wantedEntry = catalogEntry(wanted.model);
|
|
1244
1268
|
// Unknown stored/default id -> the backend default -> the first model we do
|
|
1245
1269
|
// have that is not a hidden built-in (#422; a hidden id is still a valid pick).
|
|
@@ -1250,11 +1274,43 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1250
1274
|
const changed = next.model !== st.picker.model || next.effort !== st.picker.effort;
|
|
1251
1275
|
st.picker = next;
|
|
1252
1276
|
// D11: persist ONLY a repair of a pick the user actually made. Writing the
|
|
1253
|
-
// backend default here would make it authoritative exactly once, ever
|
|
1254
|
-
|
|
1277
|
+
// backend default here would make it authoritative exactly once, ever — and a
|
|
1278
|
+
// thread's pick is never the browser's.
|
|
1279
|
+
if (changed && st.pickerFromStore && !st.pickerFromThread) storeModel();
|
|
1255
1280
|
updatePickerButton();
|
|
1256
1281
|
}
|
|
1257
1282
|
|
|
1283
|
+
/** Back to the browser-level pick (new chat, a thread with no model of its own). */
|
|
1284
|
+
function restoreBrowserPick() {
|
|
1285
|
+
const home = browserPick();
|
|
1286
|
+
st.picker = home.picker;
|
|
1287
|
+
st.pickerFromStore = home.pickerFromStore;
|
|
1288
|
+
st.effortFromStore = home.effortFromStore;
|
|
1289
|
+
st.pickerFromThread = false;
|
|
1290
|
+
if (st.catalog) applyCatalogToPicker();
|
|
1291
|
+
else updatePickerButton();
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/** A thread SWITCH shows that thread's last model/effort; a row without one gets the browser-level pick. */
|
|
1295
|
+
function applyThreadPick(thread) {
|
|
1296
|
+
const t = thread && typeof thread === 'object' ? thread : null;
|
|
1297
|
+
if (!t || typeof t.model !== 'string' || !t.model || typeof t.effort !== 'string' || !t.effort) { restoreBrowserPick(); return; }
|
|
1298
|
+
st.picker = { model: t.model, effort: t.effort };
|
|
1299
|
+
st.pickerFromThread = true;
|
|
1300
|
+
if (st.catalog) applyCatalogToPicker(); // effort coerced against the entry; a dropped model falls back
|
|
1301
|
+
else updatePickerButton(); // the raw id shows until the catalog lands
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
/** Keep the pick on the open thread, so switching away and back finds it before any send. */
|
|
1305
|
+
function persistThreadPick() {
|
|
1306
|
+
const id = st.threadId;
|
|
1307
|
+
if (!id) return; // a brand-new chat: the first send stores it
|
|
1308
|
+
const body = JSON.stringify({ model: st.picker.model, effort: st.picker.effort });
|
|
1309
|
+
Promise.resolve()
|
|
1310
|
+
.then(() => fetch(`/api/ask/threads/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body }))
|
|
1311
|
+
.catch(() => { /* the next message stores the pick anyway */ });
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1258
1314
|
function loadCatalog() {
|
|
1259
1315
|
if (st.catalog) return Promise.resolve(st.catalog);
|
|
1260
1316
|
if (st.catalogLoading) return st.catalogLoading;
|
|
@@ -1313,7 +1369,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1313
1369
|
st.picker = { model: id, effort: coerceEffort(catalogEntry(id), st.picker.effort) };
|
|
1314
1370
|
st.pickerFromStore = true; // an explicit model choice claims the slot (D11)
|
|
1315
1371
|
st.effortFromStore = true;
|
|
1372
|
+
st.pickerFromThread = false; // the user's own pick now, not the thread's
|
|
1316
1373
|
storeModel();
|
|
1374
|
+
persistThreadPick();
|
|
1317
1375
|
updatePickerButton();
|
|
1318
1376
|
closePopover({ focusTrigger: false });
|
|
1319
1377
|
focusComposer();
|
|
@@ -1323,6 +1381,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1323
1381
|
st.picker = { ...st.picker, effort };
|
|
1324
1382
|
st.effortFromStore = true; // the effort only — the model slot is untouched (D11)
|
|
1325
1383
|
storeModel();
|
|
1384
|
+
persistThreadPick();
|
|
1326
1385
|
updatePickerButton();
|
|
1327
1386
|
closePopover({ focusTrigger: false });
|
|
1328
1387
|
focusComposer();
|
|
@@ -1355,6 +1414,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1355
1414
|
if (m.needsSignIn) {
|
|
1356
1415
|
item.appendChild(tag('needs sign-in', 'is-err', m.signInMessage || 'The provider behind this model is not usable yet — Settings › Models › Providers.'));
|
|
1357
1416
|
}
|
|
1417
|
+
// Credential broker: whether the signed-in person has the key this model spends from.
|
|
1418
|
+
const cb = credentialBadge(m.id);
|
|
1419
|
+
if (cb) item.appendChild(tag(cb.text, cb.missing ? 'is-err' : 'is-key', cb.title));
|
|
1358
1420
|
if (m.id === st.picker.model) item.appendChild(make('span', 'ask-model-check', '✓'));
|
|
1359
1421
|
return item;
|
|
1360
1422
|
};
|
|
@@ -1640,6 +1702,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
1640
1702
|
storeThread(null);
|
|
1641
1703
|
el.title.textContent = 'Ask Worca';
|
|
1642
1704
|
applyThreadScope(null); // #397: a brand-new chat starts on Auto
|
|
1705
|
+
restoreBrowserPick(); // …and on the browser-level pick, not the last chat's
|
|
1643
1706
|
pruneCardEls(); // st.model is already null — renderTranscript's keep set cannot see the old ids
|
|
1644
1707
|
renderTranscript();
|
|
1645
1708
|
updateMeters();
|
|
@@ -2404,6 +2467,55 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2404
2467
|
return { el: rootEl };
|
|
2405
2468
|
}
|
|
2406
2469
|
|
|
2470
|
+
/** The web card (propose_web_access): proposed → applied | failed, or declined. The exact URL is shown, so a
|
|
2471
|
+
* request that smuggles data is visible before the click. Every value is text. */
|
|
2472
|
+
function buildWebCard(block) {
|
|
2473
|
+
const card = block.card || {};
|
|
2474
|
+
const host = card.host || 'a website';
|
|
2475
|
+
if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${card.summary || `Read ${host}`}`) };
|
|
2476
|
+
const rootEl = make('div', `ask-card ask-mcard ask-webcard is-${block.state}`);
|
|
2477
|
+
rootEl.setAttribute('data-ask-webcard', block.state);
|
|
2478
|
+
const result = card.result || null;
|
|
2479
|
+
const head = make('div', 'ask-mcard-head');
|
|
2480
|
+
const title = block.state === 'failed' ? 'Web access not granted'
|
|
2481
|
+
: block.state === 'applied' ? (result && result.scope === 'always' ? 'Always allowed' : 'Allowed for this chat')
|
|
2482
|
+
: 'Ask Worca wants to read a new site';
|
|
2483
|
+
head.appendChild(make('span', 'ask-mcard-title', title));
|
|
2484
|
+
head.appendChild(make('span', 'ask-mcard-kind', 'Web'));
|
|
2485
|
+
rootEl.appendChild(head);
|
|
2486
|
+
const body = make('div', 'ask-mcard-body');
|
|
2487
|
+
const sum = make('div', 'ask-mcard-summary');
|
|
2488
|
+
if (block.state === 'applied') sum.appendChild(svgIcon(WF_ICO.check, 15, 2.4));
|
|
2489
|
+
sum.appendChild(make('span', null, host));
|
|
2490
|
+
body.appendChild(sum);
|
|
2491
|
+
if (card.reason) body.appendChild(make('div', 'ask-mcard-note', card.reason));
|
|
2492
|
+
const ul = make('ul', 'ask-mcard-changes');
|
|
2493
|
+
const li = make('li');
|
|
2494
|
+
li.appendChild(make('span', 'ask-mcard-change-label', 'URL'));
|
|
2495
|
+
const val = make('span', 'ask-mcard-change-val');
|
|
2496
|
+
val.appendChild(make('span', 'ask-mcard-after', String(card.url || '')));
|
|
2497
|
+
li.appendChild(val);
|
|
2498
|
+
ul.appendChild(li);
|
|
2499
|
+
body.appendChild(ul);
|
|
2500
|
+
if (block.state === 'failed') body.appendChild(make('div', 'ask-mcard-failed', `Could not allow: ${block.error || (result && result.error) || 'unknown error'}`));
|
|
2501
|
+
rootEl.appendChild(body);
|
|
2502
|
+
rootEl.appendChild(make('div', 'ask-card-err'));
|
|
2503
|
+
if (block.state === 'proposed') {
|
|
2504
|
+
const actions = make('div', 'ask-mcard-actions');
|
|
2505
|
+
const btn = (cls, text, attr) => { const b = make('button', cls, text); b.type = 'button'; b.setAttribute(attr, ''); return b; };
|
|
2506
|
+
const decline = btn('ask-card-not-now', 'Deny', 'data-ask-web-decline');
|
|
2507
|
+
decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
|
|
2508
|
+
const always = btn('ask-card-not-now', 'Always allow', 'data-ask-web-always');
|
|
2509
|
+
always.title = `Adds ${host} to Settings → Ask Worca → Web access`;
|
|
2510
|
+
always.addEventListener('click', () => postCard(block, rootEl, { state: 'applied', scope: 'always' }, always));
|
|
2511
|
+
const chat = btn('ask-card-start', 'Allow for this chat', 'data-ask-web-chat');
|
|
2512
|
+
chat.addEventListener('click', () => postCard(block, rootEl, { state: 'applied', scope: 'chat' }, chat));
|
|
2513
|
+
actions.append(make('span', 'ask-card-actions-spacer'), decline, always, chat);
|
|
2514
|
+
rootEl.appendChild(actions);
|
|
2515
|
+
}
|
|
2516
|
+
return { el: rootEl };
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2407
2519
|
/** The clone card (propose_clone_project): proposed → cloning → applied | failed, or declined. Every value is text. */
|
|
2408
2520
|
function buildCloneCard(block) {
|
|
2409
2521
|
const card = block.card || {};
|
|
@@ -2480,6 +2592,12 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2480
2592
|
closePopover({ focusTrigger: true });
|
|
2481
2593
|
});
|
|
2482
2594
|
item.appendChild(make('span', 'ask-model-name', m.label || m.id));
|
|
2595
|
+
const cb = credentialBadge(m.id);
|
|
2596
|
+
if (cb) {
|
|
2597
|
+
const t = make('span', `ask-model-tag ${cb.missing ? 'is-err' : 'is-key'}`, cb.text);
|
|
2598
|
+
t.title = cb.title;
|
|
2599
|
+
item.appendChild(t);
|
|
2600
|
+
}
|
|
2483
2601
|
if (m.id === cur().model) item.appendChild(make('span', 'ask-model-check', '✓'));
|
|
2484
2602
|
p.appendChild(item);
|
|
2485
2603
|
}
|
|
@@ -3122,7 +3240,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3122
3240
|
function isProgressBlock(block) {
|
|
3123
3241
|
const card = block.card || {};
|
|
3124
3242
|
if (card.type === PROGRESS_CARD_TYPE) return true;
|
|
3125
|
-
if (card.type === 'workflow' || card.type === 'metrics' || card.type === 'policy' || card.type === 'schedule' || card.type === 'model' || card.type === 'clone') return false;
|
|
3243
|
+
if (card.type === 'workflow' || card.type === 'metrics' || card.type === 'policy' || card.type === 'schedule' || card.type === 'model' || card.type === 'clone' || card.type === 'web') return false;
|
|
3126
3244
|
return block.state === 'started' || (block.state === 'failed' && !!block.runId);
|
|
3127
3245
|
}
|
|
3128
3246
|
function buildCard(block) {
|
|
@@ -3134,14 +3252,16 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3134
3252
|
const isSchedule = !!(block.card && block.card.type === 'schedule');
|
|
3135
3253
|
const isModel = !!(block.card && block.card.type === 'model');
|
|
3136
3254
|
const isClone = !!(block.card && block.card.type === 'clone');
|
|
3255
|
+
const isWeb = !!(block.card && block.card.type === 'web');
|
|
3137
3256
|
const isProgress = isProgressBlock(block);
|
|
3138
|
-
if (cached && cached.state === block.state && (isWorkflow || isMetrics || isSchedule || isModel || isClone || isProgress || block.state === 'proposed')) return cached.el;
|
|
3257
|
+
if (cached && cached.state === block.state && (isWorkflow || isMetrics || isSchedule || isModel || isClone || isWeb || isProgress || block.state === 'proposed')) return cached.el;
|
|
3139
3258
|
if (cached) disposeCardEntry(cached);
|
|
3140
3259
|
const built = isWorkflow ? buildWorkflowCard(block, cached)
|
|
3141
3260
|
: isMetrics ? buildMetricsCard(block)
|
|
3142
3261
|
: isSchedule ? buildScheduleCard(block)
|
|
3143
3262
|
: isModel ? buildModelCard(block)
|
|
3144
3263
|
: isClone ? buildCloneCard(block)
|
|
3264
|
+
: isWeb ? buildWebCard(block)
|
|
3145
3265
|
: isProgress ? buildProgressCard(block)
|
|
3146
3266
|
: { el: block.state === 'proposed' ? buildCardForm(block) : buildCardTerminal(block) };
|
|
3147
3267
|
st.cardEls.set(block.id, { el: built.el, state: block.state, handle: built.handle || null, dispose: built.dispose || null, animate: !!built.animate, cancelAnim: null, lastW: -1 });
|
|
@@ -3386,10 +3506,15 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3386
3506
|
// Stopped after — and nothing more while the turn is live: the orb row at the
|
|
3387
3507
|
// bottom of the message owns the elapsed and the meter, and printing either
|
|
3388
3508
|
// set twice is the noise this replaced. A turn that ended badly says so
|
|
3389
|
-
// instead of Done; nothing else marks a stop.
|
|
3509
|
+
// instead of Done; nothing else marks a stop. A turn that ended before any
|
|
3510
|
+
// result, or within a few ms (a signed-out CLI answers in ~20 ms), has no
|
|
3511
|
+
// duration worth printing: plain Stopped, never a dangling "Stopped after"
|
|
3512
|
+
// or "Stopped after 0.0s".
|
|
3390
3513
|
if (!isLive) {
|
|
3391
|
-
|
|
3392
|
-
|
|
3514
|
+
const shown = fmtElapsed(r.durationMs);
|
|
3515
|
+
const elapsed = shown && shown !== '0.0s' ? shown : '';
|
|
3516
|
+
if (stopped) parts.push(make('span', 'ask-activity-label', elapsed ? 'Stopped after' : 'Stopped'));
|
|
3517
|
+
parts.push(make('span', 'ask-activity-elapsed', elapsed));
|
|
3393
3518
|
parts.push(make('span', 'ask-activity-spacer'));
|
|
3394
3519
|
const meter = [fmtCtx(r.usage && r.usage.ctx), fmtUsd(r.costUsd)].filter(Boolean).join(' · ');
|
|
3395
3520
|
parts.push(make('span', 'ask-activity-meter', meter));
|
|
@@ -3515,7 +3640,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3515
3640
|
if (b.kind === 'notice') parts.push(`n:${b.id ?? ''}:${b.text || ''}:${b.href || ''}`);
|
|
3516
3641
|
else if (b.kind === 'card') parts.push(`c:${b.id}:${b.state || ''}:${(b.card && b.card.type) || ''}:${b.runId || ''}:${b.error || ''}`);
|
|
3517
3642
|
}
|
|
3518
|
-
parts.push(`r:${row.status || ''}:${row.errorMessage || ''}:${isLiveRow(row) ? 1 : 0}`);
|
|
3643
|
+
parts.push(`r:${row.status || ''}:${row.errorMessage || ''}:${row.errorCode || ''}:${isLiveRow(row) ? 1 : 0}`);
|
|
3519
3644
|
return parts.join('|');
|
|
3520
3645
|
}
|
|
3521
3646
|
|
|
@@ -3565,7 +3690,15 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3565
3690
|
}
|
|
3566
3691
|
if (cur.status === 'error') {
|
|
3567
3692
|
const explained = (cur.blocks || []).some((b) => b && b.kind === 'notice');
|
|
3568
|
-
if (cur.
|
|
3693
|
+
if (cur.errorCode === 'claude-signed-out' && typeof openClaudeSetup === 'function') {
|
|
3694
|
+
// The CLI's raw "Not logged in" → one line whose link opens Connect Claude Code.
|
|
3695
|
+
const line = make('div', 'ask-error-line', "Claude Code isn't signed in. ");
|
|
3696
|
+
const link = make('a', '', 'Sign in…');
|
|
3697
|
+
link.href = '#';
|
|
3698
|
+
link.addEventListener('click', (e) => { e.preventDefault(); openClaudeSetup(); });
|
|
3699
|
+
line.appendChild(link);
|
|
3700
|
+
wrap.appendChild(line);
|
|
3701
|
+
} else if (cur.errorMessage) wrap.appendChild(make('div', 'ask-error-line', cur.errorMessage));
|
|
3569
3702
|
else if (!explained) wrap.appendChild(make('div', 'ask-error-line', 'This turn ended with an error.'));
|
|
3570
3703
|
}
|
|
3571
3704
|
if (isLiveRow(cur)) {
|
|
@@ -3643,12 +3776,16 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3643
3776
|
// A SWITCH starts a fresh ledger, so the new chat rises in. A resync or a
|
|
3644
3777
|
// reconnect re-loads the SAME thread and must keep it: those repaint rows the
|
|
3645
3778
|
// user is already reading, mid-turn.
|
|
3646
|
-
|
|
3779
|
+
const switched = st.threadId !== id;
|
|
3780
|
+
if (switched) st.seenRows = new Set();
|
|
3647
3781
|
st.threadId = id;
|
|
3648
3782
|
st.model = createThreadModel({ threadId: id });
|
|
3649
3783
|
st.model.load(snap);
|
|
3650
3784
|
el.title.textContent = (snap.thread && snap.thread.title) || 'Ask Worca';
|
|
3651
3785
|
applyThreadScope(snap.thread && snap.thread.context); // #397: restore the pin
|
|
3786
|
+
// The picker follows the chat — on a SWITCH only: a resync of the same thread
|
|
3787
|
+
// would otherwise clobber a pick the user just made (its PATCH may not have landed).
|
|
3788
|
+
if (switched) applyThreadPick(snap.thread);
|
|
3652
3789
|
renderTranscript();
|
|
3653
3790
|
updateMeters();
|
|
3654
3791
|
// P4: the count rides the snapshot loadThread ALREADY fetched — no extra GET.
|