@worca/app 1.4.0 → 1.5.0-rc.3
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 +36 -1
- package/docker/.env.example +54 -0
- package/docker/compose.clonein.yml +24 -0
- package/docker/compose.dev.yml +24 -0
- package/docker/compose.egress.yml +47 -0
- package/docker/compose.ssh.yml +17 -0
- package/docker/compose.teams.yml +25 -0
- package/docker/compose.yml +57 -0
- package/package.json +4 -1
- package/src/cli/container.mjs +227 -0
- package/src/cli/models.mjs +5 -4
- package/src/cli/schedule.mjs +23 -10
- package/src/cli/worca-cc.mjs +62 -4
- package/src/core/agent-user.mjs +92 -0
- package/src/core/artifacts.mjs +41 -9
- package/src/core/ask/clone-deps.mjs +25 -0
- package/src/core/ask/clone-proposal.mjs +79 -0
- package/src/core/ask/events.mjs +10 -0
- package/src/core/ask/mcp-stdio.mjs +4 -2
- package/src/core/ask/model-proposal.mjs +14 -3
- package/src/core/ask/prompt.mjs +27 -6
- package/src/core/ask/schedule-deps.mjs +16 -11
- package/src/core/ask/spawn.mjs +8 -3
- package/src/core/ask/store.mjs +40 -10
- package/src/core/ask/tool-deps.mjs +36 -1
- package/src/core/ask/tools.mjs +69 -8
- package/src/core/ask/turn.mjs +31 -1
- package/src/core/bridge/errors.mjs +50 -2
- package/src/core/bridge/provider-ops.mjs +20 -7
- package/src/core/bridge/providers/copilot.mjs +51 -5
- package/src/core/bridge/translate/common.mjs +144 -0
- package/src/core/bridge/translate/request.mjs +16 -73
- package/src/core/bridge/translate/responses-request.mjs +185 -0
- package/src/core/bridge/translate/responses-stream.mjs +327 -0
- package/src/core/bridge/upstream.mjs +37 -10
- package/src/core/cf-access.mjs +90 -0
- package/src/core/chat/command-router.mjs +17 -12
- package/src/core/claude-runner.mjs +26 -3
- package/src/core/clone-project.mjs +139 -0
- package/src/core/config.mjs +16 -9
- package/src/core/db.mjs +60 -5
- package/src/core/deployment.mjs +33 -0
- package/src/core/diff-comments.mjs +17 -8
- package/src/core/git-info.mjs +27 -7
- package/src/core/github-app.mjs +107 -0
- package/src/core/github-credentials.mjs +93 -0
- package/src/core/graph/executor.mjs +22 -4
- package/src/core/graph/script-runner.mjs +24 -3
- package/src/core/guardrails.mjs +1 -0
- package/src/core/identity.mjs +83 -0
- package/src/core/metrics/pr-events-workflow.yml +127 -0
- package/src/core/metrics/prs.mjs +383 -0
- package/src/core/metrics/read.mjs +39 -29
- package/src/core/metrics/record.mjs +30 -2
- package/src/core/metrics/sync.mjs +35 -4
- package/src/core/model-env.mjs +47 -7
- package/src/core/model-test.mjs +29 -5
- package/src/core/notifications.mjs +36 -14
- package/src/core/orchestrator.mjs +10 -4
- package/src/core/phases.mjs +2 -0
- package/src/core/pipeline-delete.mjs +4 -3
- package/src/core/plugin-repo.mjs +7 -3
- package/src/core/policy/gate.mjs +9 -5
- package/src/core/policy/state.mjs +2 -2
- package/src/core/policy/sync.mjs +5 -4
- package/src/core/remote-access.mjs +138 -0
- package/src/core/run-harness.mjs +71 -9
- package/src/core/scheduler.mjs +63 -33
- package/src/shared/team-metrics/timeline.mjs +330 -0
- package/ui/public/app.js +591 -14
- package/ui/public/ask-panel.mjs +65 -3
- package/ui/public/bridge-view.mjs +72 -14
- package/ui/public/index.html +42 -9
- package/ui/public/schedules-view.mjs +21 -2
- package/ui/public/session-guard.mjs +77 -0
- package/ui/public/style.css +171 -1
- package/ui/public/team-metrics-timeline.mjs +480 -0
- package/ui/public/team-metrics-view.mjs +10 -2
- package/ui/public/team-policy-view.mjs +6 -1
- package/ui/server.mjs +454 -104
package/ui/public/ask-panel.mjs
CHANGED
|
@@ -2404,6 +2404,61 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2404
2404
|
return { el: rootEl };
|
|
2405
2405
|
}
|
|
2406
2406
|
|
|
2407
|
+
/** The clone card (propose_clone_project): proposed → cloning → applied | failed, or declined. Every value is text. */
|
|
2408
|
+
function buildCloneCard(block) {
|
|
2409
|
+
const card = block.card || {};
|
|
2410
|
+
const summary = card.summary || 'clone a repository';
|
|
2411
|
+
if (block.state === 'declined') return { el: make('div', 'ask-card-stub', `Declined — ${summary}`) };
|
|
2412
|
+
const rootEl = make('div', `ask-card ask-mcard ask-clonecard is-${block.state}`);
|
|
2413
|
+
rootEl.setAttribute('data-ask-clonecard', block.state);
|
|
2414
|
+
const head = make('div', 'ask-mcard-head');
|
|
2415
|
+
const title = block.state === 'applied' ? 'Project cloned' : block.state === 'failed' ? 'Clone failed'
|
|
2416
|
+
: block.state === 'cloning' ? 'Cloning…' : 'Proposed project';
|
|
2417
|
+
head.appendChild(make('span', 'ask-mcard-title', title));
|
|
2418
|
+
head.appendChild(make('span', 'ask-mcard-kind', 'Clone'));
|
|
2419
|
+
rootEl.appendChild(head);
|
|
2420
|
+
const body = make('div', 'ask-mcard-body');
|
|
2421
|
+
const sum = make('div', 'ask-mcard-summary');
|
|
2422
|
+
if (block.state === 'applied') sum.appendChild(svgIcon(WF_ICO.check, 15, 2.4));
|
|
2423
|
+
sum.appendChild(make('span', null, summary));
|
|
2424
|
+
body.appendChild(sum);
|
|
2425
|
+
if (card.note) body.appendChild(make('div', 'ask-mcard-note', card.note));
|
|
2426
|
+
const rows = [['Repository', card.url], ['Branch', card.branch || 'default branch'], ['Folder', card.dir], ['GitHub', card.github]];
|
|
2427
|
+
const ul = make('ul', 'ask-mcard-changes');
|
|
2428
|
+
for (const [label, value] of rows) {
|
|
2429
|
+
if (!value) continue;
|
|
2430
|
+
const li = make('li');
|
|
2431
|
+
li.appendChild(make('span', 'ask-mcard-change-label', label));
|
|
2432
|
+
const val = make('span', 'ask-mcard-change-val');
|
|
2433
|
+
val.appendChild(make('span', 'ask-mcard-after', String(value)));
|
|
2434
|
+
li.appendChild(val);
|
|
2435
|
+
ul.appendChild(li);
|
|
2436
|
+
}
|
|
2437
|
+
body.appendChild(ul);
|
|
2438
|
+
const result = card.result || null;
|
|
2439
|
+
if (block.state === 'failed') body.appendChild(make('div', 'ask-mcard-failed', `Could not clone: ${block.error || (result && result.error) || 'unknown error'}`));
|
|
2440
|
+
else if (block.state === 'applied' && result && result.project && result.project.path) {
|
|
2441
|
+
body.appendChild(make('div', 'ask-mcard-detail', `Registered as ${result.project.name || card.name} at ${result.project.path}`));
|
|
2442
|
+
}
|
|
2443
|
+
rootEl.appendChild(body);
|
|
2444
|
+
rootEl.appendChild(make('div', 'ask-card-err'));
|
|
2445
|
+
if (block.state === 'proposed') {
|
|
2446
|
+
const actions = make('div', 'ask-mcard-actions');
|
|
2447
|
+
const btn = (cls, text, attr, icon) => {
|
|
2448
|
+
const b = make('button', cls, text); b.type = 'button'; b.setAttribute(attr, '');
|
|
2449
|
+
if (icon) b.prepend(svgIcon(icon, 12, 2.2));
|
|
2450
|
+
return b;
|
|
2451
|
+
};
|
|
2452
|
+
const decline = btn('ask-card-not-now', 'Decline', 'data-ask-clone-decline');
|
|
2453
|
+
decline.addEventListener('click', () => postCard(block, rootEl, { state: 'declined' }, decline));
|
|
2454
|
+
const apply = btn('ask-card-start', 'Clone', 'data-ask-clone-apply', WF_ICO.save);
|
|
2455
|
+
apply.addEventListener('click', () => postCard(block, rootEl, { state: 'applied' }, apply));
|
|
2456
|
+
actions.append(make('span', 'ask-card-actions-spacer'), decline, apply);
|
|
2457
|
+
rootEl.appendChild(actions);
|
|
2458
|
+
}
|
|
2459
|
+
return { el: rootEl };
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2407
2462
|
/** The model · effort picker (mockup §C): the panel's popover chrome, anchored under the chip. Rows are menuitems (PD28). */
|
|
2408
2463
|
function openChipPicker(chip, nodeId, card, wf, handle) {
|
|
2409
2464
|
const node = card.nodes && card.nodes[nodeId];
|
|
@@ -3067,7 +3122,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3067
3122
|
function isProgressBlock(block) {
|
|
3068
3123
|
const card = block.card || {};
|
|
3069
3124
|
if (card.type === PROGRESS_CARD_TYPE) return true;
|
|
3070
|
-
if (card.type === 'workflow' || card.type === 'metrics' || card.type === 'policy' || card.type === 'schedule' || card.type === 'model') return false;
|
|
3125
|
+
if (card.type === 'workflow' || card.type === 'metrics' || card.type === 'policy' || card.type === 'schedule' || card.type === 'model' || card.type === 'clone') return false;
|
|
3071
3126
|
return block.state === 'started' || (block.state === 'failed' && !!block.runId);
|
|
3072
3127
|
}
|
|
3073
3128
|
function buildCard(block) {
|
|
@@ -3078,13 +3133,15 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3078
3133
|
const isMetrics = !!(block.card && (block.card.type === 'metrics' || block.card.type === 'policy'));
|
|
3079
3134
|
const isSchedule = !!(block.card && block.card.type === 'schedule');
|
|
3080
3135
|
const isModel = !!(block.card && block.card.type === 'model');
|
|
3136
|
+
const isClone = !!(block.card && block.card.type === 'clone');
|
|
3081
3137
|
const isProgress = isProgressBlock(block);
|
|
3082
|
-
if (cached && cached.state === block.state && (isWorkflow || isMetrics || isSchedule || isModel || isProgress || block.state === 'proposed')) return cached.el;
|
|
3138
|
+
if (cached && cached.state === block.state && (isWorkflow || isMetrics || isSchedule || isModel || isClone || isProgress || block.state === 'proposed')) return cached.el;
|
|
3083
3139
|
if (cached) disposeCardEntry(cached);
|
|
3084
3140
|
const built = isWorkflow ? buildWorkflowCard(block, cached)
|
|
3085
3141
|
: isMetrics ? buildMetricsCard(block)
|
|
3086
3142
|
: isSchedule ? buildScheduleCard(block)
|
|
3087
3143
|
: isModel ? buildModelCard(block)
|
|
3144
|
+
: isClone ? buildCloneCard(block)
|
|
3088
3145
|
: isProgress ? buildProgressCard(block)
|
|
3089
3146
|
: { el: block.state === 'proposed' ? buildCardForm(block) : buildCardTerminal(block) };
|
|
3090
3147
|
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 });
|
|
@@ -3704,7 +3761,12 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
3704
3761
|
|
|
3705
3762
|
function pushServerFrame(frame) {
|
|
3706
3763
|
if (st.destroyed || !frame) return;
|
|
3707
|
-
if (frame.type === 'ask-history-cleared') {
|
|
3764
|
+
if (frame.type === 'ask-history-cleared') {
|
|
3765
|
+
// A shared deployment's clear names the threads it removed: only a tab showing one of them resets.
|
|
3766
|
+
if (Array.isArray(frame.threadIds) && !frame.threadIds.includes(st.threadId)) { scheduleThreadsRefresh(); return; }
|
|
3767
|
+
onHistoryCleared();
|
|
3768
|
+
return;
|
|
3769
|
+
}
|
|
3708
3770
|
if (THREADS_REFRESH_FRAMES.has(frame.type)) scheduleThreadsRefresh();
|
|
3709
3771
|
// Defence-in-depth: the model's own threadId filter is the real router — this early return only saves an apply() call and cannot be observed from tests (the model would drop the frame identically).
|
|
3710
3772
|
if (!st.model || frame.threadId !== st.threadId) return;
|
|
@@ -15,9 +15,43 @@ function h(doc, tag, cls, text) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/** Mirrors model-env.mjs PROVIDER_APIS — the server validates; this is the form. */
|
|
18
|
-
export const PROVIDER_APIS = { copilot: ['anthropic', 'openai-chat'], openai: ['openai-chat'], anthropic: ['anthropic'] };
|
|
18
|
+
export const PROVIDER_APIS = { copilot: ['anthropic', 'openai-chat', 'openai-responses'], openai: ['openai-chat', 'openai-responses'], anthropic: ['anthropic'] };
|
|
19
|
+
/** Mirrors model-env.mjs TRANSLATED_APIS: the apis the bridge translates (no passthrough). */
|
|
20
|
+
export const TRANSLATED_APIS = ['openai-chat', 'openai-responses'];
|
|
19
21
|
export const PROVIDER_LABELS = { copilot: 'GitHub Copilot', openai: 'OpenAI-compatible', anthropic: 'Anthropic-compatible' };
|
|
20
|
-
export const API_LABELS = { anthropic: 'Anthropic Messages', 'openai-chat': 'OpenAI chat completions' };
|
|
22
|
+
export const API_LABELS = { anthropic: 'Anthropic Messages', 'openai-chat': 'OpenAI chat completions', 'openai-responses': 'OpenAI Responses' };
|
|
23
|
+
/** The import sheet's short API column. */
|
|
24
|
+
const API_SHORT = { anthropic: 'Messages', 'openai-chat': 'chat', 'openai-responses': 'Responses' };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The effort levels an imported model listed, stashed on the Connection block
|
|
28
|
+
* (spec §6.3) — only while the provider and the upstream model id are still the
|
|
29
|
+
* ones they came from — or null.
|
|
30
|
+
*/
|
|
31
|
+
function storedReasoningEfforts(conn, provider) {
|
|
32
|
+
const raw = conn && conn.dataset ? conn.dataset.reasoningEfforts : '';
|
|
33
|
+
if (!raw || conn.dataset.reasoningEffortsProvider !== provider) return null;
|
|
34
|
+
const modelIn = conn.querySelector('.mv-conn-model');
|
|
35
|
+
if ((modelIn ? modelIn.value.trim() : '') !== (conn.dataset.reasoningEffortsModel || '')) return null;
|
|
36
|
+
try {
|
|
37
|
+
const list = JSON.parse(raw);
|
|
38
|
+
return Array.isArray(list) && list.length ? list : null;
|
|
39
|
+
} catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Stash (or clear) an upstream's effort levels on the Connection block: no form field edits them. */
|
|
43
|
+
function stashReasoningEfforts(conn, upstream) {
|
|
44
|
+
const list = upstream && upstream.capabilities && upstream.capabilities.reasoningEfforts;
|
|
45
|
+
if (Array.isArray(list) && list.length) {
|
|
46
|
+
conn.dataset.reasoningEfforts = JSON.stringify(list);
|
|
47
|
+
conn.dataset.reasoningEffortsProvider = upstream.provider;
|
|
48
|
+
conn.dataset.reasoningEffortsModel = String(upstream.model || '').trim();
|
|
49
|
+
} else {
|
|
50
|
+
delete conn.dataset.reasoningEfforts;
|
|
51
|
+
delete conn.dataset.reasoningEffortsProvider;
|
|
52
|
+
delete conn.dataset.reasoningEffortsModel;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
21
55
|
export const CAPABILITY_FLAGS = [['toolCalls', 'Tool calls'], ['vision', 'Vision'], ['reasoning', 'Reasoning']];
|
|
22
56
|
|
|
23
57
|
/** The notice every Copilot sign-in must acknowledge (§8.2). One source of truth: the UI modal and the CLI print it. */
|
|
@@ -33,12 +67,15 @@ export const COPILOT_TERMS = Object.freeze({
|
|
|
33
67
|
confirm: 'Continue',
|
|
34
68
|
});
|
|
35
69
|
|
|
36
|
-
/** One-line degradation copy for a translated (openai-chat) model, '' otherwise (§8.5). */
|
|
70
|
+
/** One-line degradation copy for a translated (openai-chat / openai-responses) model, '' otherwise (§8.5). */
|
|
37
71
|
export function degradationLine(m) {
|
|
38
|
-
if (!m || !m.upstream || m.upstream.api
|
|
72
|
+
if (!m || !m.upstream || !TRANSLATED_APIS.includes(m.upstream.api)) return '';
|
|
39
73
|
const caps = m.upstream.capabilities || {};
|
|
40
74
|
const limit = caps.maxPromptTokens ? `, prompt limit ~${Math.round(caps.maxPromptTokens / 1000)}k tokens` : '';
|
|
41
|
-
|
|
75
|
+
const lead = m.upstream.api === 'openai-responses'
|
|
76
|
+
? 'translated — reasoning summaries only, no WebSearch/WebFetch'
|
|
77
|
+
: 'translated — no thinking blocks, no WebSearch/WebFetch';
|
|
78
|
+
return `${lead}${limit}`;
|
|
42
79
|
}
|
|
43
80
|
|
|
44
81
|
/** The `bridged: <provider>` badge for a catalog card, or null. */
|
|
@@ -195,7 +232,7 @@ export function renderProvidersCard(providers, { doc = globalThis.document, sign
|
|
|
195
232
|
: k.keyOptional ? h(doc, 'span', 'badge green', 'local — no key needed') : h(doc, 'span', 'badge grey', 'no key'));
|
|
196
233
|
main.appendChild(rh);
|
|
197
234
|
main.appendChild(h(doc, 'small', 'hint', name === 'openai'
|
|
198
|
-
? 'OpenAI, Azure, Ollama, vLLM, Groq, an in-house gateway — anything with a /chat/completions endpoint. Models run through the translation layer (no
|
|
235
|
+
? 'OpenAI, Azure, Ollama, vLLM, Groq, an in-house gateway — anything with a /chat/completions or /responses endpoint. Models run through the translation layer (no web tools).'
|
|
199
236
|
: 'A gateway that already speaks the Anthropic Messages API but needs a key Worca holds for it. Calls pass through untouched.'));
|
|
200
237
|
if (k.keySource === 'env') main.appendChild(h(doc, 'small', 'hint', `Key read from your shell env (${k.keyRef}).`));
|
|
201
238
|
main.appendChild(h(doc, 'small', 'hint mv-pv-msg'));
|
|
@@ -294,7 +331,7 @@ export function renderImportSheet(models, { doc = globalThis.document } = {}) {
|
|
|
294
331
|
const root = h(doc, 'section', 'card mv-editor mvi');
|
|
295
332
|
root.appendChild(h(doc, 'h3', 'mv-editor-title', 'Import from GitHub Copilot'));
|
|
296
333
|
root.appendChild(h(doc, 'small', 'hint',
|
|
297
|
-
'These run through your Copilot subscription. Claude models keep extended thinking; other vendors run through a translation layer (no
|
|
334
|
+
'These run through your Copilot subscription. Claude models keep extended thinking; other vendors run through a translation layer (no web tools; reasoning arrives as summaries on the Responses API). Imported entries are priced free — Copilot bills premium requests, not tokens — and Worca counts requests per run.'));
|
|
298
335
|
const list = Array.isArray(models) ? models : [];
|
|
299
336
|
if (!list.length) {
|
|
300
337
|
root.appendChild(h(doc, 'div', 'hist-empty', 'Copilot returned no chat models for this account.'));
|
|
@@ -306,7 +343,7 @@ export function renderImportSheet(models, { doc = globalThis.document } = {}) {
|
|
|
306
343
|
const all = h(doc, 'input', 'mvi-all'); all.type = 'checkbox'; all.title = 'Select all importable';
|
|
307
344
|
allTh.appendChild(all);
|
|
308
345
|
hr.appendChild(allTh);
|
|
309
|
-
for (const t of ['Model', 'Vendor', 'Context', 'Tools', 'Vision', 'Reasoning', 'Status']) hr.appendChild(h(doc, 'th', null, t));
|
|
346
|
+
for (const t of ['Model', 'Vendor', 'Context', 'Tools', 'Vision', 'Reasoning', 'API', 'Status']) hr.appendChild(h(doc, 'th', null, t));
|
|
310
347
|
thead.appendChild(hr);
|
|
311
348
|
tbl.appendChild(thead);
|
|
312
349
|
const tbody = h(doc, 'tbody');
|
|
@@ -328,6 +365,7 @@ export function renderImportSheet(models, { doc = globalThis.document } = {}) {
|
|
|
328
365
|
tr.appendChild(h(doc, 'td', null, yesNo(m.toolCalls)));
|
|
329
366
|
tr.appendChild(h(doc, 'td', null, yesNo(m.vision)));
|
|
330
367
|
tr.appendChild(h(doc, 'td', null, yesNo(m.reasoning)));
|
|
368
|
+
tr.appendChild(h(doc, 'td', 'mvi-api', API_SHORT[m.api] || '—'));
|
|
331
369
|
const status = [];
|
|
332
370
|
if (m.inCatalog) status.push('in catalog ✓');
|
|
333
371
|
if (m.preview) status.push('preview');
|
|
@@ -450,6 +488,7 @@ export function renderConnectionSection(model, { doc = globalThis.document, prov
|
|
|
450
488
|
const groupName = `mv-conn-mode-${editing ? model.id : 'new'}`;
|
|
451
489
|
const wrap = h(doc, 'div', 'mv-conn');
|
|
452
490
|
wrap.dataset.provider = upstream ? upstream.provider : '';
|
|
491
|
+
stashReasoningEfforts(wrap, upstream);
|
|
453
492
|
|
|
454
493
|
const modes = h(doc, 'div', 'mv-conn-modes');
|
|
455
494
|
const startMode = upstream ? 'provider' : (editing && model.env && Object.keys(model.env).length ? 'env' : 'direct');
|
|
@@ -611,28 +650,43 @@ export function applyConnectionMode(connEl) {
|
|
|
611
650
|
}
|
|
612
651
|
const note = conn.querySelector('.mv-conn-note');
|
|
613
652
|
if (note) {
|
|
653
|
+
const copilotPricing = provider === 'copilot' ? ' Copilot bills premium requests, so Pricing defaults to Free.' : '';
|
|
614
654
|
note.textContent = mode !== 'provider' ? '' : api === 'openai-chat'
|
|
615
|
-
? `Translated: no thinking blocks, WebSearch/WebFetch withheld, prompt limit per the capabilities above.${
|
|
616
|
-
:
|
|
617
|
-
|
|
655
|
+
? `Translated: no thinking blocks, WebSearch/WebFetch withheld, prompt limit per the capabilities above.${copilotPricing}`
|
|
656
|
+
: api === 'openai-responses'
|
|
657
|
+
? `Translated to the Responses API: reasoning summaries arrive as thinking, WebSearch/WebFetch withheld, prompt limit per the capabilities above.${copilotPricing}`
|
|
658
|
+
: provider === 'copilot' ? 'Copilot’s native Anthropic endpoint: thinking blocks and cache accounting arrive intact. Copilot bills premium requests, so Pricing defaults to Free.'
|
|
659
|
+
: 'Passthrough: the request reaches the endpoint untouched; only auth and headers are added.';
|
|
618
660
|
}
|
|
619
661
|
const reasoning = !!conn.querySelector('.mv-conn-cap-cb[data-cap="reasoning"]')?.checked;
|
|
620
662
|
// Efforts live in the editor grid beside this block.
|
|
621
663
|
const editor = conn.closest ? conn.closest('.mv-editor') : null;
|
|
622
664
|
const effCbs = editor ? [...editor.querySelectorAll('.mv-effort-cb')] : [];
|
|
623
665
|
const effHint = editor ? editor.querySelector('.mv-efforts-hint') : null;
|
|
624
|
-
const
|
|
666
|
+
const translated = mode === 'provider' && TRANSLATED_APIS.includes(api);
|
|
667
|
+
const translatedNoReasoning = translated && !reasoning;
|
|
668
|
+
// The model's own effort levels (carried from Import): offer only the Worca efforts it lists.
|
|
669
|
+
const levels = translated && reasoning ? storedReasoningEfforts(conn, provider) : null;
|
|
670
|
+
const offered = levels ? effCbs.map((cb) => cb.value).filter((v) => levels.includes(v)) : null;
|
|
671
|
+
const allowed = offered && !offered.length ? ['medium'] : offered;
|
|
625
672
|
for (const cb of effCbs) {
|
|
626
673
|
if (translatedNoReasoning) {
|
|
627
674
|
cb.disabled = cb.value !== 'medium';
|
|
628
675
|
if (cb.value !== 'medium') cb.checked = false; else cb.checked = true;
|
|
676
|
+
} else if (allowed) {
|
|
677
|
+
cb.disabled = !allowed.includes(cb.value);
|
|
678
|
+
if (cb.disabled) cb.checked = false;
|
|
629
679
|
} else cb.disabled = false;
|
|
630
680
|
}
|
|
681
|
+
// Every stored effort fell outside the model's levels: offer its levels rather than
|
|
682
|
+
// save an empty list (the server reads empty as every effort, the disabled ones too).
|
|
683
|
+
if (allowed && !effCbs.some((cb) => cb.checked)) for (const cb of effCbs) if (allowed.includes(cb.value)) cb.checked = true;
|
|
631
684
|
if (effHint) {
|
|
632
685
|
effHint.textContent = translatedNoReasoning
|
|
633
686
|
? 'This model has no reasoning control; effort would be ignored — only medium is offered.'
|
|
634
|
-
:
|
|
635
|
-
: '
|
|
687
|
+
: levels ? `Maps to this model's effort levels: ${levels.join(', ')}.`
|
|
688
|
+
: translated ? 'Maps to reasoning_effort low / medium / high — xhigh and max are not distinct.'
|
|
689
|
+
: 'All checked = every effort (the default).';
|
|
636
690
|
}
|
|
637
691
|
}
|
|
638
692
|
|
|
@@ -640,6 +694,7 @@ export function applyConnectionMode(connEl) {
|
|
|
640
694
|
export function setModelUpstream(connEl, upstream) {
|
|
641
695
|
const conn = connEl.classList && connEl.classList.contains('mv-conn') ? connEl : connEl.querySelector('.mv-conn');
|
|
642
696
|
if (!conn) return;
|
|
697
|
+
stashReasoningEfforts(conn, upstream);
|
|
643
698
|
const mode = upstream ? 'provider' : 'direct';
|
|
644
699
|
for (const rb of conn.querySelectorAll('.mv-conn-mode-rb')) rb.checked = rb.value === mode;
|
|
645
700
|
if (upstream) {
|
|
@@ -689,6 +744,9 @@ export function collectConnection(connEl) {
|
|
|
689
744
|
const v = (inp.value || '').trim();
|
|
690
745
|
if (v !== '') capabilities[inp.dataset.limit] = Number(v);
|
|
691
746
|
}
|
|
747
|
+
// The model's effort levels ride along unedited while it stays a translated reasoning model on the same provider.
|
|
748
|
+
const levels = storedReasoningEfforts(conn, provider);
|
|
749
|
+
if (levels && capabilities.reasoning && TRANSLATED_APIS.includes(api)) capabilities.reasoningEfforts = levels;
|
|
692
750
|
upstream.capabilities = capabilities;
|
|
693
751
|
return { upstream };
|
|
694
752
|
}
|
package/ui/public/index.html
CHANGED
|
@@ -110,6 +110,9 @@
|
|
|
110
110
|
</nav>
|
|
111
111
|
|
|
112
112
|
<div class="side-foot">
|
|
113
|
+
<!-- Who this browser is signed in as (GET /api/whoami). Filled only behind an
|
|
114
|
+
identity proxy or an operator-set name; a local install never shows it. -->
|
|
115
|
+
<div class="side-who" id="side-who" hidden><span class="side-who-label">Signed in as</span> <span class="side-who-name"></span></div>
|
|
113
116
|
<div id="side-spend"></div>
|
|
114
117
|
<button type="button" class="side-toggle" id="side-toggle"
|
|
115
118
|
aria-expanded="true" aria-controls="side-rail"
|
|
@@ -509,6 +512,7 @@
|
|
|
509
512
|
<div class="rc-meta">
|
|
510
513
|
<span class="rc-status-word"></span>
|
|
511
514
|
<span class="rc-seg"><span class="rc-dot">·</span><span class="rm-text"></span></span>
|
|
515
|
+
<span class="rc-seg rc-by" hidden><span class="rc-dot">·</span><span class="person-ini" aria-hidden="true"></span><span class="rc-by-text"></span></span>
|
|
512
516
|
<span class="rc-seg rc-prog" hidden data-min-level="advanced"><span class="rc-dot">·</span><span class="rc-prog-text"></span></span>
|
|
513
517
|
<span class="rc-seg"><span class="rc-dot">·</span><span class="run-time mono"></span></span>
|
|
514
518
|
<span class="rc-seg"><span class="rc-dot">·</span><span class="run-cost mono"></span></span>
|
|
@@ -665,6 +669,7 @@
|
|
|
665
669
|
<span class="hm-seg hist-clock-seg"><span class="hm-dot">·</span><span class="hist-clock"></span></span>
|
|
666
670
|
<span class="hm-seg hist-time-seg"><span class="hm-dot">·</span><span class="hist-time mono"></span></span>
|
|
667
671
|
<span class="hm-seg hist-total-seg"><span class="hm-dot">·</span><span class="hist-total mono"></span></span>
|
|
672
|
+
<span class="hm-seg hist-by-seg"><span class="hm-dot">·</span><span class="hist-by"></span></span>
|
|
668
673
|
<span class="hist-diff-pill" hidden data-min-level="advanced">
|
|
669
674
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"><path d="M6 3h8l4 4v14H6z" stroke-linejoin="round"></path><path d="M14 3v4h4" stroke-linejoin="round"></path></svg>
|
|
670
675
|
<span class="hist-diff mono" aria-label="lines changed"></span>
|
|
@@ -857,6 +862,10 @@
|
|
|
857
862
|
</div>
|
|
858
863
|
<div class="tm-filter">
|
|
859
864
|
<div class="select-wrap tm-scope-wrap"><select id="tm-scope" class="select" aria-label="Scope"></select></div>
|
|
865
|
+
<div class="seg tm-tabs" id="tm-tabs" role="tablist" aria-label="Team metrics view">
|
|
866
|
+
<button type="button" role="tab" data-tm-tab="overview" aria-selected="true" class="on">Overview</button>
|
|
867
|
+
<button type="button" role="tab" data-tm-tab="timeline" aria-selected="false">Timeline</button>
|
|
868
|
+
</div>
|
|
860
869
|
<div class="seg" id="tm-range" role="group" aria-label="Time range">
|
|
861
870
|
<button type="button" data-range="this-month">This month</button>
|
|
862
871
|
<button type="button" data-range="last-month">Last month</button>
|
|
@@ -871,6 +880,7 @@
|
|
|
871
880
|
</div>
|
|
872
881
|
<div class="tm-active-filters" id="tm-filters" hidden></div>
|
|
873
882
|
<div id="tm-body"><small class="hint">Loading…</small></div>
|
|
883
|
+
<div id="tm-timeline" hidden></div>
|
|
874
884
|
<div id="tm-tip" class="chart-tip" hidden></div>
|
|
875
885
|
</section>
|
|
876
886
|
<!-- /view team-metrics -->
|
|
@@ -2269,18 +2279,41 @@
|
|
|
2269
2279
|
<h2 id="proj-add-title">Add project</h2>
|
|
2270
2280
|
<button type="button" id="proj-add-cancel" class="btn btn-mini">Close</button>
|
|
2271
2281
|
</div>
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2282
|
+
<!-- Two ways in: a folder already on this machine, or a repository cloned into the
|
|
2283
|
+
projects folder by the server (POST /api/projects/clone). Clone is the default
|
|
2284
|
+
where no folder picker exists (a container or hosted worca). -->
|
|
2285
|
+
<div class="md-tabs proj-add-tabs" id="proj-add-tabs" role="tablist" aria-label="How to add the project">
|
|
2286
|
+
<button type="button" class="md-tab" role="tab" id="proj-add-tab-folder" data-mode="folder" aria-selected="true" aria-controls="proj-add-folder-pane">Folder</button>
|
|
2287
|
+
<button type="button" class="md-tab" role="tab" id="proj-add-tab-clone" data-mode="clone" aria-selected="false" aria-controls="proj-add-clone-pane">Clone from URL</button>
|
|
2275
2288
|
</div>
|
|
2276
|
-
<div
|
|
2277
|
-
<
|
|
2278
|
-
|
|
2279
|
-
<input id="proj-add-
|
|
2280
|
-
|
|
2289
|
+
<div id="proj-add-folder-pane" role="tabpanel" aria-labelledby="proj-add-tab-folder">
|
|
2290
|
+
<div class="field">
|
|
2291
|
+
<label for="proj-add-name">Name</label>
|
|
2292
|
+
<input id="proj-add-name" class="input" type="text" spellcheck="false" placeholder="Project name" />
|
|
2293
|
+
</div>
|
|
2294
|
+
<div class="field">
|
|
2295
|
+
<label for="proj-add-path">Folder</label>
|
|
2296
|
+
<div class="path-row">
|
|
2297
|
+
<input id="proj-add-path" class="input" type="text" spellcheck="false" placeholder="/absolute/path/to/project" />
|
|
2298
|
+
<button type="button" id="proj-add-browse" class="btn btn-ghost btn-mini" title="Pick the project folder">Choose folder…</button>
|
|
2299
|
+
</div>
|
|
2300
|
+
</div>
|
|
2301
|
+
</div>
|
|
2302
|
+
<div id="proj-add-clone-pane" role="tabpanel" aria-labelledby="proj-add-tab-clone" hidden>
|
|
2303
|
+
<div class="field">
|
|
2304
|
+
<label for="proj-clone-url">Repository URL</label>
|
|
2305
|
+
<input id="proj-clone-url" class="input" type="url" spellcheck="false" autocomplete="off" placeholder="https://github.com/owner/repo" />
|
|
2306
|
+
</div>
|
|
2307
|
+
<div class="field">
|
|
2308
|
+
<label for="proj-clone-branch">Branch <span class="opt">(optional)</span></label>
|
|
2309
|
+
<input id="proj-clone-branch" class="input" type="text" spellcheck="false" autocomplete="off" placeholder="the repository's default branch" />
|
|
2310
|
+
</div>
|
|
2311
|
+
<div class="field">
|
|
2312
|
+
<label for="proj-clone-name">Folder name <span class="opt">(optional)</span></label>
|
|
2313
|
+
<input id="proj-clone-name" class="input" type="text" spellcheck="false" autocomplete="off" placeholder="the repository name" />
|
|
2281
2314
|
</div>
|
|
2282
2315
|
</div>
|
|
2283
|
-
<small class="hint" id="proj-add-msg"></small>
|
|
2316
|
+
<small class="hint" id="proj-add-msg" aria-live="polite"></small>
|
|
2284
2317
|
<div class="confirm-actions">
|
|
2285
2318
|
<button type="button" id="proj-add-save" class="btn btn-primary btn-mini">Add project</button>
|
|
2286
2319
|
</div>
|
|
@@ -100,6 +100,19 @@ export function createSchedulesView({ tabsHost = null, feedHost, onceHost, repea
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// ── rows ───────────────────────────────────────────────────────────────────
|
|
103
|
+
// Who made / last changed it (step 2's display rule lives in app.js: deps.personLabel/personShown
|
|
104
|
+
// return '' unless the deployment has per-person sign-ins; 'you' for the viewer).
|
|
105
|
+
const label = (v) => (typeof deps.personLabel === 'function' ? deps.personLabel(v) : '');
|
|
106
|
+
const shown = (v) => (typeof deps.personShown === 'function' ? deps.personShown(v) : '');
|
|
107
|
+
/** The card's meta segment: "· (AL) by ada@…" / "by you" for the creator; null when nobody is shown. */
|
|
108
|
+
function bySeg(item) {
|
|
109
|
+
const who = label(item.createdBy);
|
|
110
|
+
if (!who) return null;
|
|
111
|
+
const full = shown(item.createdBy);
|
|
112
|
+
const ini = typeof deps.personIni === 'function' ? deps.personIni(full, `Created by ${full}`) : null;
|
|
113
|
+
return h('span', { class: 'rc-seg sched-by', title: `Created by ${full}` }, h('span', { class: 'rc-dot', text: '·' }), ini, h('span', { class: 'sched-by-text', text: `by ${who}` }));
|
|
114
|
+
}
|
|
115
|
+
|
|
103
116
|
function metaLine(item) {
|
|
104
117
|
const bits = [deps.targetLabel(item), deps.workflowLabel(item.summary.workflowId)];
|
|
105
118
|
if (item.summary.sourceBranch) bits.push(`from ${item.summary.sourceBranch}`);
|
|
@@ -114,6 +127,8 @@ export function createSchedulesView({ tabsHost = null, feedHost, onceHost, repea
|
|
|
114
127
|
const src = item.summary.source && item.summary.source.type === 'plugin' ? item.summary.source : null;
|
|
115
128
|
body.append(...[
|
|
116
129
|
row('Task', src ? `${src.plugin} · ${src.taskId} — fetched when the run starts` : (item.summary.prompt || '—')),
|
|
130
|
+
row('Created by', shown(item.createdBy)),
|
|
131
|
+
shown(item.updatedBy) && shown(item.updatedBy) !== shown(item.createdBy) ? row('Changed by', shown(item.updatedBy)) : null,
|
|
117
132
|
item.after ? row('After', `${(item.after.title || item.after.id.slice(0, 8))} · ${afterWord(item.after)}`) : null,
|
|
118
133
|
item.after ? row('If it fails', item.after.policy === 'any' ? 'Start anyway' : 'Do not start') : null,
|
|
119
134
|
item.sourceFromPrevious ? row('Source branch', 'the run before it') : null,
|
|
@@ -206,7 +221,8 @@ export function createSchedulesView({ tabsHost = null, feedHost, onceHost, repea
|
|
|
206
221
|
h('div', { class: 'rc-title', text: t.title || 'Scheduled run' }),
|
|
207
222
|
h('div', { class: 'rc-meta' },
|
|
208
223
|
h('span', { class: `rc-status-word st-${family}`, text: statusWord }),
|
|
209
|
-
h('span', { class: 'rc-seg' }, h('span', { class: 'rc-dot', text: '·' }), h('span', { class: 'sched-when', 'data-at': missed || chained ? '' : t.runAt, text: timeText }))
|
|
224
|
+
h('span', { class: 'rc-seg' }, h('span', { class: 'rc-dot', text: '·' }), h('span', { class: 'sched-when', 'data-at': missed || chained ? '' : t.runAt, text: timeText })),
|
|
225
|
+
bySeg(t)),
|
|
210
226
|
h('div', { class: 'sched-target', text: `${t.scheduleId ? 'Repeating' : 'Once'} · ${metaLine(t)}` }),
|
|
211
227
|
missed && t.failReason ? h('div', { class: 'sched-reason', text: t.failReason }) : null)),
|
|
212
228
|
footer(t, acts, details), details);
|
|
@@ -257,7 +273,8 @@ export function createSchedulesView({ tabsHost = null, feedHost, onceHost, repea
|
|
|
257
273
|
h('div', { class: 'rc-title', text: s.title || 'Repeating schedule' }),
|
|
258
274
|
h('div', { class: 'rc-meta' },
|
|
259
275
|
h('span', { class: `rc-status-word st-${family}`, text: statusWord }),
|
|
260
|
-
h('span', { class: 'rc-seg' }, h('span', { class: 'rc-dot', text: '·' }), h('span', { class: 'sched-rule', text: s.sentence }))
|
|
276
|
+
h('span', { class: 'rc-seg' }, h('span', { class: 'rc-dot', text: '·' }), h('span', { class: 'sched-rule', text: s.sentence })),
|
|
277
|
+
bySeg(s)),
|
|
261
278
|
h('div', { class: 'sched-target', text: metaLine(s) }),
|
|
262
279
|
(next || last) ? h('div', { class: 'sched-sub' }, next, last) : null),
|
|
263
280
|
ended ? null : sw),
|
|
@@ -416,6 +433,8 @@ export function createSchedulesView({ tabsHost = null, feedHost, onceHost, repea
|
|
|
416
433
|
|
|
417
434
|
return {
|
|
418
435
|
load, loadFeed, tickCountdowns, showTab,
|
|
436
|
+
/** Repaint from the loaded model (the viewer arrived after the first paint). */
|
|
437
|
+
repaint() { if (model.loaded) paintList(); },
|
|
419
438
|
get tab() { return model.tab; },
|
|
420
439
|
get defaults() { return model.defaults; },
|
|
421
440
|
get counts() { return model.counts; },
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Session expiry behind an identity proxy (docs/remote-access.md). When the
|
|
2
|
+
// Cloudflare Access session runs out, every same-origin request is redirected
|
|
3
|
+
// to the proxy's sign-in page on another origin: fetch() fails with a bare
|
|
4
|
+
// TypeError (CORS) and the WebSocket cannot reconnect, so the UI would just look
|
|
5
|
+
// broken. This module tells that case apart from "server down" and shows one
|
|
6
|
+
// banner with a reload button. Locally (no proxy) it never fires: a down
|
|
7
|
+
// server fails the probe too, and worca itself never redirects /api/health.
|
|
8
|
+
|
|
9
|
+
const PROBE_MIN_MS = 10_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Wraps `win.fetch` once. Returns `{ check }`: call it when something failed in
|
|
13
|
+
* a way that could be an expired session (e.g. the WebSocket closed).
|
|
14
|
+
*/
|
|
15
|
+
export function installSessionGuard({ win = window, doc = document, reload = () => win.location.reload() } = {}) {
|
|
16
|
+
if (win.__worcaSessionGuard) return win.__worcaSessionGuard;
|
|
17
|
+
const origFetch = win.fetch.bind(win);
|
|
18
|
+
let lastProbe = 0;
|
|
19
|
+
let probing = null;
|
|
20
|
+
let shown = false;
|
|
21
|
+
|
|
22
|
+
const sameOrigin = (input) => {
|
|
23
|
+
try {
|
|
24
|
+
const url = new URL(typeof input === 'string' ? input : input?.url ?? String(input), win.location.href);
|
|
25
|
+
return url.origin === win.location.origin;
|
|
26
|
+
} catch { return false; }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function showBanner(reason) {
|
|
30
|
+
if (shown) return;
|
|
31
|
+
shown = true;
|
|
32
|
+
const bar = doc.createElement('div');
|
|
33
|
+
bar.className = 'session-expired';
|
|
34
|
+
bar.setAttribute('role', 'alert');
|
|
35
|
+
bar.dataset.reason = reason;
|
|
36
|
+
const text = doc.createElement('span');
|
|
37
|
+
text.className = 'se-text';
|
|
38
|
+
text.textContent = reason === 'unauthorized'
|
|
39
|
+
? 'Worca could not confirm your sign-in.'
|
|
40
|
+
: 'Your sign-in has expired.';
|
|
41
|
+
const btn = doc.createElement('button');
|
|
42
|
+
btn.type = 'button';
|
|
43
|
+
btn.className = 'se-reload';
|
|
44
|
+
btn.textContent = 'Sign in again';
|
|
45
|
+
btn.addEventListener('click', () => reload());
|
|
46
|
+
bar.append(text, btn);
|
|
47
|
+
doc.body.prepend(bar);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A manual-redirect probe of an unauthenticated route: an opaque redirect
|
|
51
|
+
// can only come from a proxy in front of worca asking for a new sign-in.
|
|
52
|
+
function check() {
|
|
53
|
+
if (shown || probing) return probing || Promise.resolve();
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
if (now - lastProbe < PROBE_MIN_MS) return Promise.resolve();
|
|
56
|
+
lastProbe = now;
|
|
57
|
+
probing = origFetch('/api/health', { redirect: 'manual', cache: 'no-store', credentials: 'same-origin' })
|
|
58
|
+
.then((r) => { if (r.type === 'opaqueredirect' || r.status === 401) showBanner('expired'); })
|
|
59
|
+
.catch(() => { /* server unreachable: not a session problem */ })
|
|
60
|
+
.finally(() => { probing = null; });
|
|
61
|
+
return probing;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
win.fetch = async (input, init) => {
|
|
65
|
+
try {
|
|
66
|
+
const res = await origFetch(input, init);
|
|
67
|
+
if (res.status === 401 && sameOrigin(input)) showBanner('unauthorized');
|
|
68
|
+
return res;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (sameOrigin(input) && err?.name !== 'AbortError') check();
|
|
71
|
+
throw err;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
win.__worcaSessionGuard = { check };
|
|
76
|
+
return win.__worcaSessionGuard;
|
|
77
|
+
}
|