toolroll 0.9.63 → 0.9.67
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/CHANGELOG.md +108 -0
- package/README.md +33 -5
- package/dist/acp-agents.d.ts +43 -0
- package/dist/acp-agents.js +62 -0
- package/dist/acp-attest.d.ts +44 -0
- package/dist/acp-attest.js +269 -0
- package/dist/acp-check.d.ts +28 -0
- package/dist/acp-check.js +410 -0
- package/dist/acp-client.d.ts +163 -0
- package/dist/acp-client.js +738 -0
- package/dist/acp-inspect.d.ts +23 -0
- package/dist/acp-inspect.js +109 -0
- package/dist/agent-board.d.ts +19 -6
- package/dist/agent-board.js +30 -9
- package/dist/agent-fence.d.ts +117 -2
- package/dist/agent-fence.js +439 -12
- package/dist/agent-pulse.js +9 -2
- package/dist/agent-state-words.d.ts +19 -0
- package/dist/agent-state-words.js +20 -0
- package/dist/agent-state.d.ts +65 -0
- package/dist/agent-state.js +229 -0
- package/dist/agentconfig.js +39 -13
- package/dist/agents-board.d.ts +34 -2
- package/dist/agents-board.js +110 -1
- package/dist/approval-rules-ui.js +2 -2
- package/dist/assignment-delivery.d.ts +14 -0
- package/dist/assignment-delivery.js +13 -0
- package/dist/assignment.js +13 -0
- package/dist/attest.js +7 -0
- package/dist/backup-ui.js +2 -2
- package/dist/batch-checks.js +3 -2
- package/dist/browser/THIRD_PARTY_NOTICES.txt +94 -0
- package/dist/browser/terminal.css +37 -0
- package/dist/browser/terminal.js +228 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +42 -38
- package/dist/browser-shell.js +6 -3
- package/dist/browser-workspace.d.ts +235 -1
- package/dist/browser-workspace.js +60 -0
- package/dist/builder.d.ts +10 -1
- package/dist/builder.js +87 -24
- package/dist/chat-actions.d.ts +13 -1
- package/dist/chat-actions.js +197 -15
- package/dist/chat-ask.d.ts +4 -0
- package/dist/chat-ask.js +14 -0
- package/dist/chat-channel.js +20 -0
- package/dist/chat-continuity.js +5 -4
- package/dist/chat-decide.d.ts +0 -14
- package/dist/chat-decide.js +4 -1
- package/dist/chat-delivery.d.ts +9 -0
- package/dist/chat-delivery.js +45 -6
- package/dist/chat-messages.d.ts +8 -0
- package/dist/chat-messages.js +36 -4
- package/dist/check-levels-ui.js +2 -2
- package/dist/checkout-proof.d.ts +28 -0
- package/dist/checkout-proof.js +62 -0
- package/dist/claim.d.ts +12 -0
- package/dist/claim.js +19 -0
- package/dist/cli-help.d.ts +1 -1
- package/dist/cli-help.js +4 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +21 -2
- package/dist/coding-ui.js +6 -4
- package/dist/console-instance.d.ts +124 -0
- package/dist/console-instance.js +337 -0
- package/dist/contracts/chat-actions.d.ts +16 -0
- package/dist/contracts/chat-actions.js +4 -0
- package/dist/contracts/chat-content.d.ts +19 -0
- package/dist/contracts/chat-content.js +6 -0
- package/dist/contracts/console-api.d.ts +13 -4
- package/dist/contracts/console-api.js +13 -4
- package/dist/contracts/flow-card.d.ts +4 -0
- package/dist/contracts/flow-card.js +3 -0
- package/dist/contracts/flow.d.ts +46 -0
- package/dist/contracts/flow.js +11 -2
- package/dist/contracts/lead-tools.d.ts +92 -0
- package/dist/contracts/lead-tools.js +13 -0
- package/dist/contracts/provider.d.ts +11 -1
- package/dist/contracts/provider.js +1 -1
- package/dist/contracts/route.d.ts +76 -1
- package/dist/contracts/route.js +1 -1
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/control-ui.js +2 -2
- package/dist/converse.d.ts +16 -0
- package/dist/converse.js +32 -4
- package/dist/demo-chat.js +4 -4
- package/dist/demo.js +2 -2
- package/dist/desktop-update-gate.js +6 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +4 -1
- package/dist/doctor.js +1 -1
- package/dist/egress-bridge.d.ts +17 -0
- package/dist/egress-bridge.js +111 -0
- package/dist/egress.d.ts +119 -0
- package/dist/egress.js +423 -0
- package/dist/exec.d.ts +78 -0
- package/dist/exec.js +367 -9
- package/dist/export-ui.js +2 -2
- package/dist/flow-engine.js +24 -5
- package/dist/flow-gallery-ui.js +2 -2
- package/dist/flow-gallery.js +27 -8
- package/dist/flow-share.js +16 -5
- package/dist/flow-triggers.d.ts +77 -8
- package/dist/flow-triggers.js +517 -70
- package/dist/flows-cli.js +8 -2
- package/dist/flows-ui.d.ts +0 -2
- package/dist/flows-ui.js +52 -4
- package/dist/flows.d.ts +4 -0
- package/dist/flows.js +9 -1
- package/dist/html.d.ts +9 -0
- package/dist/html.js +11 -0
- package/dist/invoke.d.ts +35 -0
- package/dist/invoke.js +347 -28
- package/dist/keys.d.ts +18 -14
- package/dist/keys.js +5 -0
- package/dist/lead-capabilities.js +3 -2
- package/dist/lead-contract.js +2 -0
- package/dist/lead-doors.d.ts +2 -0
- package/dist/lead-doors.js +7 -0
- package/dist/lead-follow.d.ts +2 -0
- package/dist/lead-follow.js +58 -2
- package/dist/lead-progress.js +1 -0
- package/dist/lead-queue-runtime.d.ts +46 -0
- package/dist/lead-queue-runtime.js +191 -0
- package/dist/lead-queue.d.ts +104 -0
- package/dist/lead-queue.js +187 -0
- package/dist/lead-sweep.d.ts +67 -0
- package/dist/lead-sweep.js +99 -0
- package/dist/lead-tools.d.ts +22 -0
- package/dist/lead-tools.js +152 -1
- package/dist/lead.d.ts +13 -5
- package/dist/lead.js +89 -29
- package/dist/limits-ui.d.ts +8 -1
- package/dist/limits-ui.js +26 -0
- package/dist/live-bus.d.ts +8 -0
- package/dist/live-bus.js +40 -4
- package/dist/live-detail.d.ts +27 -0
- package/dist/live-detail.js +57 -0
- package/dist/live-notes.d.ts +20 -0
- package/dist/live-notes.js +47 -0
- package/dist/live-records.d.ts +140 -0
- package/dist/live-records.js +935 -0
- package/dist/live-terminal.d.ts +269 -0
- package/dist/live-terminal.js +686 -0
- package/dist/live.d.ts +38 -27
- package/dist/live.js +142 -124
- package/dist/mcp-oauth.d.ts +6 -3
- package/dist/mcp-oauth.js +8 -4
- package/dist/memory-pass.js +2 -2
- package/dist/mobile-viewport.d.ts +5 -1
- package/dist/mobile-viewport.js +18 -2
- package/dist/model-catalog.js +4 -0
- package/dist/monitoring-ui.js +2 -2
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +11 -4
- package/dist/operate.d.ts +4 -0
- package/dist/operate.js +335 -19
- package/dist/passwordless.d.ts +23 -0
- package/dist/passwordless.js +26 -0
- package/dist/peek-cli.d.ts +12 -4
- package/dist/peek-cli.js +19 -11
- package/dist/phase-routing.js +10 -3
- package/dist/planner-source.d.ts +3 -0
- package/dist/planner-source.js +14 -0
- package/dist/planner.d.ts +2 -1
- package/dist/planner.js +7 -1
- package/dist/policy-ui.js +2 -2
- package/dist/policy.d.ts +1 -20
- package/dist/policy.js +29 -2
- package/dist/project-delete-ui.d.ts +18 -0
- package/dist/project-delete-ui.js +60 -9
- package/dist/project-delete.js +2 -2
- package/dist/project-memory.d.ts +3 -1
- package/dist/project-memory.js +57 -25
- package/dist/project-tools.d.ts +5 -3
- package/dist/project-tools.js +12 -9
- package/dist/provider-auth.d.ts +4 -3
- package/dist/provider-auth.js +5 -4
- package/dist/provider-connection.d.ts +2 -1
- package/dist/provider-connection.js +5 -0
- package/dist/provider.d.ts +25 -2
- package/dist/provider.js +63 -9
- package/dist/pull-request-ui.js +2 -2
- package/dist/push.js +1 -1
- package/dist/quick-start.d.ts +24 -0
- package/dist/quick-start.js +127 -0
- package/dist/release-adapters.d.ts +6 -0
- package/dist/release-adapters.js +70 -3
- package/dist/release.d.ts +10 -1
- package/dist/release.js +34 -15
- package/dist/request-budget-ui.js +2 -2
- package/dist/result-follow-ups.js +3 -2
- package/dist/result-review.d.ts +1 -1
- package/dist/result-review.js +9 -3
- package/dist/retention-ui.js +2 -2
- package/dist/retention.js +10 -1
- package/dist/run-efficiency.d.ts +73 -0
- package/dist/run-efficiency.js +228 -0
- package/dist/runner.d.ts +5 -1
- package/dist/runner.js +22 -1
- package/dist/scope.d.ts +183 -5
- package/dist/scope.js +256 -31
- package/dist/scout-net.d.ts +4 -1
- package/dist/scout-net.js +37 -12
- package/dist/scout.d.ts +20 -6
- package/dist/scout.js +27 -3
- package/dist/serve.js +248 -40
- package/dist/server/chat.d.ts +1 -1
- package/dist/server/chat.js +122 -31
- package/dist/server/chrome.d.ts +16 -2
- package/dist/server/chrome.js +63 -11
- package/dist/server/console.css +107 -42
- package/dist/server/flows.js +38 -8
- package/dist/server/guards.d.ts +1 -0
- package/dist/server/guards.js +23 -9
- package/dist/server/http.d.ts +7 -0
- package/dist/server/http.js +3 -2
- package/dist/server/live.d.ts +34 -1
- package/dist/server/live.js +39 -17
- package/dist/server/pages-assets.js +5 -0
- package/dist/server/pages.js +90 -13
- package/dist/server/people-tokens.d.ts +3 -0
- package/dist/server/people-tokens.js +55 -17
- package/dist/server/remote-hooks.js +12 -6
- package/dist/server/remote.d.ts +2 -1
- package/dist/server/remote.js +14 -5
- package/dist/server/render-chat.d.ts +12 -3
- package/dist/server/render-chat.js +47 -16
- package/dist/server/render-pages.d.ts +32 -4
- package/dist/server/render-pages.js +83 -13
- package/dist/server/render-people.d.ts +3 -2
- package/dist/server/render-people.js +3 -2
- package/dist/server/render-results.d.ts +18 -2
- package/dist/server/render-results.js +37 -28
- package/dist/server/render-settings.js +6 -5
- package/dist/server/render-tasks.d.ts +35 -10
- package/dist/server/render-tasks.js +111 -27
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +2 -1
- package/dist/server/route-table.d.ts +3 -0
- package/dist/server/route-table.js +47 -5
- package/dist/server/runtime.d.ts +33 -2
- package/dist/server/session.d.ts +25 -1
- package/dist/server/session.js +18 -4
- package/dist/server/settings.js +142 -10
- package/dist/server/sweeps.d.ts +4 -0
- package/dist/server/sweeps.js +58 -0
- package/dist/server/task-views.d.ts +2 -1
- package/dist/server/task-views.js +96 -6
- package/dist/server/tasks.js +327 -78
- package/dist/server/terminals.d.ts +31 -0
- package/dist/server/terminals.js +365 -0
- package/dist/setup-guide.js +3 -2
- package/dist/slack-chat.js +4 -1
- package/dist/spend-ui.js +3 -3
- package/dist/spend.js +2 -1
- package/dist/steer-capabilities.d.ts +32 -0
- package/dist/steer-capabilities.js +60 -0
- package/dist/steer-controls.d.ts +14 -0
- package/dist/steer-controls.js +36 -0
- package/dist/steer-status.js +2 -2
- package/dist/storage-ui.js +4 -4
- package/dist/store-baseline.d.ts +1 -1
- package/dist/store-baseline.js +1 -1
- package/dist/store.d.ts +440 -15
- package/dist/store.js +1072 -52
- package/dist/subagent-desk.js +1 -1
- package/dist/summary.js +3 -0
- package/dist/task-source.d.ts +58 -0
- package/dist/task-source.js +93 -0
- package/dist/team-leads.js +1 -1
- package/dist/teams-chat.js +4 -1
- package/dist/teardown-git.d.ts +22 -0
- package/dist/teardown-git.js +136 -2
- package/dist/telegram-lead.d.ts +3 -0
- package/dist/telegram-lead.js +44 -7
- package/dist/telegram.d.ts +11 -2
- package/dist/telegram.js +51 -17
- package/dist/terminal-catalog.d.ts +11 -0
- package/dist/terminal-catalog.js +129 -0
- package/dist/text-limits.d.ts +2 -0
- package/dist/text-limits.js +2 -0
- package/dist/tool-broker.d.ts +14 -0
- package/dist/tool-broker.js +67 -2
- package/dist/toolroll-update-ui.js +2 -2
- package/dist/toolroll-update.js +13 -3
- package/dist/tools-ui.js +2 -2
- package/dist/unfenced-test-support.d.ts +2 -0
- package/dist/unfenced-test-support.js +11 -0
- package/dist/webhook-limits.d.ts +23 -0
- package/dist/webhook-limits.js +30 -0
- package/dist/webhook-readers.d.ts +56 -0
- package/dist/webhook-readers.js +138 -0
- package/dist/webhook-signing.d.ts +27 -0
- package/dist/webhook-signing.js +119 -0
- package/dist/work-index.d.ts +29 -0
- package/dist/work-index.js +16 -2
- package/dist/workspace-revision.js +3 -1
- package/dist/writes-mode.d.ts +9 -0
- package/dist/writes-mode.js +8 -0
- package/package.json +15 -2
package/dist/server/chat.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { freshFacts, freshlyApprovable, freshnessFor, spendingOnlyOnSuccess, withFreshness } from "../scope.js";
|
|
1
2
|
import { handlersOf } from './handler-registry.js';
|
|
2
3
|
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
3
4
|
/** chat handlers, moved without changing their route bodies. */
|
|
@@ -34,9 +35,9 @@ import { runActivityOf } from "../task-activity.js";
|
|
|
34
35
|
import { teamWorkspaceHtml } from '../team-ui.js';
|
|
35
36
|
import { loadBotToken, redactToken } from "../telegram.js";
|
|
36
37
|
import { WORK_INDEX_MAX_LIMIT, workIndexPage } from "../work-index.js";
|
|
37
|
-
import { html, htmlString, postForm } from "../html.js";
|
|
38
|
+
import { html, htmlString, postForm, reauth } from "../html.js";
|
|
38
39
|
import { requestContext } from "./request-context.js";
|
|
39
|
-
import { refuse, screen } from "./chrome.js";
|
|
40
|
+
import { refuse, screen, whenTime } from "./chrome.js";
|
|
40
41
|
import { chatResultHref, chatReturnWithLatest, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
|
|
41
42
|
import { chatAckPage, chatPage, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
|
|
42
43
|
import {} from "./render-pages.js";
|
|
@@ -60,17 +61,18 @@ export function createChatHandlers(runtime) {
|
|
|
60
61
|
}
|
|
61
62
|
store.sweepStaleLeadTurns(now);
|
|
62
63
|
const request = url.searchParams.get("request") ?? "";
|
|
63
|
-
|
|
64
|
+
// Received: the engine took it, or the lead queue holds it (a queued acknowledgement lost on the way still frees the draft).
|
|
65
|
+
const received = /^[a-f0-9]{32}$/.test(request) && store.leadRequestReceived(session.id, request);
|
|
64
66
|
const requestedTask = url.searchParams.get("task");
|
|
65
67
|
const focusTask = requestedTask === null || requestedTask === "" ? null : taskChatFocus(requestedTask, now, who, { mintNonce: false });
|
|
66
68
|
if (requestedTask !== null && requestedTask !== "" && focusTask === null) {
|
|
67
69
|
// The lens's task is gone or no longer admitted: the page keeps its
|
|
68
70
|
// draft and says so; it never falls back to the unified thread.
|
|
69
|
-
return respond(response, 200, "application/json", JSON.stringify({ session: session.id, task: requestedTask, unavailable: true, received
|
|
71
|
+
return respond(response, 200, "application/json", JSON.stringify({ session: session.id, task: requestedTask, unavailable: true, received }));
|
|
70
72
|
}
|
|
71
73
|
const chatProject = focusTask !== null ? null : chatProjectOf(url.searchParams.getAll("project").filter(one => one !== ""));
|
|
72
74
|
if (chatProject === undefined)
|
|
73
|
-
return respond(response, 200, "application/json", JSON.stringify({ session: session.id, unavailable: true, received
|
|
75
|
+
return respond(response, 200, "application/json", JSON.stringify({ session: session.id, unavailable: true, received }));
|
|
74
76
|
const rows = leadConversationRows(who, principal, focusTask, now, chatProject);
|
|
75
77
|
const version = leadChatVersion({ ...rows, focusTask });
|
|
76
78
|
const known = url.searchParams.get("version") ?? "";
|
|
@@ -87,7 +89,7 @@ export function createChatHandlers(runtime) {
|
|
|
87
89
|
task: focusTask?.id ?? "",
|
|
88
90
|
version,
|
|
89
91
|
pending: rows.pending !== null,
|
|
90
|
-
received
|
|
92
|
+
received,
|
|
91
93
|
approval: focusTask?.approval?.digest ?? "",
|
|
92
94
|
...(fragments === null ? {} : { fragments }),
|
|
93
95
|
}));
|
|
@@ -357,7 +359,7 @@ export function createChatHandlers(runtime) {
|
|
|
357
359
|
// The one nonce in chat — this screen restates exact financial terms
|
|
358
360
|
// and re-enables spend, which is precisely what nonces are for.
|
|
359
361
|
const nonce = mintApprovalNonce(who.name, `chat-ack-${turn.id}`, String(turn.reservedMicrousd));
|
|
360
|
-
return sendScreen(response, 200, chatAckPage(chromeFor(project, "chat"), turn, nonce));
|
|
362
|
+
return sendScreen(response, 200, chatAckPage(chromeFor(project, "chat"), turn, nonce, store.leadTurnOfStep(turn.id)));
|
|
361
363
|
}
|
|
362
364
|
async function sharedActionReview(ctx) {
|
|
363
365
|
const { url, response } = ctx;
|
|
@@ -391,7 +393,22 @@ export function createChatHandlers(runtime) {
|
|
|
391
393
|
const review = mintSharedActionReview(store, principal, id, evidenceRoot, clock()), action = review.payload;
|
|
392
394
|
const terms = action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`);
|
|
393
395
|
const evidenceLink = action.operation === 'result_accept' ? html `<a href="/r/${Number(action.request['run'])}">Inspect this result</a>` : '';
|
|
394
|
-
|
|
396
|
+
// Cockpit B2: approving standard work may stand on a password remembered on this session, exactly as on the task
|
|
397
|
+
// page (the same rule approve() applies when the form comes back); the field then may be left empty.
|
|
398
|
+
const freshSince = (() => {
|
|
399
|
+
const task = typeof action.request['task'] === 'string' ? action.request['task'] : null;
|
|
400
|
+
if (action.operation !== 'scope_approve' || task === null)
|
|
401
|
+
return null;
|
|
402
|
+
const scope = store.getScope(task), fresh = withFreshness('approve-standard', () => freshnessFor(who.name, Date.now(), true));
|
|
403
|
+
if (scope === null || fresh === null)
|
|
404
|
+
return null;
|
|
405
|
+
const gate = store.approvalGate(task, who.name, 'person');
|
|
406
|
+
return freshlyApprovable(store, task, scope, gate.verdict === 'seal' && !gate.protectedWork) ? new Date(fresh.at) : null;
|
|
407
|
+
})();
|
|
408
|
+
const passwordField = !CHAT_ACTIONS[action.operation].password ? '' : reauth(freshSince === null
|
|
409
|
+
? html `<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>`
|
|
410
|
+
: html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label><p class="approval-password-note" data-approval-fresh>Your password from ${whenTime(freshSince.toISOString())} is remembered on this computer — leave this blank to approve with it, or type it again.</p>`);
|
|
411
|
+
const content = html `<section class="shared-action"><h1>${action.title}</h1>${evidenceLink}${terms}${postForm(`/chat/proposal/${id}/confirm`, html `<label class="arm"><input type="checkbox" name="confirm" value="yes" required>I confirm this exact action</label>${passwordField}<button>${CHAT_ACTIONS[action.operation].label}</button>`, { returnTo: '/chat', hidden: { nonce: review.nonce } })}</section>`;
|
|
395
412
|
return sendScreen(response, 200, screen('Review action', content, { chrome: chromeFor(action.repo, 'chat') }));
|
|
396
413
|
}
|
|
397
414
|
catch (error) {
|
|
@@ -665,6 +682,22 @@ export function createChatHandlers(runtime) {
|
|
|
665
682
|
leadSaid.delete(who.session.csrf);
|
|
666
683
|
return redirect(response, back);
|
|
667
684
|
}
|
|
685
|
+
/** Remove a message still waiting in the person's lead queue: theirs only, only while it waits; its words are forgotten. */
|
|
686
|
+
async function leadQueuedRemove(ctx) {
|
|
687
|
+
const { url, who, request, response, now, posted } = ctx;
|
|
688
|
+
const body = readForm(posted, CONSOLE_FORMS.leadQueuedRemove);
|
|
689
|
+
if (who.via !== "cookie")
|
|
690
|
+
return refuse(response, who, 403, "the lead is a browser surface");
|
|
691
|
+
const wantsJson = String(request.headers["accept"] ?? "").includes("application/json");
|
|
692
|
+
const back = safeChatReturn(body.get("return"));
|
|
693
|
+
const outcome = runtime.personalQueue.queue.remove(who.name, Number(url.pathname.split("/")[3]), now);
|
|
694
|
+
const words = outcome === "removed" ? "Removed. It won't be sent." : outcome === "started" ? "Already sent: it started before it could be removed." : "That message isn't waiting.";
|
|
695
|
+
if (wantsJson)
|
|
696
|
+
return respond(response, outcome === "not-found" ? 404 : outcome === "started" ? 409 : 200, "application/json", JSON.stringify({ ok: outcome === "removed", said: words }));
|
|
697
|
+
if (outcome === "not-found")
|
|
698
|
+
return refuse(response, who, 404, words, back);
|
|
699
|
+
return redirect(response, chatReturnWithSaid(back, words));
|
|
700
|
+
}
|
|
668
701
|
async function leadStop(ctx) {
|
|
669
702
|
const { who, response, now, posted } = ctx;
|
|
670
703
|
const body = readForm(posted, CONSOLE_FORMS.leadStop);
|
|
@@ -722,7 +755,12 @@ export function createChatHandlers(runtime) {
|
|
|
722
755
|
noteLead(who.session.csrf, null, "that proposal was already acted on");
|
|
723
756
|
return redirect(response, chatReturnWithLatest(back));
|
|
724
757
|
}
|
|
725
|
-
|
|
758
|
+
// A scope approval's re-check is spent only if the whole confirmation commits (one transaction: the proposal's
|
|
759
|
+
// settlement included). Any other action spends it whatever comes of it (its effects aren't all in the database).
|
|
760
|
+
const confirmIt = () => confirmLeadProposal(store, principal, id, now, { chatProvider: teamChatProvider, confirm: body.get("confirm") === "yes", via: "web", evidenceRoot, ...(body.has("nonce") ? { actionReview: { nonce: body.get("nonce") ?? "", password: body.get("token") ?? "" } } : {}) });
|
|
761
|
+
const proposed = store.getLeadProposal(id);
|
|
762
|
+
const approving = proposed?.kind === "action" && sharedActionPayload(proposed.payload)?.operation === "scope_approve";
|
|
763
|
+
const outcome = approving ? spendingOnlyOnSuccess(confirmIt, result => !result.ok || ("already" in result && result.already === true)) : confirmIt();
|
|
726
764
|
if (cardJson) {
|
|
727
765
|
if (!outcome.ok)
|
|
728
766
|
return cardAnswer(outcome.reason === "standing" ? 403 : outcome.reason === "not-yours" ? 404 : 409, false, outcome.said);
|
|
@@ -839,10 +877,12 @@ export function createChatHandlers(runtime) {
|
|
|
839
877
|
// its own terms and the thread shows why.
|
|
840
878
|
const leadSession = who.role === "approver" ? store.activeLeadSession(who.name) : null;
|
|
841
879
|
if (leadSession !== null) {
|
|
842
|
-
const
|
|
843
|
-
if (
|
|
880
|
+
const sentRequest = body.get("request");
|
|
881
|
+
if (sentRequest !== null && (body.getAll("request").length !== 1 || body.getAll("request-session").length !== 1 || body.get("request-session") !== String(leadSession.id))) {
|
|
844
882
|
return said(409, "This conversation changed. Reload it before sending your message.");
|
|
845
883
|
}
|
|
884
|
+
// Every send has one identity (a form without one gets it here): a message that waits is known by it.
|
|
885
|
+
const requestId = sentRequest ?? randomBytes(16).toString("hex");
|
|
846
886
|
const principal = leadPrincipal(who);
|
|
847
887
|
if (principal === null)
|
|
848
888
|
return wantsJson ? said(403, "your approver standing changed — sign in again") : refuse(response, who, 403, "your approver standing changed — sign in again", back);
|
|
@@ -851,15 +891,50 @@ export function createChatHandlers(runtime) {
|
|
|
851
891
|
return said(400, `a message is 1 to ${LEAD_MESSAGE_MAX_CHARS} characters`, leadSession.id);
|
|
852
892
|
}
|
|
853
893
|
const opened = store.openLeadThread(who.name, principal.ceilingDigest, now, chatScopeOf(focusTask, chatProject));
|
|
854
|
-
|
|
855
|
-
|
|
894
|
+
// The personal queue (docs/plans/lead-message-queue.md): while this person's lead is busy, or something of theirs
|
|
895
|
+
// waits, the message waits its turn instead of being refused. A request the engine already took replays as before.
|
|
896
|
+
const queueContext = focusTask !== null
|
|
897
|
+
? { task: focusTask.id, ...(viewedRun === null ? {} : { result: viewedRun.id }), ...(modeValue === null ? {} : { mode: modeValue }) }
|
|
898
|
+
: chatProject !== null ? { project: chatProject } : null;
|
|
899
|
+
const queueIt = () => runtime.personalQueue.queue.sendOrQueue({ approver: who.name, approverGeneration: principal.generation, session: leadSession.id,
|
|
900
|
+
ceilingDigest: principal.ceilingDigest, thread: opened.thread.id, text: message, requestId, context: queueContext }, clock());
|
|
901
|
+
// A message the engine would refuse for a secret never waits (nothing stored): it goes straight to the engine,
|
|
902
|
+
// which refuses it before any row exists, exactly as when the lead is free.
|
|
903
|
+
if (scanForSecrets(message).length === 0 && store.leadRequestReceipt(leadSession.id, requestId) === null) {
|
|
904
|
+
const decided = queueIt();
|
|
905
|
+
if (decided.kind === "conflict")
|
|
906
|
+
return said(409, "This message was already sent with different words. Reload the conversation to see it.", leadSession.id);
|
|
907
|
+
if (decided.kind === "full")
|
|
908
|
+
return said(409, decided.message, leadSession.id);
|
|
909
|
+
if (decided.kind === "queued") {
|
|
910
|
+
runtime.personalQueue.kick();
|
|
911
|
+
return wantsJson
|
|
912
|
+
? respond(response, 202, "application/json", JSON.stringify({ ok: true, queued: true, session: leadSession.id, task: focusTask?.id ?? "", ...(chatProject === null ? {} : { project: chatProject }), request: requestId, message: decided.row.message, status: decided.row.status }))
|
|
913
|
+
: redirect(response, chatReturnWithLatest(back));
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// The live display starts once this message holds a turn: a refusal (a secret, a turn still running) never blanks
|
|
917
|
+
// the display of a reply that is still running in the same conversation.
|
|
918
|
+
let onProgress;
|
|
919
|
+
const ended = (ok) => { if (onProgress !== undefined)
|
|
920
|
+
endLiveTurn(opened.thread.id, ok, onProgress); };
|
|
921
|
+
// Detached work never carries this request's browser session (Passwordless reads it): it runs with no session facts.
|
|
922
|
+
void freshFacts.run(null, () => runLeadTurn({ store, who: principal, session: leadSession, thread: opened.thread, config: enabled.config, key: enabled.key, message,
|
|
923
|
+
onAdmitted: () => { onProgress = beginLiveTurn(opened.thread.id); }, onProgress: event => onProgress?.(event), channel: "console", requestId, ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot }))
|
|
856
924
|
.then(outcome => {
|
|
925
|
+
// Lost a race to another turn of this person's between deciding and admission: it waits instead of being refused.
|
|
926
|
+
if (!outcome.ok && "refused" in outcome && (outcome.refused === "concurrent" || outcome.refused === "backlog") && queueIt().kind === "queued") {
|
|
927
|
+
ended(false);
|
|
928
|
+
return;
|
|
929
|
+
}
|
|
857
930
|
// A turn that saved its outcome in the thread (one stopped at its deadline) is said there, once.
|
|
858
931
|
if (!outcome.ok && !("saved" in outcome && outcome.saved === true))
|
|
859
932
|
noteLead(who.session.csrf, "turn" in outcome ? outcome.turn : null, outcome.message);
|
|
860
|
-
|
|
933
|
+
ended(outcome.ok);
|
|
861
934
|
})
|
|
862
|
-
.catch(() => { noteLead(who.session.csrf, null, "the turn failed unexpectedly");
|
|
935
|
+
.catch(() => { noteLead(who.session.csrf, null, "the turn failed unexpectedly"); ended(false); })
|
|
936
|
+
// A personal turn ended: the next waiting message (this person's or anyone's) may start now.
|
|
937
|
+
.finally(() => runtime.personalQueue.kick());
|
|
863
938
|
// Accepted for the engine, not received: the receipt is written by
|
|
864
939
|
// the turn's own admission, and the status poll is where the page
|
|
865
940
|
// learns of it — the same road as after a native send.
|
|
@@ -877,10 +952,6 @@ export function createChatHandlers(runtime) {
|
|
|
877
952
|
}
|
|
878
953
|
// The password, typed again, on EVERY message (v2 ruling 2): chat is
|
|
879
954
|
// spend, and a seven-day cookie is not a spend credential.
|
|
880
|
-
const password = body.get("token") ?? "";
|
|
881
|
-
if (password === "" || !authenticateApprover(who, password).ok) {
|
|
882
|
-
return redirect(response, `/chat?said=${encodeURIComponent("chat spends — your password, typed again, with every message")}`);
|
|
883
|
-
}
|
|
884
955
|
const message = (body.get("message") ?? "").trim();
|
|
885
956
|
if (message === "" || message.length > 2_000) {
|
|
886
957
|
return redirect(response, `/chat?said=${encodeURIComponent("a message is 1 to 2000 characters")}`);
|
|
@@ -912,16 +983,28 @@ export function createChatHandlers(runtime) {
|
|
|
912
983
|
if (disallowed !== null)
|
|
913
984
|
return redirect(response, `/chat?said=${encodeURIComponent(disallowed)}`);
|
|
914
985
|
const reserved = worstCaseForPrice(enabled.price, Buffer.byteLength(composed.body, "utf8"));
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
986
|
+
// The password comes last, right where the turn opens: every refusal above leaves an explicit re-check unspent,
|
|
987
|
+
// and a turn that doesn't open (a cap, a budget) puts it back.
|
|
988
|
+
const password = body.get("token") ?? "";
|
|
989
|
+
const opened = spendingOnlyOnSuccess(() => {
|
|
990
|
+
// Typed now, or (cockpit B2) a password or explicit provider re-check given on this session, on a direct local
|
|
991
|
+
// request, within its window. An ordinary provider sign-in never starts that window.
|
|
992
|
+
if (!withFreshness("chat-send", () => authenticateApprover(who, password)).ok)
|
|
993
|
+
return null;
|
|
994
|
+
return store.openChatTurn({
|
|
995
|
+
approver: who.name,
|
|
996
|
+
credentialKey: enabled.credentialKey,
|
|
997
|
+
provider: enabled.config.provider,
|
|
998
|
+
model: enabled.config.model,
|
|
999
|
+
reservedMicrousd: reserved,
|
|
1000
|
+
dailyTurns: enabled.config.dailyTurns,
|
|
1001
|
+
weeklyCeilingMicrousd: enabled.config.weeklyCeilingMicrousd,
|
|
1002
|
+
deadlineMs: TURN_WALL_CLOCK_MS + 10_000,
|
|
1003
|
+
}, now);
|
|
1004
|
+
}, result => result === null || !result.ok);
|
|
1005
|
+
if (opened === null) {
|
|
1006
|
+
return redirect(response, `/chat?said=${encodeURIComponent("chat spends — your password, typed again, with every message")}`);
|
|
1007
|
+
}
|
|
925
1008
|
if (!opened.ok) {
|
|
926
1009
|
const said = opened.reason === "latched"
|
|
927
1010
|
? "a turn with unknown cost blocks this credential — acknowledge it below first"
|
|
@@ -1046,6 +1129,9 @@ export function createChatHandlers(runtime) {
|
|
|
1046
1129
|
const wanted = values[0] ?? "";
|
|
1047
1130
|
return values.length === 1 && visible(wanted) && managedRepos().includes(wanted) ? wanted : undefined;
|
|
1048
1131
|
}
|
|
1132
|
+
/** Which live display each progress handle started: ending a turn ends only its own display (a turn stopped from
|
|
1133
|
+
* another window that settles late can never end the next turn's). */
|
|
1134
|
+
const progressOf = new WeakMap();
|
|
1049
1135
|
function beginLiveTurn(thread) {
|
|
1050
1136
|
const previous = liveTurns.get(thread);
|
|
1051
1137
|
if (previous?.expiry)
|
|
@@ -1055,7 +1141,7 @@ export function createChatHandlers(runtime) {
|
|
|
1055
1141
|
// A page that has this conversation open follows the reply (GET /live, its chat room).
|
|
1056
1142
|
runtime.liveTurnStarted(thread);
|
|
1057
1143
|
const current = () => live.steps.at(-1) ?? (live.steps.push({ tools: [], toolCalls: [], text: "" }), live.steps.at(-1));
|
|
1058
|
-
|
|
1144
|
+
const progress = (event) => {
|
|
1059
1145
|
if (live.done)
|
|
1060
1146
|
return;
|
|
1061
1147
|
if (event.kind === "step")
|
|
@@ -1078,11 +1164,15 @@ export function createChatHandlers(runtime) {
|
|
|
1078
1164
|
for (const listener of live.listeners)
|
|
1079
1165
|
listener();
|
|
1080
1166
|
};
|
|
1167
|
+
progressOf.set(progress, live);
|
|
1168
|
+
return progress;
|
|
1081
1169
|
}
|
|
1082
|
-
function endLiveTurn(thread, ok) {
|
|
1170
|
+
function endLiveTurn(thread, ok, progress) {
|
|
1083
1171
|
const live = liveTurns.get(thread);
|
|
1084
1172
|
if (live === undefined || live.done)
|
|
1085
1173
|
return;
|
|
1174
|
+
if (progress !== undefined && progressOf.get(progress) !== live)
|
|
1175
|
+
return;
|
|
1086
1176
|
live.done = true;
|
|
1087
1177
|
live.ok = ok;
|
|
1088
1178
|
for (const listener of live.listeners)
|
|
@@ -1388,6 +1478,7 @@ export function createChatHandlers(runtime) {
|
|
|
1388
1478
|
"chat.mate-follow": leadFollow,
|
|
1389
1479
|
"chat.mate-end": leadEnd,
|
|
1390
1480
|
"chat.mate-stop": leadStop,
|
|
1481
|
+
"chat.queued-remove": leadQueuedRemove,
|
|
1391
1482
|
"chat.proposal.confirm": chatProposalAct("confirm"),
|
|
1392
1483
|
"chat.proposal.dismiss": chatProposalAct("dismiss"),
|
|
1393
1484
|
"coordinator.proposal.confirm": coordinatorProposalAct("confirm"),
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -128,13 +128,16 @@ export type Chrome = {
|
|
|
128
128
|
* at the sink like every page, fetched same-origin — and the nonce'd CSP
|
|
129
129
|
* refuses to execute anything the region could smuggle. Nothing reads on a timer.
|
|
130
130
|
*/
|
|
131
|
-
export declare function regionScript(regionId: string, fragmentName: string, path?: string
|
|
131
|
+
export declare function regionScript(regionId: string, fragmentName: string, path?: string, options?: {
|
|
132
|
+
animate?: boolean;
|
|
133
|
+
}): string;
|
|
132
134
|
/**
|
|
133
135
|
* The live transcript's DEDICATED poller (arc 1 §4): a JSON byte-offset
|
|
134
136
|
* protocol, not an HTML fragment — the response is raw sanitized text and
|
|
135
137
|
* the ONLY sink is textContent, so nothing here can become markup. One
|
|
136
138
|
* request in flight, visibility-paused, stops on `final`, and a `replaced`
|
|
137
|
-
* answer restarts from zero with a visible
|
|
139
|
+
* answer — or another file's generation — restarts from zero with a visible
|
|
140
|
+
* line, never a silent re-read.
|
|
138
141
|
*/
|
|
139
142
|
export declare function transcriptScript(path?: string, elementId?: string): string;
|
|
140
143
|
/**
|
|
@@ -170,6 +173,10 @@ export type Screen = {
|
|
|
170
173
|
/** Render sensitive even when no password field is visible — one-time
|
|
171
174
|
* secrets and judgment calls the classifier cannot see. */
|
|
172
175
|
forceSensitive?: boolean;
|
|
176
|
+
/** The page's sensitivity, said explicitly (cockpit §2, R14): true for a page that asks for a password or shows a
|
|
177
|
+
* secret even when no password field renders (an SSO step-up, a fresh-password grant); false only where a page is
|
|
178
|
+
* known safe. Unset: judged from the page as authored. A sensitive page has no sidebar states and no summary room. */
|
|
179
|
+
sensitive?: boolean;
|
|
173
180
|
/** Structured conversation; complex guarded forms stay native islands. */
|
|
174
181
|
workspace?: Partial<Pick<BrowserWorkspace, 'conversation' | 'team' | 'focus' | 'result' | 'notices' | 'view' | 'firstRun' | 'phone' | 'home'>> & {
|
|
175
182
|
/** Markup the workspace shows in place; it becomes bytes where the page's JSON is written (sendScreen). */
|
|
@@ -292,3 +299,10 @@ export declare function safePrUrl(url: string | null): string | null;
|
|
|
292
299
|
export declare function shortDigest(digest: string): Html;
|
|
293
300
|
/** One display line, bounded — turn text is data, never layout. */
|
|
294
301
|
export declare function oneLineOf(text: string, cap: number): string;
|
|
302
|
+
/**
|
|
303
|
+
* The board's clocks tick in the page: each <time data-tick> counts up from
|
|
304
|
+
* its datetime, measured against the server's own now (the board's data-now)
|
|
305
|
+
* so a skewed device clock cannot run them backwards. The server-rendered
|
|
306
|
+
* minutes are the answer without script.
|
|
307
|
+
*/
|
|
308
|
+
export declare function boardTickScript(regionId?: string): string;
|
package/dist/server/chrome.js
CHANGED
|
@@ -161,7 +161,7 @@ export const WORKSPACE_STYLE = styleAsset(STYLE + BRAND_MARK_CSS + INBOX_TABS_CS
|
|
|
161
161
|
* at the sink like every page, fetched same-origin — and the nonce'd CSP
|
|
162
162
|
* refuses to execute anything the region could smuggle. Nothing reads on a timer.
|
|
163
163
|
*/
|
|
164
|
-
export function regionScript(regionId, fragmentName, path) {
|
|
164
|
+
export function regionScript(regionId, fragmentName, path, options = {}) {
|
|
165
165
|
// Where the fragment lives: the page's own URL by default; an explicit
|
|
166
166
|
// path when a page embeds another entity's region (the task page embeds
|
|
167
167
|
// the live run's peek, slice 1c — no proxy route exists for it).
|
|
@@ -184,7 +184,7 @@ export function regionScript(regionId, fragmentName, path) {
|
|
|
184
184
|
`function keep(){var data={lanes:[],focus:null,pager:-1,fold:{}};` +
|
|
185
185
|
`region.querySelectorAll("details.lane").forEach(function(d){var k=laneKey(d);if(k)data.fold[k]=d.open;});` +
|
|
186
186
|
`var act=document.activeElement;` +
|
|
187
|
-
`if(act&®ion.contains(act)&&act.matches&&act.matches("a.row, a.lane-card"))data.focus=act.getAttribute("href")
|
|
187
|
+
`if(act&®ion.contains(act)&&act.matches&&act.matches("a.row, a.lane-card, a.worker-job")){data.focus=act.getAttribute("href");data.focusKind=act.matches("a.worker-job")?"a.worker-job":"a.row, a.lane-card";}` +
|
|
188
188
|
`var board=region.querySelector(".board");` +
|
|
189
189
|
`if(board&&board.scrollLeft>0){var lanes=board.querySelectorAll(".lane");` +
|
|
190
190
|
`var bc=board.getBoundingClientRect();var mid=bc.left+bc.width/2;var best=-1,bd=1e9;` +
|
|
@@ -208,24 +208,56 @@ export function regionScript(regionId, fragmentName, path) {
|
|
|
208
208
|
`if(board){var lanes=board.querySelectorAll(".lane");var at=Math.min(data.pager,lanes.length-1);` +
|
|
209
209
|
`if(at>=0){var br=board.getBoundingClientRect();var lr=lanes[at].getBoundingClientRect();` +
|
|
210
210
|
`board.scrollLeft=board.scrollLeft+(lr.left+lr.width/2)-(br.left+br.width/2);}}}` +
|
|
211
|
-
`if(data.focus!==null){var links=region.querySelectorAll(
|
|
211
|
+
`if(data.focus!==null){var links=region.querySelectorAll(data.focusKind);` +
|
|
212
212
|
`for(var i=0;i<links.length;i++){if(links[i].getAttribute("href")===data.focus){links[i].focus({preventScroll:true});break;}}}}` +
|
|
213
213
|
`function tell(){if(!stamp||stopped)return;var s=Math.round((Date.now()-last)/1000);` +
|
|
214
214
|
`stamp.textContent=failed?"stale — retrying ("+s+"s old)":"updated "+s+"s ago";}` +
|
|
215
215
|
`setInterval(tell,1000);` +
|
|
216
|
+
// The animated swap (the live board): a card that changed lane slides there and a new one drops in, through one
|
|
217
|
+
// same-document view transition keyed by each card's data-vt; a worker whose builds changed flashes once
|
|
218
|
+
// (data-flash). Never under reduced motion, in a hidden tab, or where the browser lacks view transitions — there
|
|
219
|
+
// the swap is the plain instant one. Names are set only for the swap and cleared after it.
|
|
220
|
+
`var animate=${options.animate === true ? "true" : "false"};` +
|
|
221
|
+
// A name spells the whole key in hex — two tasks can never share one, so a card's identity never jumps to another.
|
|
222
|
+
`function vtName(v){var n="so-c";for(var i=0;i<v.length;i++){n+=("000"+v.charCodeAt(i).toString(16)).slice(-4);}return n;}` +
|
|
223
|
+
`function nameAll(){var seen={};region.querySelectorAll("[data-vt]").forEach(function(e){var n=vtName(e.getAttribute("data-vt"));if(seen[n])return;seen[n]=1;e.style.setProperty("view-transition-name",n);});}` +
|
|
224
|
+
`function flashes(){var m={};region.querySelectorAll("[data-flash]").forEach(function(e){m[e.getAttribute("data-flash")]=e.getAttribute("data-flash-value");});return m;}` +
|
|
225
|
+
`function apply(t){var before=animate?flashes():{};var kept=keep();region.innerHTML=t;restore(kept);` +
|
|
226
|
+
`if(animate)region.querySelectorAll("[data-flash]").forEach(function(e){var k=e.getAttribute("data-flash"),v=e.getAttribute("data-flash-value");if(k in before&&before[k]!==v&&v!=="")e.classList.add("flash");});}` +
|
|
227
|
+
// The swap resolves when its transition has finished, and the next fetch waits for it (refresh holds busy), so
|
|
228
|
+
// one transition runs at a time; each cleans up only its own (the generation), never a later one's.
|
|
229
|
+
`var generation=0;` +
|
|
230
|
+
`function swap(t){var calm=!window.matchMedia||matchMedia("(prefers-reduced-motion: reduce)").matches;` +
|
|
231
|
+
`if(!animate||calm||document.hidden||typeof document.startViewTransition!=="function"){apply(t);return Promise.resolve();}` +
|
|
232
|
+
// Bounded: a transition that never settles (or never runs its update) is let go after 1.5 s — the fragment is
|
|
233
|
+
// applied exactly once either way, so the board can never stop refreshing behind an animation.
|
|
234
|
+
`var root=document.documentElement;var mine=++generation;var applied=false;var settled=false;nameAll();root.classList.add("so-swap");` +
|
|
235
|
+
`var once=function(){if(applied)return false;applied=true;apply(t);return true;};` +
|
|
236
|
+
`var end=function(){if(settled)return;settled=true;once();if(mine!==generation)return;root.classList.remove("so-swap");region.querySelectorAll("[data-vt]").forEach(function(e){e.style.removeProperty("view-transition-name");});};` +
|
|
237
|
+
`var vt;try{vt=document.startViewTransition(function(){if(once())nameAll();});}catch(e){end();return Promise.resolve();}` +
|
|
238
|
+
`return new Promise(function(done){var timer=setTimeout(function(){end();done();},1500);` +
|
|
239
|
+
`var fin=function(){clearTimeout(timer);end();done();};try{Promise.resolve(vt&&vt.finished).then(fin,fin);}catch(e){fin();}});}` +
|
|
216
240
|
`function refresh(){if(stopped)return;if(busy){again=true;return;}busy=true;again=false;` +
|
|
217
241
|
`var q=location.search?location.search+"&fragment="+${JSON.stringify(fragmentName)}:"?fragment="+${JSON.stringify(fragmentName)};` +
|
|
218
242
|
`fetch(${target},{redirect:"manual",cache:"no-store"})` +
|
|
219
243
|
`.then(function(r){if(r.type==="opaqueredirect"||r.status===401||r.status===403){location.href="/login";return null;}` +
|
|
220
244
|
`return r.ok?r.text():null;})` +
|
|
221
|
-
`.then(function(t){if(t){
|
|
245
|
+
`.then(function(t){if(t){return swap(t).then(function(){last=Date.now();failed=false;});}failed=true;})` +
|
|
222
246
|
`.catch(function(){failed=true;})` +
|
|
223
247
|
// A fragment that marks itself final stops: a finished or abandoned build is not fetched again.
|
|
224
248
|
`.then(function(){busy=false;tell();if(region.querySelector("[data-region-stop]")){stopped=true;if(stamp)stamp.textContent="";return;}if(again)refresh();});}` +
|
|
225
249
|
`function heard(name){if(name==="change"||name==="reload")refresh();}` +
|
|
226
|
-
|
|
227
|
-
`
|
|
250
|
+
// On a workspace page the page's one stream (browser/live.ts) carries the workspace room: ask it, never open a second.
|
|
251
|
+
`function mine(e){var d=e.detail;if(d&&(d.room==="workspace"||d.room===null))heard(d.event);}` +
|
|
252
|
+
`if(document.getElementById("standing-orders-workspace")){(window.__soLiveRooms=window.__soLiveRooms||[]).push("workspace");addEventListener("so:live",mine);}` +
|
|
253
|
+
`else if(document.documentElement.dataset.live==="1"){addEventListener("so:live",mine);}` +
|
|
254
|
+
// Refused (signed out, access lost): stop instead of reconnecting at once into a request that can't sign in, and
|
|
255
|
+
// listen again in 30 s, as the page's own stream does (browser/live.ts RETRY_MS).
|
|
256
|
+
`else if(typeof EventSource!=="undefined"){var listen=function(){var es=new EventSource("/live?room=workspace");` +
|
|
228
257
|
`es.addEventListener("change",function(){heard("change");});es.addEventListener("reload",function(){heard("reload");});` +
|
|
258
|
+
// A retry the server answers with an error (still signed out) is given up by the browser: try again later too.
|
|
259
|
+
`var again=function(){es.close();setTimeout(listen,30000);};` +
|
|
260
|
+
`es.addEventListener("ended",again);es.addEventListener("error",function(){if(es.readyState===2)again();});};listen();` +
|
|
229
261
|
`document.addEventListener("visibilitychange",function(){if(!document.hidden)refresh();});}` +
|
|
230
262
|
`})();`);
|
|
231
263
|
}
|
|
@@ -234,23 +266,24 @@ export function regionScript(regionId, fragmentName, path) {
|
|
|
234
266
|
* protocol, not an HTML fragment — the response is raw sanitized text and
|
|
235
267
|
* the ONLY sink is textContent, so nothing here can become markup. One
|
|
236
268
|
* request in flight, visibility-paused, stops on `final`, and a `replaced`
|
|
237
|
-
* answer restarts from zero with a visible
|
|
269
|
+
* answer — or another file's generation — restarts from zero with a visible
|
|
270
|
+
* line, never a silent re-read.
|
|
238
271
|
*/
|
|
239
272
|
export function transcriptScript(path, elementId = "live-transcript") {
|
|
240
273
|
const base = path === undefined ? "location.pathname" : JSON.stringify(path);
|
|
241
274
|
return (`(function(){var out=document.getElementById(${JSON.stringify(elementId)});if(!out)return;` +
|
|
242
|
-
`var from=0,busy=false,stopped=false;` +
|
|
275
|
+
`var from=0,gen="",inkey=false,ktail="",busy=false,stopped=false;` +
|
|
243
276
|
`function near(){return out.scrollHeight-out.scrollTop-out.clientHeight<40;}` +
|
|
244
277
|
`function go(){if(stopped)return;if(busy||document.hidden){later();return;}busy=true;` +
|
|
245
|
-
`fetch(${base}+"?fragment=transcript&from="+from,{redirect:"manual",cache:"no-store"})` +
|
|
278
|
+
`fetch(${base}+"?fragment=transcript&from="+from+(gen?"&gen="+encodeURIComponent(gen):"")+(inkey?"&inkey=1":"")+(ktail?"&ktail="+encodeURIComponent(ktail):""),{redirect:"manual",cache:"no-store"})` +
|
|
246
279
|
`.then(function(r){if(r.type==="opaqueredirect"||r.status===401||r.status===403){stopped=true;return null;}` +
|
|
247
280
|
`if(r.status===409)return{error:"replaced"};return r.ok?r.json():null;})` +
|
|
248
281
|
`.then(function(d){busy=false;if(d===null){later();return;}if(stopped)return;` +
|
|
249
|
-
`if(d.error==="replaced"){from=0;out.textContent="[the view restarted]\\n";later();return;}` +
|
|
282
|
+
`if(d.error==="replaced"){from=0;gen="";inkey=false;ktail="";out.textContent="[the view restarted]\\n";later();return;}` +
|
|
250
283
|
`if(d.error){stopped=true;return;}` +
|
|
251
284
|
`var stick=near();` +
|
|
252
285
|
`if(typeof d.text==="string"&&d.text!==""){out.appendChild(document.createTextNode(d.text));if(stick)out.scrollTop=out.scrollHeight;}` +
|
|
253
|
-
`if(typeof d.nextOffset==="number"&&d.nextOffset>=from){from=d.nextOffset;}
|
|
286
|
+
`if(typeof d.nextOffset==="number"&&d.nextOffset>=from){from=d.nextOffset;}if(typeof d.gen==="string")gen=d.gen;if(typeof d.inKey==="boolean")inkey=d.inKey;if(typeof d.keyTail==="string")ktail=d.keyTail;` +
|
|
254
287
|
`if(d.final===true){stopped=true;var m=document.getElementById(${JSON.stringify(`${elementId}-state`)});` +
|
|
255
288
|
`if(m)m.textContent="the agent finished — the record on this page is the story";return;}` +
|
|
256
289
|
`later();})` +
|
|
@@ -680,6 +713,8 @@ export const NAV_ICONS = {
|
|
|
680
713
|
export function workToolRows(scoped = false, _offersChat = false, offersCode = false) {
|
|
681
714
|
const rows = [
|
|
682
715
|
// D5: coding sessions are deprecated; /code still opens this release, but nothing links to it.
|
|
716
|
+
// Live terminals: an operator's own Claude Code, Codex or shell, in a real terminal (docs/plans/live-terminal.md).
|
|
717
|
+
...(offersCode ? [{ key: "code", href: "/term", label: "Live", hint: "your own terminals: Claude Code, Codex or a shell" }] : []),
|
|
683
718
|
{ key: "inbox", href: "/inbox", label: "Inbox", hint: "questions, approvals, and repairs to act on" },
|
|
684
719
|
{ key: "board", href: "/board", label: "Board", hint: "lanes by state, with the order view" },
|
|
685
720
|
{ key: "tasks", href: "/tasks", label: "Task list", hint: "everything, filterable by state" },
|
|
@@ -749,3 +784,20 @@ export function oneLineOf(text, cap) {
|
|
|
749
784
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
750
785
|
return flat.length <= cap ? flat : `${flat.slice(0, cap - 1)}…`;
|
|
751
786
|
}
|
|
787
|
+
/**
|
|
788
|
+
* The board's clocks tick in the page: each <time data-tick> counts up from
|
|
789
|
+
* its datetime, measured against the server's own now (the board's data-now)
|
|
790
|
+
* so a skewed device clock cannot run them backwards. The server-rendered
|
|
791
|
+
* minutes are the answer without script.
|
|
792
|
+
*/
|
|
793
|
+
export function boardTickScript(regionId = "board-region") {
|
|
794
|
+
return (`(function(){var seen=null,offset=0;var region=document.getElementById(${JSON.stringify(regionId)});` +
|
|
795
|
+
`function fmt(s){if(s<0)s=0;var h=Math.floor(s/3600),m=Math.floor(s%3600/60),x=s%60;return h>0?h+"h "+(m<10?"0":"")+m+"m":m+":"+(x<10?"0":"")+x;}` +
|
|
796
|
+
`function run(){var scope=region||document;var anchor=scope.querySelector("[data-now]");var v=anchor?anchor.getAttribute("data-now"):null;` +
|
|
797
|
+
`if(v!==null&&v!==seen){seen=v;var n=Number(v);offset=isFinite(n)?n-Date.now():0;}` +
|
|
798
|
+
`var now=Date.now()+offset;scope.querySelectorAll("time[data-tick]").forEach(function(t){var at=Date.parse(t.getAttribute("datetime")||"");if(isNaN(at))return;var text=fmt(Math.floor((now-at)/1000));if(t.textContent!==text)t.textContent=text;});}` +
|
|
799
|
+
// A swapped region brings back the server's minutes, so a swap (the region's children replaced — never a clock's
|
|
800
|
+
// own text) ticks at once and no clock flickers back to them. Hidden tabs do not tick.
|
|
801
|
+
`run();setInterval(function(){if(!document.hidden)run();},1000);document.addEventListener("visibilitychange",function(){if(!document.hidden)run();});` +
|
|
802
|
+
`if(region&&typeof MutationObserver==="function")new MutationObserver(run).observe(region,{childList:true});})();`);
|
|
803
|
+
}
|