toolroll 0.9.53 → 0.9.55
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 +17 -0
- package/README.md +5 -4
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +15 -20
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +2 -3
- package/dist/browser-workspace.js +0 -3
- package/dist/chat-ask.js +8 -8
- package/dist/chat-core.d.ts +171 -0
- package/dist/chat-core.js +288 -0
- package/dist/chat-decide.d.ts +10 -8
- package/dist/chat-decide.js +26 -21
- package/dist/chat-delivery-state.d.ts +51 -75
- package/dist/chat-delivery-state.js +140 -170
- package/dist/chat-delivery.d.ts +21 -2
- package/dist/chat-delivery.js +313 -194
- package/dist/chat-flow.d.ts +29 -1
- package/dist/chat-flow.js +24 -50
- package/dist/chat-messages.d.ts +319 -0
- package/dist/chat-messages.js +841 -0
- package/dist/chat-migration.d.ts +30 -0
- package/dist/chat-migration.js +802 -0
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-quiet.d.ts +3 -0
- package/dist/chat-quiet.js +5 -0
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +9 -3
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/chat-content.d.ts +71 -2
- package/dist/contracts/chat-content.js +22 -3
- package/dist/contracts/chat-tables.d.ts +11 -4
- package/dist/contracts/chat-tables.js +165 -87
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +1 -1
- package/dist/contracts/console-api.js +6 -5
- package/dist/contracts/discord-callback.js +2 -1
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/slack-callback.js +3 -2
- package/dist/contracts/store-json.d.ts +1 -5
- package/dist/contracts/store-json.js +1 -4
- package/dist/contracts/teams-callback.js +2 -1
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/discord-chat.js +19 -38
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +21 -55
- package/dist/discord.js +4 -14
- package/dist/envelope.js +10 -0
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/export.js +4 -4
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-triggers.d.ts +1 -0
- package/dist/flow-triggers.js +16 -14
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +19 -30
- package/dist/flows.js +2 -1
- package/dist/guides.js +108 -3
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +18 -20
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/lead-context.d.ts +11 -1
- package/dist/lead-context.js +6 -8
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +2 -1
- package/dist/limits-ui.js +7 -7
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mate-tools.js +3 -2
- package/dist/mcp-oauth.js +35 -36
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/modes.d.ts +13 -1
- package/dist/modes.js +17 -1
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +30 -0
- package/dist/notification-rows.js +102 -0
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.js +16 -9
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/plane-review.js +5 -11
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +15 -27
- package/dist/project-delete.js +21 -26
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/result-shots.js +2 -2
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +69 -58
- package/dist/server/chat.d.ts +3 -6
- package/dist/server/chat.js +969 -1006
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +840 -766
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +5 -2
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +580 -572
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +37 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +147 -0
- package/dist/server/render-settings.js +577 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +163 -75
- package/dist/server/runtime.d.ts +15 -7
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2182 -2117
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1486 -1418
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-api.d.ts +5 -1
- package/dist/slack-api.js +2 -2
- package/dist/slack-chat.d.ts +9 -4
- package/dist/slack-chat.js +29 -52
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +23 -57
- package/dist/slack.js +6 -16
- package/dist/spend-ui.d.ts +2 -1
- package/dist/spend-ui.js +9 -32
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/store.d.ts +97 -364
- package/dist/store.js +207 -1531
- package/dist/surface.js +14 -3
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teammate-question.js +12 -12
- package/dist/teammates-ui.d.ts +5 -4
- package/dist/teammates-ui.js +61 -90
- package/dist/teams-chat.js +12 -9
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +22 -33
- package/dist/teams.js +4 -5
- package/dist/telegram-flow.js +5 -5
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram.js +4 -4
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +10 -0
- package/dist/toolroll-update.js +50 -3
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-revision.js +3 -1
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -1
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/slack-state.d.ts +0 -13
- package/dist/slack-state.js +0 -10
|
@@ -0,0 +1,927 @@
|
|
|
1
|
+
/** The conversation: chat and lead pages, proposal cards and the task chat. */
|
|
2
|
+
import { assignmentSummaryHtml } from "../assignment-ui.js";
|
|
3
|
+
import {} from "../assignment.js";
|
|
4
|
+
import {} from "../browser-workspace.js";
|
|
5
|
+
import { CHAT_ACTIONS, sharedActionNeedsReview, sharedActionPayload, sharedActionReviewPath } from "../chat-actions.js";
|
|
6
|
+
import { CHAT_CONTINUITY_SCRIPT } from "../chat-continuity.js";
|
|
7
|
+
import { CHAT_CONTROLS, chatControlHref, isChatControl } from "../chat-controls.js";
|
|
8
|
+
import { chatActivityDetailsHtml, chatWorkingHtml, completedWorkHtml } from "../chat-polish.js";
|
|
9
|
+
import { CHAT_TASK_ACTIONS, isChatTaskAction } from "../chat-task-actions.js";
|
|
10
|
+
import { isCheckLevel } from "../check-levels.js";
|
|
11
|
+
import { isSubscriptionChatProvider } from "../converse.js";
|
|
12
|
+
import {} from "../dispatch.js";
|
|
13
|
+
import { firstTaskJourney, START_COMMAND } from "../first-run.js";
|
|
14
|
+
import { FLOW_HREF } from "../flow-engine.js";
|
|
15
|
+
import { PROPOSAL_WAIT_REASON, proposalActGate } from "../mate-doors.js";
|
|
16
|
+
import {} from "../mate-progress.js";
|
|
17
|
+
import { MATE_MESSAGE_MAX_CHARS } from "../mate.js";
|
|
18
|
+
import {} from "../needs-you.js";
|
|
19
|
+
import { projectName } from "../project.js";
|
|
20
|
+
import { replyHtmlInline, shapeReply } from "../reply-shape.js";
|
|
21
|
+
import { RESULT_REVIEW_SCRIPT } from "../result-review.js";
|
|
22
|
+
import {} from "../scope.js";
|
|
23
|
+
import { MATE_ASK_OTHER } from "../store.js";
|
|
24
|
+
import {} from "../task-control.js";
|
|
25
|
+
import { assignmentStageOf, stageOfCode } from "../task-status.js";
|
|
26
|
+
import {} from "../team-chat-authorization.js";
|
|
27
|
+
import {} from "../team-contract.js";
|
|
28
|
+
import { buildProgressOf } from "../workspace-ui.js";
|
|
29
|
+
import { html, htmlString, isHtml, joinHtml, postForm, replaceMarkup } from "../html.js";
|
|
30
|
+
import { requestContext, withFormToken } from "./request-context.js";
|
|
31
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
32
|
+
import { chatMoney, oneLineOf, relativeAge, safePrUrl, screen, sentenceCase, strokeIcon } from "./chrome.js";
|
|
33
|
+
import { chatResultHref, projectChatHref, taskChatHref, taskHref } from "./http.js";
|
|
34
|
+
import {} from "./render-pages.js";
|
|
35
|
+
import { completionReceiptCard } from "./render-results.js";
|
|
36
|
+
import {} from "./render-settings.js";
|
|
37
|
+
import { agentsAvailabilityHtml, agentsBadgesHtml, agentsStripHtml, agentsSummaryWords, agentsWhyHtml, approvalSheetHtml, checkProgressHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, dispatchHeadline, milestoneProgressHtml, planRevisionLedgerHtml, profileWords, sizeConsequence, taskControlDetailsHtml, taskStatusCard, taskViewSwitch } from "./render-tasks.js";
|
|
38
|
+
/** The installation fact that the lead was turned on by default, once (its value: the provider). */
|
|
39
|
+
export const LEAD_BY_DEFAULT_FACT = "lead-on-by-default";
|
|
40
|
+
/** The installation fact that the phone card was put away. */
|
|
41
|
+
export const PHONE_CARD_FACT = "phone-card-dismissed";
|
|
42
|
+
export const TASK_COMPOSER_MODES = {
|
|
43
|
+
build: "The operator chose Build: if they ask for a change to the result, read it with get_result and propose a revision of this task with propose_review (operation revise) for them to confirm. Nothing builds until they confirm the card.",
|
|
44
|
+
plan: "The operator chose Plan only: answer with a short plan for what they ask. Do not propose a revision, a new task or any other action this turn.",
|
|
45
|
+
answer: "The operator chose Just answer: answer from what you can read. Do not propose any action this turn.",
|
|
46
|
+
};
|
|
47
|
+
export function taskChatContext(focus) {
|
|
48
|
+
return html `<aside class="task-chat-context" aria-label="current task"><div class="task-chat-context-head"><span class="eyebrow">current task</span></div><h2>${focus.title}</h2><p class="meta mono">${focus.id}${focus.project === null ? "" : html ` · ${focus.project}`}</p>${
|
|
49
|
+
// The agents in chat (v47): the same summary the task page and CLI
|
|
50
|
+
// print — who does what — with the reasons one tap away, so a
|
|
51
|
+
// conversation never hides which agent runs.
|
|
52
|
+
focus.route === null
|
|
53
|
+
? ""
|
|
54
|
+
: html `<div class="task-chat-agents-aside"><span class="eyebrow">agents</span><p class="agents-summary">${agentsSummaryWords(focus.route)}</p><div class="agents-badges">${agentsBadgesHtml(focus.route)}</div>${focus.route.projection === null ? "" : html `${agentsAvailabilityHtml(focus.route.projection)}${agentsWhyHtml(focus.route.projection)}`}<p class="meta"><a href="${taskHref(focus.id)}#agents">Change agents on the task →</a></p></div>`}<div class="task-chat-overview-actions"><a class="task-chat-overview-link" href="${taskHref(focus.id)}">Open full overview →</a></div></aside>`;
|
|
55
|
+
}
|
|
56
|
+
export function taskChatApproval(focus, csrf) {
|
|
57
|
+
const approval = focus.approval;
|
|
58
|
+
if (focus.plan === "requested") {
|
|
59
|
+
// The task status already names planning. No second status card.
|
|
60
|
+
return html ``;
|
|
61
|
+
}
|
|
62
|
+
if (approval === null)
|
|
63
|
+
return html ``;
|
|
64
|
+
const scope = approval.scope;
|
|
65
|
+
const returnTo = taskChatHref(focus.id);
|
|
66
|
+
if (approval.revision !== null && "problem" in approval.revision) {
|
|
67
|
+
return html `<section class="card chat-action-card" id="task-chat-action"><span class="eyebrow">approval needs attention</span><h2>The revision brief can’t be verified</h2><p class="meta">${approval.revision.problem}</p><a class="button-link" href="${taskHref(focus.id)}#approve">Fix this on the task →</a></section>`;
|
|
68
|
+
}
|
|
69
|
+
const door = consentDoorOf(scope, focus.route);
|
|
70
|
+
if (!door.open)
|
|
71
|
+
return consentClosedHtml(focus.id, door, "chat");
|
|
72
|
+
if (approval.nonce === "") {
|
|
73
|
+
return html `<section class="card chat-action-card" id="task-chat-action"><span class="eyebrow">approval needs attention</span><h2>The agent setup isn’t ready yet</h2>${profileWords(scope)}<a class="button-link" href="${taskHref(focus.id)}#scope">Fix the agent setup →</a></section>`;
|
|
74
|
+
}
|
|
75
|
+
// The same sheet as the task page (approval critique, Oct 2): the plan
|
|
76
|
+
// open in plain rows, one Approve & start, the rest in Details.
|
|
77
|
+
return html `<section class="card chat-action-card chat-plan" id="task-chat-action" data-approval="${approval.digest}">${approvalSheetHtml({
|
|
78
|
+
surface: "chat",
|
|
79
|
+
action: `${taskHref(focus.executionId)}/approve`,
|
|
80
|
+
csrf,
|
|
81
|
+
nonce: approval.nonce,
|
|
82
|
+
digest: approval.digest,
|
|
83
|
+
returnTo,
|
|
84
|
+
scope,
|
|
85
|
+
planDocument: approval.planDocument,
|
|
86
|
+
planContract: approval.planDocument === null ? null : approval.planContract,
|
|
87
|
+
revision: approval.revision,
|
|
88
|
+
revisionSourceHref: approval.revision === null ? "" : chatResultHref(focus.id, approval.revision.sourceRun),
|
|
89
|
+
repairChain: approval.repairChain,
|
|
90
|
+
route: focus.route,
|
|
91
|
+
coordinator: approval.coordinator,
|
|
92
|
+
deliverable: approval.deliverable,
|
|
93
|
+
// Edit plan opens the task page's sheet with its fields open for editing.
|
|
94
|
+
editHref: `${taskHref(focus.id)}?edit=plan#plan-editor`,
|
|
95
|
+
notNowHref: "/chat",
|
|
96
|
+
sticky: false,
|
|
97
|
+
earlier: { active: focus.assignment?.earlierActive ?? 0, running: focus.assignment?.earlierRunning ?? 0 },
|
|
98
|
+
edit: null,
|
|
99
|
+
})}</section>`;
|
|
100
|
+
}
|
|
101
|
+
/** The chat's status sentence, worded as the task page words it: what a failed attempt missed, or which step a live
|
|
102
|
+
* build is on (and the step it is stuck on). Undefined keeps the shared status sentence. */
|
|
103
|
+
export function chatStatusSentence(focus) {
|
|
104
|
+
if (focus.state === "failed" && focus.failure != null)
|
|
105
|
+
return [focus.failure.line, focus.failure.evidence].filter(Boolean).join(" ");
|
|
106
|
+
const steps = focus.liveRun === null ? null : buildProgressOf(focus.milestoneProgress);
|
|
107
|
+
return steps === null ? undefined : steps.line;
|
|
108
|
+
}
|
|
109
|
+
/** One live, server-derived journey from request to proof. The fragment is
|
|
110
|
+
* safe to refresh independently, so an in-progress message is never lost. */
|
|
111
|
+
export function taskChatLiveRegion(focus, csrf, fragment = false, inert = false, approvalHref = taskChatHref(focus.id) + "#task-chat-action") {
|
|
112
|
+
const receiptLeads = focus.state === "done" && focus.result !== null;
|
|
113
|
+
const approvalContent = (inert && focus.approval !== null && focus.plan !== "requested"
|
|
114
|
+
? html `<section class="card chat-action-card"><span class="eyebrow">approval ready</span><h2>Finish the current chat response first</h2><p class="meta">The secure approval step appears here as soon as this response lands.</p></section>`
|
|
115
|
+
: fragment && focus.approval !== null && focus.plan !== "requested"
|
|
116
|
+
? html `<section class="card chat-action-card chat-refresh-action"><a class="button-link" href="${approvalHref}" data-primary-action>Approve plan</a></section>`
|
|
117
|
+
: taskChatApproval(focus, csrf));
|
|
118
|
+
const approvalCard = focus.approval !== null && focus.dispatch?.action !== "approve-scope"
|
|
119
|
+
? html `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approvalContent}</details>` : approvalContent;
|
|
120
|
+
const polling = !inert && ((focus.approval === null || focus.plan === "requested") && focus.state !== "done" && focus.state !== "cancelled" || focus.control.kind === "stopping" || focus.control.kind === "stop");
|
|
121
|
+
const status = focus.assignment !== null
|
|
122
|
+
? html `${assignmentSummaryHtml(focus.assignment, { workStatus: focus.status, hideAction: focus.approval !== null && focus.dispatch?.action === "approve-scope", sentence: chatStatusSentence(focus), ...(receiptLeads ? { resultHref: chatResultHref(focus.id, focus.result.runId) } : {}) })}${receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status, focus.assignment, false) : ""}`
|
|
123
|
+
: receiptLeads ? completionReceiptCard(focus.result, focus.id, "chat", focus.status) : taskStatusCard(focus.status, focus.id, focus.dispatch, focus.liveRun?.id ?? null, focus.approval !== null && focus.dispatch?.action === "approve-scope");
|
|
124
|
+
const publication = focus.publication;
|
|
125
|
+
const prUrl = publication === null ? null : safePrUrl(publication.prUrl);
|
|
126
|
+
return html `<section id="task-chat-live" aria-live="polite" data-task="${focus.id}" data-execution="${focus.executionId}" data-source="/chat/task-status?task=${encodeURIComponent(focus.id)}" data-poll="${polling ? "1" : "0"}" data-approval="${focus.approval?.digest ?? ""}" data-plan="${focus.plan ?? ""}">${focus.guide === true && focus.state !== "cancelled" ? firstTaskJourneyHtml(focus) : ""}<div class="task-live-summary">${status}${checkProgressHtml(focus.checkProgress)}${
|
|
127
|
+
// The exact-run control (v52): the SAME component the task page
|
|
128
|
+
// renders, refreshed with the live region — typed input in the
|
|
129
|
+
// composer is untouched because only this region is replaced.
|
|
130
|
+
taskControlDetailsHtml(focus.control, focus.executionId, csrf, "chat", inert)}${
|
|
131
|
+
// The compact agents strip (v47): always visible, phones included,
|
|
132
|
+
// where the desktop context panel is hidden.
|
|
133
|
+
agentsStripHtml(focus.route, focus.executionId)}</div>${milestoneProgressHtml(focus.milestoneProgress)}${planRevisionLedgerHtml(focus.planRevisions, focus.executionId, csrf)}${approvalCard}${focus.history}${focus.decisions.length === 0
|
|
134
|
+
? ""
|
|
135
|
+
: html `<section class="chat-decisions"><div class="chat-section-head"><h2>Needs your answer</h2></div>${focus.decisions.map(one => focus.approval !== null || inert
|
|
136
|
+
? html `<div class="decide-card"><p class="q">${one.question}</p><p class="meta">${oneLineOf(one.recap, 160)}</p><a href="/d/${one.id}?return=${encodeURIComponent(taskChatHref(focus.id))}">Review and answer →</a></div>`
|
|
137
|
+
: decisionAnswerCard(one, csrf, focus.now, false, taskChatHref(focus.id)))}</section>`}${focus.result === null || receiptLeads ? "" : html `<details class="task-previous-result"><summary>Previous result</summary>${completionReceiptCard(focus.result, focus.id, "chat")}</details>`}${publication === null ? "" : html `<p class="chat-publication meta">Published as ${prUrl === null ? html `<span class="mono">PR #${publication.prNumber ?? "?"}</span>` : html `<a href="${prUrl}">PR #${publication.prNumber ?? "?"}</a>`} · ${publication.state}${publication.lastCheckState === null ? "" : html ` · CI ${publication.lastCheckState}`}</p>`}</section>`;
|
|
138
|
+
}
|
|
139
|
+
/** The first task's way to Ready, filled in as it moves (onboarding): the same stage every surface reads. */
|
|
140
|
+
export function firstTaskJourneyHtml(focus) {
|
|
141
|
+
const planning = focus.plan === "requested" && focus.approval === null;
|
|
142
|
+
const read = focus.assignment !== null ? assignmentStageOf(focus.assignment, { token: focus.status.token }, planning) : stageOfCode(focus.status.token, { planning });
|
|
143
|
+
const steps = firstTaskJourney(read, focus.scope === "approved", focus.state === "done");
|
|
144
|
+
const current = steps.find(one => one.state === "current" || one.state === "stuck");
|
|
145
|
+
return html `<ol class="first-task-journey" aria-label="Where this task is${current === undefined ? ": Ready" : `: ${current.label}`}" data-first-task-journey>${steps.map(one => html `<li data-step="${one.key}" data-state="${one.state}"${one.state === "current" || one.state === "stuck" ? html ` aria-current="step"` : ""}><span class="first-task-journey-mark" aria-hidden="true"></span><span>${one.label}</span></li>`)}</ol>`;
|
|
146
|
+
}
|
|
147
|
+
export function taskChatHeading(focus) {
|
|
148
|
+
return html `<div class="chat-head task-chat-head"><div><p class="meta chat-task-back"><a href="${taskHref(focus.id)}">← task overview</a></p><div class="task-chat-title-line"><div><h1>${focus.title}</h1></div>${taskViewSwitch(focus.id, "ask")}</div></div></div>`;
|
|
149
|
+
}
|
|
150
|
+
export function chatWorkspace(content, projects, inert, focus, resultPanel = null) {
|
|
151
|
+
if (focus === null)
|
|
152
|
+
return html `<div class="chat-workspace">${chatProjectRail(projects, inert)}<section class="chat-main">${content}</section></div>`;
|
|
153
|
+
// The result detail (package 3) is the ONE auxiliary panel when open:
|
|
154
|
+
// it takes the context panel's place beside the conversation on a wide
|
|
155
|
+
// screen and becomes the dedicated view, with Back to chat, when the
|
|
156
|
+
// screen has no room for both (CSS decides; the markup is the same).
|
|
157
|
+
if (resultPanel !== null) {
|
|
158
|
+
return html `<div class="chat-workspace task-chat-workspace result-open" data-chat-result-open><section class="chat-main">${content}</section><aside class="chat-result t-panel-slide" data-open="true" aria-label="result">${resultPanel}</aside></div>`;
|
|
159
|
+
}
|
|
160
|
+
return html `<div class="chat-workspace task-chat-workspace">${taskChatContext(focus)}<section class="chat-main">${content}</section></div>`;
|
|
161
|
+
}
|
|
162
|
+
/** The folded overview's one-line summary (UI polish 2026-09-13): the
|
|
163
|
+
* two counts a phone reader scans before deciding to open it. */
|
|
164
|
+
export function chatOverviewSummaryHtml(projects, attention) {
|
|
165
|
+
const total = (key) => projects.reduce((sum, one) => sum + (one.peek?.[key] ?? 0), 0);
|
|
166
|
+
const needsYou = attention?.count ?? total("waiting");
|
|
167
|
+
const running = total("running");
|
|
168
|
+
return html `<summary>Project overview<span class="meta">${needsYou > 0 ? html `<span class="hot">${needsYou}${attention?.saturated ? "+" : ""} need${needsYou === 1 ? "s" : ""} you</span>` : "nothing waiting"} · ${running} building</span></summary>`;
|
|
169
|
+
}
|
|
170
|
+
export function chatFleetOverview(snapshot, projects, interactive) {
|
|
171
|
+
if (snapshot === null) {
|
|
172
|
+
return html `<section class="card chat-overview"><h2>Project summary unavailable</h2><p class="meta">Reload to try again. You can still use chat.</p></section>`;
|
|
173
|
+
}
|
|
174
|
+
const total = (key) => projects.reduce((sum, one) => sum + (one.peek?.[key] ?? 0), 0);
|
|
175
|
+
const needsYou = snapshot.attentionCount?.count ?? total("waiting");
|
|
176
|
+
const running = total("running");
|
|
177
|
+
const queued = total("queued");
|
|
178
|
+
const done = total("doneRecently");
|
|
179
|
+
const projectOf = (index) => projects[index]?.label ?? `r${index + 1}`;
|
|
180
|
+
const rows = [];
|
|
181
|
+
for (const decision of snapshot.decisions.slice(0, 2)) {
|
|
182
|
+
rows.push(html `<a class="chat-overview-item decision" href="/d/${decision.id}"><span class="chat-overview-icon">${strokeIcon(html `<path d="M9.1 9a3 3 0 1 1 5.8 1c0 2-3 2-3 4"/><path d="M12 18h.01"/><circle cx="12" cy="12" r="9"/>`)}</span><span class="chat-overview-copy"><strong>${decision.question}</strong><span>${projectOf(decision.repoIndex)} · ${decision.taskId} · decision #${decision.id}</span></span><span class="chat-overview-arrow" aria-hidden="true">→</span></a>`);
|
|
183
|
+
}
|
|
184
|
+
for (const task of snapshot.tasks
|
|
185
|
+
.filter(one => one.dispatch?.condition === "waiting" && one.state !== "done")
|
|
186
|
+
.slice(0, Math.max(0, 3 - rows.length))) {
|
|
187
|
+
const dispatch = task.dispatch;
|
|
188
|
+
rows.push(html `<a class="chat-overview-item decision" href="${taskHref(task.rootId ?? task.id)}" data-dispatch-status="${dispatch.code}"><span class="chat-overview-icon">${strokeIcon(html `<path d="M12 8v4"/><path d="M12 16h.01"/><circle cx="12" cy="12" r="9"/>`)}</span><span class="chat-overview-copy"><strong>${task.title}</strong><span>${projectOf(task.repoIndex)} · ${task.id} · ${dispatchHeadline(dispatch)}</span></span><span class="chat-overview-arrow" aria-hidden="true">→</span></a>`);
|
|
189
|
+
}
|
|
190
|
+
for (const task of snapshot.tasks.filter(one => one.state === "failed").slice(0, Math.max(0, 3 - rows.length))) {
|
|
191
|
+
rows.push(html `<a class="chat-overview-item failed" href="${taskHref(task.rootId ?? task.id)}"><span class="chat-overview-icon">${strokeIcon(html `<path d="M12 9v4"/><path d="M12 17h.01"/><path d="m10.3 2.9-8.6 15A2 2 0 0 0 3.4 21h17.2a2 2 0 0 0 1.7-3.1l-8.6-15a2 2 0 0 0-3.4 0z"/>`)}</span><span class="chat-overview-copy"><strong>${task.title}</strong><span>${projectOf(task.repoIndex)} · ${task.id} · failed</span></span><span class="chat-overview-arrow" aria-hidden="true">→</span></a>`);
|
|
192
|
+
}
|
|
193
|
+
// Current assignments own attention. The saved assessment is history,
|
|
194
|
+
// never a second queue after the lead marked the result complete.
|
|
195
|
+
for (const task of snapshot.tasks
|
|
196
|
+
.filter(one => one.state === "done" && ["ready-to-check", "needs-decision"].includes(snapshot.assignmentStates?.[one.id]?.state ?? ""))
|
|
197
|
+
.slice(0, Math.max(0, 3 - rows.length))) {
|
|
198
|
+
rows.push(html `<a class="chat-overview-item failed" href="${taskHref(task.rootId ?? task.id)}"><span class="chat-overview-icon">${strokeIcon(html `<path d="M12 9v4"/><path d="M12 17h.01"/><circle cx="12" cy="12" r="9"/>`)}</span><span class="chat-overview-copy"><strong>${task.title}</strong><span>${projectOf(task.repoIndex)} · ${snapshot.assignmentStates[task.id].label} · ${snapshot.assignmentStates[task.id].detail}</span></span><span class="chat-overview-arrow" aria-hidden="true">→</span></a>`);
|
|
199
|
+
}
|
|
200
|
+
for (const task of snapshot.tasks.filter(one => one.state === "running").slice(0, Math.max(0, 4 - rows.length))) {
|
|
201
|
+
rows.push(html `<a class="chat-overview-item running" href="${taskHref(task.rootId ?? task.id)}"><span class="chat-overview-icon">${strokeIcon(html `<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>`)}</span><span class="chat-overview-copy"><strong>${task.title}</strong><span>${projectOf(task.repoIndex)} · ${task.id} · building now</span></span><span class="chat-overview-arrow" aria-hidden="true">→</span></a>`);
|
|
202
|
+
}
|
|
203
|
+
const saturated = snapshot.tasksSaturated || snapshot.decisionsSaturated || snapshot.incidentsSaturated;
|
|
204
|
+
const briefing = "Brief me on what needs my attention, what is building, and the highest-leverage next action across every project.";
|
|
205
|
+
return html `<section class="card chat-overview" aria-label="live portfolio overview" data-card-kind="fleet-overview"><div class="chat-overview-head"><h2>${projects.length} project${projects.length === 1 ? "" : "s"}</h2>${interactive
|
|
206
|
+
? postForm("/chat", html `<button type="submit" name="message" value="${briefing}" class="quiet">Brief me</button>`, { attrs: { class: "inline" } })
|
|
207
|
+
: html `<a href="/board?scope=all" class="chat-overview-link">open board</a>`}</div><div class="chat-overview-stats"><a href="/work?view=needs-you" class="chat-overview-stat attention"><b>${needsYou}${snapshot.attentionCount?.saturated ? "+" : ""}</b><span>need you</span></a><a href="/board?scope=all" class="chat-overview-stat live"><b>${running}</b><span>building</span></a><a href="/board?scope=all&view=order" class="chat-overview-stat"><b>${queued}</b><span>queued</span></a><a href="/work" class="chat-overview-stat"><b>${done}</b><span>finished today</span></a></div>${rows.length === 0 ? html `<p class="chat-overview-clear">${needsYou === 0 && running === 0 ? html `<span class="dot dot-ok"></span>No tasks are waiting and no builds are running.` : html `<a href="/work">Open Tasks to inspect current work.</a>`}</p>` : html `<div class="chat-overview-items">${rows}</div>`}${completedWorkHtml(snapshot, projects.map(project => project.label), snapshot.assignmentStates)}${saturated ? html `<p class="meta chat-overview-note">Some items aren’t shown. Open Work to see more.</p>` : ""}</section>`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* The mate's project rail: one bounded pulse per admitted project, plus
|
|
211
|
+
* two roads that preserve the plane's contracts. "ask" sends the stable
|
|
212
|
+
* rN alias the model already sees; "board" uses the existing POST switch
|
|
213
|
+
* instead of smuggling a project change through a GET parameter.
|
|
214
|
+
*/
|
|
215
|
+
export function chatProjectRail(projects, inert) {
|
|
216
|
+
const statusOf = (peek) => peek === null
|
|
217
|
+
? "unavailable"
|
|
218
|
+
: peek.waiting > 0
|
|
219
|
+
? "needs you"
|
|
220
|
+
: peek.running > 0
|
|
221
|
+
? "building"
|
|
222
|
+
: peek.queued > 0
|
|
223
|
+
? "queued"
|
|
224
|
+
: "quiet";
|
|
225
|
+
const rows = projects.map(one => {
|
|
226
|
+
const peek = one.peek;
|
|
227
|
+
const ask = `Give me a concise status for ${one.id} (${one.label}) and recommend the next reversible action.`;
|
|
228
|
+
return html `<div class="chat-project-card"><div class="chat-project-name"><span class="mono">${one.id}</span><strong>${one.label}</strong><span class="badge">${sentenceCase(statusOf(peek))}</span></div>${peek === null
|
|
229
|
+
? html `<p class="meta">Pulse unavailable</p>`
|
|
230
|
+
: html `<div class="chat-project-stats"><span${peek.waiting > 0 ? html ` class="hot"` : ""}><b>${peek.waiting}</b> need you</span><span><b>${peek.running}</b> live</span><span><b>${peek.queued}</b> queued</span><span><b>${peek.doneRecently}</b> done today</span></div>`}<div class="chat-project-actions">${inert
|
|
231
|
+
? ""
|
|
232
|
+
: postForm("/chat", html `<button type="submit" name="message" value="${ask}" class="quiet" aria-label="ask about ${one.label}">Ask</button>`, { attrs: { class: "inline" } })}${postForm("/projects/open", html `<button type="submit" class="quiet" aria-label="open ${one.label} board">Board</button>`, { attrs: { class: "inline" }, hidden: { path: one.path, return: "/board" } })}</div></div>`;
|
|
233
|
+
});
|
|
234
|
+
return html `<aside class="chat-projects" id="chat-project-panel" aria-label="projects in this conversation"><div class="chat-projects-head"><h2>Projects</h2><span class="badge">${projects.length}</span><button type="button" class="chat-project-close quiet" aria-label="close projects">×</button></div><div class="chat-project-list">${rows}</div></aside>`;
|
|
235
|
+
}
|
|
236
|
+
/** Spend-authorized one-click questions: ordinary /chat posts, not a new door. */
|
|
237
|
+
export function matePromptStarters(focus = null) {
|
|
238
|
+
const prompts = focus === null
|
|
239
|
+
? [
|
|
240
|
+
["brief me", "Brief me on what needs my attention, what is building, and the highest-leverage next action across every project."],
|
|
241
|
+
["decisions", "Walk me through the open decisions, their options, and what you recommend I inspect first."],
|
|
242
|
+
["new task", "Help me define a new task. Ask only about choices that materially change the result; otherwise use your judgment and sensible reversible defaults."],
|
|
243
|
+
]
|
|
244
|
+
: [
|
|
245
|
+
["What’s happening", "Read this task and explain its current status, what is blocking it, and what should happen next."],
|
|
246
|
+
["Review results", "Review the evidence recorded for this task and tell me what is proven and what is still unverified."],
|
|
247
|
+
["Adjust the plan", "Read this task and propose a tighter scope if that would improve the outcome. Explain why before I confirm anything."],
|
|
248
|
+
];
|
|
249
|
+
return html `<div class="chat-prompts" aria-label="suggested questions">${prompts.map(([label, message]) => postForm("/chat", html `<button type="submit" name="message" value="${message}" class="quiet">${label}</button>`, { attrs: { class: "inline" }, hidden: { task: focus?.id } }))}</div>`;
|
|
250
|
+
}
|
|
251
|
+
export function chatHeading(copy, projectCount, showProjectToggle = true) {
|
|
252
|
+
return html `<div class="chat-head"><div><h1>Chat</h1>${copy === "" ? "" : html `<p class="meta">${copy}</p>`}</div><div class="chat-head-actions">${showProjectToggle
|
|
253
|
+
? html `<button type="button" class="chat-project-toggle quiet" aria-controls="chat-project-panel" aria-expanded="false" title="show or hide projects">${strokeIcon(html `<path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/>`)}<span>projects</span><span class="badge">${projectCount}</span></button>`
|
|
254
|
+
: ""}</div></div>`;
|
|
255
|
+
}
|
|
256
|
+
/** A deliberately small rich-text grammar for model copy. The reply is shaped first (the lead's voice, enforced:
|
|
257
|
+
* no headers, at most three bold anchors, labelled links, no internal ids unless `asked` wanted them); input is
|
|
258
|
+
* escaped before tags are introduced: bullets, numbered steps, bold, inline code and labelled http(s) links are
|
|
259
|
+
* presentation only—never executable HTML. */
|
|
260
|
+
export function renderChatText(raw, asked) {
|
|
261
|
+
const inline = replyHtmlInline;
|
|
262
|
+
const text = shapeReply(raw, { appOrigin: requestContext.getStore()?.appOrigins ?? null, ...(asked === undefined ? {} : { asked }) });
|
|
263
|
+
const lines = text.replace(/\r\n?/g, "\n").split("\n");
|
|
264
|
+
const out = [];
|
|
265
|
+
let paragraph = [];
|
|
266
|
+
let list = null;
|
|
267
|
+
const flushParagraph = () => {
|
|
268
|
+
if (paragraph.length === 0)
|
|
269
|
+
return;
|
|
270
|
+
out.push(html `<p>${joinHtml(paragraph.map(line => inline(line)), html `<br>`)}</p>`);
|
|
271
|
+
paragraph = [];
|
|
272
|
+
};
|
|
273
|
+
const closeList = () => {
|
|
274
|
+
if (list === null)
|
|
275
|
+
return;
|
|
276
|
+
out.push(list.tag === "ul" ? html `<ul>${list.items}</ul>` : html `<ol>${list.items}</ol>`);
|
|
277
|
+
list = null;
|
|
278
|
+
};
|
|
279
|
+
for (const line of lines) {
|
|
280
|
+
const bullet = /^\s*[-*]\s+(.+)$/.exec(line);
|
|
281
|
+
const numbered = /^\s*(\d{1,9})[.)]\s+(.+)$/.exec(line);
|
|
282
|
+
if (bullet !== null || numbered !== null) {
|
|
283
|
+
flushParagraph();
|
|
284
|
+
const wanted = bullet !== null ? "ul" : "ol";
|
|
285
|
+
if (list?.tag !== wanted) {
|
|
286
|
+
closeList();
|
|
287
|
+
list = { tag: wanted, items: [] };
|
|
288
|
+
}
|
|
289
|
+
// Model replies often separate steps with blank lines. Each remains
|
|
290
|
+
// its stated number even when that blank line starts another <ol>.
|
|
291
|
+
list.items.push(html `<li${numbered === null ? "" : html ` value="${Number(numbered[1])}"`}>${inline(bullet?.[1] ?? numbered?.[2] ?? "")}</li>`);
|
|
292
|
+
}
|
|
293
|
+
else if (line.trim() === "") {
|
|
294
|
+
flushParagraph();
|
|
295
|
+
closeList();
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
closeList();
|
|
299
|
+
paragraph.push(line);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
flushParagraph();
|
|
303
|
+
closeList();
|
|
304
|
+
return html `<div class="chat-copy">${out}</div>`;
|
|
305
|
+
}
|
|
306
|
+
export function chatActivity(activity) {
|
|
307
|
+
return chatActivityDetailsHtml(activity);
|
|
308
|
+
}
|
|
309
|
+
export const CHAT_UI_SCRIPT = `(function(){var workspace=document.querySelector(".chat-workspace"),projectPanel=document.getElementById("chat-project-panel"),projectToggle=document.querySelector(".chat-project-toggle"),projectClose=document.querySelector(".chat-project-close");` +
|
|
310
|
+
`var fleet=document.querySelector(".chat-fleet-context");if(fleet&&!document.querySelector(".chat-empty")){var desktop=window.matchMedia("(min-width: 761px)");fleet.open=desktop.matches;desktop.addEventListener("change",function(){fleet.open=desktop.matches;});}` +
|
|
311
|
+
`if(workspace&&projectPanel&&projectToggle){var wide=window.matchMedia("(min-width: 1200px)");var saved="";try{saved=localStorage.getItem("standing-orders:chat-projects")||"";}catch(e){}` +
|
|
312
|
+
`var background=[];function restoreBackground(){background.forEach(function(n){n.inert=false;});background=[];}` +
|
|
313
|
+
`function apply(open){var was=workspace.classList.contains("projects-open");restoreBackground();workspace.classList.toggle("projects-open",open);workspace.classList.toggle("projects-hidden",!open);projectToggle.setAttribute("aria-expanded",String(open));` +
|
|
314
|
+
`if(open&&!wide.matches){projectPanel.setAttribute("role","dialog");projectPanel.setAttribute("aria-modal","true");projectPanel.setAttribute("aria-label","Projects");` +
|
|
315
|
+
`document.querySelectorAll(".side,.mobile-top,.tabbar,.chat-main,.task-chat-context,.task-chat-agents").forEach(function(n){if(!n.contains(projectPanel)&&!n.inert){n.inert=true;background.push(n);}});if(projectClose)projectClose.focus();}` +
|
|
316
|
+
`else{projectPanel.removeAttribute("role");projectPanel.removeAttribute("aria-modal");projectPanel.removeAttribute("aria-label");if(was&&!open)projectToggle.focus();}}` +
|
|
317
|
+
`projectPanel.addEventListener("keydown",function(ev){if(ev.key!=="Tab"||wide.matches)return;var nodes=Array.from(projectPanel.querySelectorAll("a[href],button,input,select,textarea,summary,[tabindex='0']")).filter(function(n){return !n.disabled&&n.getClientRects().length;});var first=nodes[0],last=nodes[nodes.length-1];if(ev.shiftKey&&document.activeElement===first){ev.preventDefault();last.focus();}else if(!ev.shiftKey&&document.activeElement===last){ev.preventDefault();first.focus();}});` +
|
|
318
|
+
`function preferred(){return wide.matches&&saved==="open";}apply(preferred());` +
|
|
319
|
+
`projectToggle.addEventListener("click",function(){var next=!workspace.classList.contains("projects-open");apply(next);if(wide.matches){saved=next?"open":"closed";try{localStorage.setItem("standing-orders:chat-projects",saved);}catch(e){}}});` +
|
|
320
|
+
`if(projectClose)projectClose.addEventListener("click",function(){apply(false);projectToggle.focus();});` +
|
|
321
|
+
`document.addEventListener("click",function(ev){if(wide.matches||!workspace.classList.contains("projects-open"))return;var target=ev.target;if(target instanceof Node&&!projectPanel.contains(target)&&!projectToggle.contains(target))apply(false);});` +
|
|
322
|
+
`wide.addEventListener("change",function(){apply(preferred());});document.addEventListener("keydown",function(ev){if(ev.key==="Escape"&&workspace.classList.contains("projects-open")){apply(false);projectToggle.focus();}});}` +
|
|
323
|
+
`var taskLive=document.querySelector(".composer[data-chat-session]")?null:document.getElementById("task-chat-live");` +
|
|
324
|
+
`function taskEditing(){return taskLive.contains(document.activeElement)||taskLive.querySelector("details[open]")||Array.from(taskLive.querySelectorAll("input:not([type=hidden]),textarea,select")).some(function(el){return el.tagName==="SELECT"?Array.from(el.options).some(function(o){return o.selected!==o.defaultSelected;}):el.type==="checkbox"||el.type==="radio"?el.checked!==el.defaultChecked:el.value!==el.defaultValue;});}` +
|
|
325
|
+
`function refreshTask(){if(!taskLive||taskLive.getAttribute("data-poll")!=="1")return;` +
|
|
326
|
+
`if(document.hidden||taskEditing()){setTimeout(refreshTask,5000);return;}var source=taskLive.getAttribute("data-source");if(!source)return;` +
|
|
327
|
+
`fetch(source,{cache:"no-store",signal:AbortSignal.timeout(10000)}).then(function(r){if(r.status===401||r.status===403||r.redirected){location.href="/login";return null;}if(!r.ok)throw new Error("connection");return r.text();})` +
|
|
328
|
+
`.then(function(html){if(html===null||!taskLive)return;var parsed=new DOMParser().parseFromString(html,"text/html"),next=parsed.getElementById("task-chat-live");if(!next)throw new Error("response");if(taskEditing()){setTimeout(refreshTask,5000);return;}taskLive.replaceWith(next);taskLive=next;` +
|
|
329
|
+
`if(taskLive.getAttribute("data-poll")==="1")setTimeout(refreshTask,5000);}).catch(function(){setTimeout(refreshTask,10000);});}` +
|
|
330
|
+
`if(taskLive&&taskLive.getAttribute("data-poll")==="1")setTimeout(refreshTask,5000);` +
|
|
331
|
+
`var box=document.querySelector(".composer textarea");if(box){` +
|
|
332
|
+
`function size(){box.style.height="auto";box.style.height=Math.min(box.scrollHeight,208)+"px";}size();box.addEventListener("input",size);` +
|
|
333
|
+
`box.addEventListener("keydown",function(ev){if(ev.isComposing||ev.key!=="Enter"||ev.shiftKey||!window.matchMedia("(min-width: 761px)").matches)return;ev.preventDefault();if(box.value.trim()!=="")box.form.requestSubmit();});}})();`;
|
|
334
|
+
/** Provider, model, and limits under ONE disclosure (UI polish
|
|
335
|
+
* 2026-09-13): the facts stay one tap away on every chat surface, and a
|
|
336
|
+
* membership never shows a dollar figure as if it were a charge. */
|
|
337
|
+
export function chatLimitsHtml(facts) {
|
|
338
|
+
const rows = [
|
|
339
|
+
html `<span class="mono">${facts.provider} · ${facts.model}</span>`,
|
|
340
|
+
html `<span>${facts.turnsToday} / ${facts.dailyTurns} turns today</span>`,
|
|
341
|
+
];
|
|
342
|
+
if (facts.subscription)
|
|
343
|
+
rows.push(html `<span>membership login · no dollar ceiling</span>`);
|
|
344
|
+
else if (facts.weekly !== null)
|
|
345
|
+
rows.push(html `<span>this week ${chatMoney(facts.weekly.spent)} of ${chatMoney(facts.weekly.ceiling)}</span>`);
|
|
346
|
+
return html `<details class="chat-limits"><summary>Model & limits<span class="meta">${facts.subscription ? "membership" : facts.provider}</span></summary><div class="chat-budget">${rows}</div></details>`;
|
|
347
|
+
}
|
|
348
|
+
export function chatPage(chrome, data) {
|
|
349
|
+
const formFacts = { keyFacts: data.keyFacts, openrouterModels: data.openrouterModels, ...(data.liveModels === undefined ? {} : { liveModels: data.liveModels }), csrf: data.csrf,
|
|
350
|
+
returnTo: data.focusTask === null ? "/chat" : taskChatHref(data.focusTask.id) };
|
|
351
|
+
const parts = [
|
|
352
|
+
data.focusTask === null
|
|
353
|
+
? chatHeading("", data.projects.length, data.enabled.ok)
|
|
354
|
+
: taskChatHeading(data.focusTask),
|
|
355
|
+
data.focusTask === null ? (data.catchUp ?? "") : taskChatLiveRegion(data.focusTask, data.csrf, false, data.pending !== null),
|
|
356
|
+
];
|
|
357
|
+
if (data.problem !== null)
|
|
358
|
+
parts.push(html `<div class="problem">${data.problem}</div>`);
|
|
359
|
+
if (!data.enabled.ok) {
|
|
360
|
+
const code = data.enabled.code;
|
|
361
|
+
// The sandbox shows no conversation at all: chat evidence is a real
|
|
362
|
+
// subscription-backed plane, never a seeded transcript (v48 authority repair).
|
|
363
|
+
// With no lead yet, the first run says what happens next (it turns on with the signed-in agent, or shows the one
|
|
364
|
+
// command to run); otherwise one line, and its settings live in Settings → Lead (onboarding).
|
|
365
|
+
const settingsLink = data.canManage ? html ` <a href="/settings/lead">Settings → Lead</a>` : "";
|
|
366
|
+
if (!(code === "unconfigured" && data.firstRunShown === true))
|
|
367
|
+
parts.push(code === "demo"
|
|
368
|
+
? html `<div class="card" id="latest"><p><strong>Chat isn’t available in demo mode</strong></p><p class="meta">Demo data never contacts an external model. Start Toolroll with a real project to use chat: <code>${START_COMMAND}</code> in your repository.</p></div>`
|
|
369
|
+
: code === "unconfigured"
|
|
370
|
+
? html `<div class="card" id="latest" data-lead-off><p><strong>The lead is off.</strong></p><p class="meta">${data.canManage ? html `Turn it on in${settingsLink}.` : "An approver can turn it on."}</p></div>`
|
|
371
|
+
: code === "unpriced" || code === "no-key"
|
|
372
|
+
? html `<div class="card" id="latest"><p><strong>The lead can’t run yet.</strong></p><p class="meta">${data.enabled.why}${data.canManage ? html ` ·${settingsLink}` : ""}</p></div>`
|
|
373
|
+
: html `<div class="card" id="latest"><p><strong>Chat is off.</strong></p><p class="meta">${data.enabled.why}</p></div>`);
|
|
374
|
+
return screen("chat", chatWorkspace(joinHtml(parts, "\n"), data.projects, true, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: data.focusTask !== null } });
|
|
375
|
+
}
|
|
376
|
+
const config = data.enabled.config;
|
|
377
|
+
const subscription = isSubscriptionChatProvider(config.provider);
|
|
378
|
+
// The saved catch-up owns orientation. Keep the older overview only as
|
|
379
|
+
// a fallback; provider and spend terms remain available before consent.
|
|
380
|
+
if (!data.canManage) {
|
|
381
|
+
parts.push(html `<div class="card chat-readonly"><strong>Read-only view</strong><p class="meta">An approver can start the unified conversation and confirm its proposed actions. You can still open every live card and project board here.</p></div>`);
|
|
382
|
+
}
|
|
383
|
+
if (data.canManage && data.mateMint !== undefined)
|
|
384
|
+
parts.push(data.mateMint);
|
|
385
|
+
if (data.canManage && data.coordinatorProposals !== undefined)
|
|
386
|
+
parts.push(data.coordinatorProposals);
|
|
387
|
+
parts.push(data.focusTask === null && !data.catchUp ? html `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, false)}</details>` : "", chatLimitsHtml({ provider: config.provider, model: config.model, turnsToday: data.turnsToday, dailyTurns: config.dailyTurns, subscription, weekly: subscription ? null : { spent: data.weeklySpent, ceiling: config.weeklyCeilingMicrousd } }));
|
|
388
|
+
for (const turn of data.latched) {
|
|
389
|
+
parts.push(html `<div class="problem"><strong>Chat is paused.</strong> An earlier reply stopped before its cost was known; it may have cost up to ${chatMoney(turn.reservedMicrousd)}. <a href="/chat/ack/${turn.id}">Confirm that cost</a> to turn chat back on.</div>`);
|
|
390
|
+
}
|
|
391
|
+
if (data.pending !== null) {
|
|
392
|
+
parts.push(chatWorkingHtml({ details: `Turn #${data.pending.id}${subscription ? " · subscription" : ` · up to ${chatMoney(data.pending.reservedMicrousd)} reserved`}` }));
|
|
393
|
+
parts.push(html `<p class="meta"><a href="/chat">refresh now</a></p>`);
|
|
394
|
+
return screen("chat", chatWorkspace(joinHtml(parts, "\n"), data.projects, true, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: data.focusTask !== null }, refreshSeconds: 3 });
|
|
395
|
+
}
|
|
396
|
+
const last = data.chat?.lastTurn ?? null;
|
|
397
|
+
if (last !== null) {
|
|
398
|
+
if (last.staticError !== null) {
|
|
399
|
+
parts.push(html `<div class="card" id="latest"><p class="meta">${last.staticError}</p></div>`);
|
|
400
|
+
}
|
|
401
|
+
else if (last.reply !== null) {
|
|
402
|
+
parts.push(html `<div class="card" id="latest">${renderChatText(last.reply)}${last.proposalsDiscarded ? html `<p class="meta">A draft block in this answer was malformed and was discarded whole</p>` : ""}</div>`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const candidates = data.chat === null ? [] : [...data.chat.candidates.values()];
|
|
406
|
+
for (const one of candidates) {
|
|
407
|
+
const draft = one.draft;
|
|
408
|
+
parts.push(html `<div class="card"><p><strong>${draft.title}</strong> <span class="badge">Draft ${draft.kind}</span></p><p class="meta">Drafted by the model from fleet context — nothing is filed; drafts do not survive a restart</p><p style="white-space:pre-wrap">${draft.goal}</p>${draft.outOfScope === null ? "" : html `<p class="meta">Not: ${draft.outOfScope}</p>`}${draft.touches.length > 0 ? html `<p class="meta">Touches: ${draft.touches.join(", ")}</p>` : ""}<p class="meta">Repo: <span class="mono">${projectName(one.repoPath)}</span></p>${postForm(`/chat/file/${one.key}`, html `<input type="password" name="token" placeholder="your password" autocomplete="current-password"><button type="submit">File unapproved</button>`, { attrs: { class: "inline" } })}</div>`);
|
|
409
|
+
}
|
|
410
|
+
if (!subscription && data.canManage && data.focusTask === null) {
|
|
411
|
+
parts.push(html `<h2>Ask</h2>`, postForm("/chat", joinHtml([
|
|
412
|
+
html `<label>Message<textarea name="message" rows="3" maxlength="2000"></textarea></label>`,
|
|
413
|
+
html `<label>Your password <span class="meta">(every message — chat spends)</span><input type="password" name="token" autocomplete="current-password"></label>`,
|
|
414
|
+
html `<button type="submit">Ask</button>`,
|
|
415
|
+
], "\n"), { attrs: { class: "card" } }));
|
|
416
|
+
}
|
|
417
|
+
if (data.canManage) {
|
|
418
|
+
parts.push(html `<p class="meta" id="chat-settings"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`);
|
|
419
|
+
}
|
|
420
|
+
if (data.recent.length > 0) {
|
|
421
|
+
parts.push(html `<details class="chat-turn-history"><summary>Conversation activity <span class="meta">${data.recent.length} recent turns</span></summary>\n${joinHtml(data.recent.map(turn => html `<p class="row"><span class="mono">#${turn.id}</span> ${turn.state}${turn.failureReason === null ? "" : html ` · ${turn.failureReason}`} <span class="right meta">${turn.tokensIn ?? "–"} in / ${turn.tokensOut ?? "–"} out · ${subscription ? "membership" : `${chatMoney(turn.settledMicrousd ?? turn.reservedMicrousd)}${turn.settledMicrousd === null ? " reserved" : ""}`}</span></p>`), "\n")}\n</details>`);
|
|
422
|
+
}
|
|
423
|
+
return screen("chat", chatWorkspace(joinHtml(parts, "\n"), data.projects, true, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: data.focusTask !== null } });
|
|
424
|
+
}
|
|
425
|
+
export function chatAckPage(chrome, turn, nonce) {
|
|
426
|
+
return screen("chat", joinHtml([
|
|
427
|
+
html `<h1>Unknown spend</h1>`,
|
|
428
|
+
html `<div class="card">`,
|
|
429
|
+
html `<p>Turn <span class="mono">#${turn.id}</span> on <span class="mono">${turn.provider} · ${turn.model}</span> may have started before it failed (${turn.failureReason ?? "crashed"}), and its cost could not be measured.</p>`,
|
|
430
|
+
html `<p><strong>Acknowledging charges the reserved worst case, ${chatMoney(turn.reservedMicrousd)}, to the ledger and re-enables chat on this credential.</strong></p>`,
|
|
431
|
+
html `<p class="meta">Check the provider's own usage dashboard if you want the exact figure first; the ledger keeps whichever is known.</p>`,
|
|
432
|
+
postForm(`/chat/ack/${turn.id}`, joinHtml([
|
|
433
|
+
html `<label>Your password<input type="password" name="token" autocomplete="current-password"></label>`,
|
|
434
|
+
html `<button type="submit">Accept the charge — re-enable chat</button>`,
|
|
435
|
+
], "\n"), { hidden: { nonce } }),
|
|
436
|
+
html `</div>`,
|
|
437
|
+
], "\n"), { chrome });
|
|
438
|
+
}
|
|
439
|
+
/** The card that starts a conversation: the one password ceremony (mate arc §1). */
|
|
440
|
+
export function mateMintCard(enabled, returnTo = "/chat") {
|
|
441
|
+
const subscription = enabled.billing === "subscription";
|
|
442
|
+
return html `<div class="card mate-mint" id="latest">\n<p><strong>Start a conversation</strong></p>\n${postForm("/chat/mate/mint", joinHtml([
|
|
443
|
+
html `<div class="mate-terms">`,
|
|
444
|
+
subscription
|
|
445
|
+
? html `<span class="meta">Uses your ${enabled.config.provider === "codex-subscription" ? "Codex" : "Anthropic"} membership · no dollar limit · daily turn limits apply</span>`
|
|
446
|
+
: html `<label>Spend up to <span class="inline-field">$<input type="text" name="ceiling-usd" inputmode="decimal" value="5" style="width:5rem"></span> <span class="meta">(weekly chat ceiling ${chatMoney(enabled.config.weeklyCeilingMicrousd)} still applies)</span></label>`,
|
|
447
|
+
html `</div>`,
|
|
448
|
+
html `<label class="arm"><input type="checkbox" name="follow" value="yes"> Let the lead follow crew updates</label>`,
|
|
449
|
+
html `<button type="submit">Start chat</button>`,
|
|
450
|
+
], "\n"), { returnTo })}\n</div>`;
|
|
451
|
+
}
|
|
452
|
+
/** The decisions the cards on a page name, for the answer card's consequences and the builder's recommendation. */
|
|
453
|
+
export function decisionsFor(store, proposals) {
|
|
454
|
+
const out = new Map();
|
|
455
|
+
for (const one of proposals) {
|
|
456
|
+
if (one.kind !== "answer" || typeof one.payload["decision"] !== "number")
|
|
457
|
+
continue;
|
|
458
|
+
const decision = store.getDecision(one.payload["decision"]);
|
|
459
|
+
if (decision !== null)
|
|
460
|
+
out.set(decision.id, decision);
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* A proposal card: what, then confirm/dismiss, or the door's answer. An
|
|
466
|
+
* `answer` card shows the question, every option WITH its consequence,
|
|
467
|
+
* the builder's recommendation beside the proposer's pick, and — for an
|
|
468
|
+
* irreversible option — the explicit confirmation field the decision
|
|
469
|
+
* page itself uses (ruling 12).
|
|
470
|
+
*/
|
|
471
|
+
export function proposalCard(view, inert, decision, returnTo = null) {
|
|
472
|
+
return proposalCardParts(view, inert, decision, returnTo).html;
|
|
473
|
+
}
|
|
474
|
+
/** The card's HTML and, for the chat's confirm-in-place cards, the same
|
|
475
|
+
* card as data: the body is the HTML's own, the act is the same door. */
|
|
476
|
+
export function proposalCardParts(view, inert, decision, returnTo = null) {
|
|
477
|
+
const payload = view.payload;
|
|
478
|
+
const text = (key) => (typeof payload[key] === "string" ? payload[key] : "");
|
|
479
|
+
const task = text("task");
|
|
480
|
+
const repoId = text("repoId");
|
|
481
|
+
const presentations = {
|
|
482
|
+
task: { label: payload["report"] === true ? "Scout investigation" : "New task", action: "file task", icon: html `<path d="M12 5v14"/><path d="M5 12h14"/>` },
|
|
483
|
+
next: { label: "Queue priority", action: "move to front", icon: html `<path d="M12 19V5"/><path d="m5 12 7-7 7 7"/>` },
|
|
484
|
+
reserve: { label: "Worker assignment", action: payload["worker"] === null ? "release" : "reserve", icon: html `<circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/>` },
|
|
485
|
+
hold: { label: "Pause work", action: "hold", icon: html `<rect x="6" y="4" width="4" height="16" rx="1"/><rect x="14" y="4" width="4" height="16" rx="1"/>` },
|
|
486
|
+
unhold: { label: "Resume work", action: "release hold", icon: html `<path d="m7 4 13 8-13 8z"/>` },
|
|
487
|
+
steer: { label: "Guidance for next attempt", action: "add guidance", icon: html `<path d="M5 12h14"/><path d="m13 6 6 6-6 6"/>` },
|
|
488
|
+
scope: { label: "Scope revision", action: "save scope", icon: html `<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4z"/>` },
|
|
489
|
+
answer: { label: "Decision answer", action: "confirm answer", icon: html `<path d="M9.1 9a3 3 0 1 1 5.8 1c0 2-3 2-3 4"/><path d="M12 18h.01"/><circle cx="12" cy="12" r="9"/>` },
|
|
490
|
+
cancel: { label: "Cancel task", action: "open task", icon: html `<path d="m15 9-6 6"/><path d="m9 9 6 6"/><circle cx="12" cy="12" r="9"/>` },
|
|
491
|
+
repair: {
|
|
492
|
+
label: "Task is waiting",
|
|
493
|
+
action: text("operation") === "retry" ? "try again" : text("operation") === "replace" ? "wait for another task" : "continue without it",
|
|
494
|
+
icon: html `<path d="M14.7 6.3a4 4 0 0 0-5 5L4 17l3 3 5.7-5.7a4 4 0 0 0 5-5l-2.4 2.4-3-3z"/>`,
|
|
495
|
+
},
|
|
496
|
+
agents: { label: "Agents change", action: "change agents", icon: html `<circle cx="12" cy="12" r="3"/><path d="M12 2v3"/><path d="M12 19v3"/><path d="m4.9 4.9 2.2 2.2"/><path d="m16.9 16.9 2.2 2.2"/><path d="M2 12h3"/><path d="M19 12h3"/><path d="m4.9 19.1 2.2-2.2"/><path d="m16.9 7.1 2.2-2.2"/>` },
|
|
497
|
+
review: { label: text("operation") === "revise" ? "Changes to make" : "Note for later", action: text("operation") === "revise" ? "Request changes" : "Save for later", icon: html `<path d="M4 5h16v12H8l-4 3z"/>` },
|
|
498
|
+
control: { label: "Open control", action: "Open", icon: html `<path d="M5 12h14m-6-6 6 6-6 6"/>` },
|
|
499
|
+
action: { label: text("title") || "Review action", action: sharedActionPayload(payload) ? CHAT_ACTIONS[sharedActionPayload(payload).operation].label : "Unavailable", icon: html `<path d="M5 12h14m-6-6 6 6-6 6"/>` },
|
|
500
|
+
task_action: { label: payload["operation"] === "stop" ? "Stop task?" : payload["operation"] === "resume" ? "Resume task?" : "Task update", action: isChatTaskAction(payload["operation"]) ? CHAT_TASK_ACTIONS[payload["operation"]].label : "Unavailable", icon: html `<path d="M5 12h14m-6-6 6 6-6 6"/>` },
|
|
501
|
+
};
|
|
502
|
+
const presentation = presentations[view.kind];
|
|
503
|
+
const facts = (...rows) => {
|
|
504
|
+
const visible = rows.filter(([, value]) => value !== "" && !(isHtml(value) && htmlString(value) === ""));
|
|
505
|
+
return visible.length === 0 ? html `` : html `<dl class="proposal-facts">${visible.map(([key, value]) => html `<div><dt>${key}</dt><dd>${value}</dd></div>`)}</dl>`;
|
|
506
|
+
};
|
|
507
|
+
const mono = (value) => html `<span class="mono">${value}</span>`;
|
|
508
|
+
const monoLines = (value) => Array.isArray(value) ? joinHtml(value.map(mono), html `<br>`) : "";
|
|
509
|
+
let what;
|
|
510
|
+
if (view.kind === "task") {
|
|
511
|
+
const planning = text("planning") || "auto";
|
|
512
|
+
const planningWords = payload["report"] === true
|
|
513
|
+
? "not needed — this is an investigation"
|
|
514
|
+
: planning === "required"
|
|
515
|
+
? "inspect the project and draft a plan first"
|
|
516
|
+
: planning === "skip"
|
|
517
|
+
? "start from this proposed scope"
|
|
518
|
+
: "inspect first when the work needs repository context";
|
|
519
|
+
what = html `<h3>${text("title")}</h3><p class="proposal-summary">${text("goal")}</p>${facts(["project", mono(repoId)], ["deliverable", payload["report"] === true ? "report only" : "branch"], ["planning", planningWords], ["checks", isCheckLevel(payload["checks"]) ? { quick: "Quick checks", full: "Full checks", off: "Off — built, not checked" }[payload["checks"]] : ""], ["out of scope", text("not")], ["may touch", monoLines(payload["touches"])])}`;
|
|
520
|
+
}
|
|
521
|
+
else if (view.kind === "action") {
|
|
522
|
+
const action = sharedActionPayload(payload);
|
|
523
|
+
what = action === null ? html `<p>This action is unavailable.</p>` : (sharedActionNeedsReview(action) ? html `` : html `${action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`)}`);
|
|
524
|
+
}
|
|
525
|
+
else if (view.kind === "task_action") {
|
|
526
|
+
const operation = payload["operation"];
|
|
527
|
+
what = html `<h3>${text("taskTitle") || task}</h3>${operation === "stop" || operation === "resume" ? html `<p class="meta">${task} · Run #${String(payload["run"] ?? "?")}</p>` : ""}<p>${isChatTaskAction(operation) ? CHAT_TASK_ACTIONS[operation].detail : "Action unavailable."}</p>${text("dependency") === "" ? "" : html `<p>${text("dependencyTitle")}</p>`}`;
|
|
528
|
+
}
|
|
529
|
+
else if (view.kind === "control") {
|
|
530
|
+
const control = payload["control"];
|
|
531
|
+
const label = isChatControl(control) ? CHAT_CONTROLS[control].label : "Control unavailable";
|
|
532
|
+
// Navigation does not wait for approval. Give it a destination and one
|
|
533
|
+
// link, not a proposal header that falsely reads as unfinished work.
|
|
534
|
+
if (isChatControl(control) && view.state === "pending" && !inert) {
|
|
535
|
+
const title = text("taskTitle");
|
|
536
|
+
const href = chatControlHref(control, task, payload["run"], payload["project"]);
|
|
537
|
+
return {
|
|
538
|
+
html: html `<article class="card proposal proposal-control" data-card-kind="control">${title === "" ? "" : html `<div class="proposal-body"><h3>${title}</h3></div>`}<footer class="proposal-actions"><a class="button-link" href="${href}" aria-label="${title === "" ? label : `${label}: ${title}`}">${label}</a></footer></article>`,
|
|
539
|
+
card: { id: view.id, kind: view.kind, label: title === "" ? label : title, state: "pending", body: "", said: null, links: [], primary: { kind: "link", label, href }, dismissable: false, note: null },
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
what = html `<h3>${label}</h3>${text("taskTitle") === "" ? "" : html `<p>${text("taskTitle")}</p>`}`;
|
|
543
|
+
}
|
|
544
|
+
else if (view.kind === "review") {
|
|
545
|
+
const snapshot = payload["snapshot"];
|
|
546
|
+
const ids = Array.isArray(payload["notes"]) ? payload["notes"] : [];
|
|
547
|
+
const selected = snapshot?.notes.filter(one => ids.includes(one.id)) ?? [];
|
|
548
|
+
const notes = [...selected, ...(text("note") === "" ? [] : [{ note: text("note"), path: text("path") || null, line: typeof payload["line"] === "number" ? payload["line"] : null }])];
|
|
549
|
+
what = html `<h3>${text("taskTitle") || task}</h3><ul class="proposal-review-notes">${notes.map(one => html `<li>${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : html `:${one.line}`}</span> · `}${one.note}</li>`)}</ul>${text("operation") === "revise" ? html `<p class="meta">Updates this task. Your approval settings apply.</p>` : html `<p class="meta">No work starts.</p>`}`;
|
|
550
|
+
}
|
|
551
|
+
else if (view.kind === "next") {
|
|
552
|
+
what = html `<h3>Move <a href="${taskHref(task)}">${task}</a> to the front</h3>${facts(["current position", html `${String(payload["position"] ?? "?")} of ${String(payload["of"] ?? "?")}`], ["project", mono(repoId)])}`;
|
|
553
|
+
}
|
|
554
|
+
else if (view.kind === "reserve") {
|
|
555
|
+
what = html `<h3>${payload["worker"] === null ? "Release" : "Reserve"} <a href="${taskHref(task)}">${task}</a></h3>${facts(["destination", payload["worker"] === null ? "shared queue" : text("worker")], ["project", mono(repoId)])}`;
|
|
556
|
+
}
|
|
557
|
+
else if (view.kind === "hold") {
|
|
558
|
+
what = html `<h3>Hold <a href="${taskHref(task)}">${task}</a></h3><p class="proposal-summary">${text("reason")}</p>${facts(["project", mono(repoId)])}`;
|
|
559
|
+
}
|
|
560
|
+
else if (view.kind === "unhold") {
|
|
561
|
+
what = html `<h3>Release <a href="${taskHref(task)}">${task}</a> from its hold</h3>${facts(["project", mono(repoId)])}`;
|
|
562
|
+
}
|
|
563
|
+
else if (view.kind === "steer") {
|
|
564
|
+
const taskTitle = text("taskTitle") || task;
|
|
565
|
+
what = html `<h3>Guide <a href="${taskHref(task)}">${taskTitle}</a>'s next attempt</h3><p class="proposal-summary">${text("note")}</p>${facts(["when", "next attempt"], ["project", mono(repoId)])}<p class="meta proposal-disclosure">This guides the next attempt without changing the task’s scope. It does not interrupt work already running.</p>`;
|
|
566
|
+
}
|
|
567
|
+
else if (view.kind === "agents") {
|
|
568
|
+
// The agents card (v48): exactly what changes, in the same words the
|
|
569
|
+
// task page uses — the role, the exact agent, the size — and the
|
|
570
|
+
// consequence for the standing approval.
|
|
571
|
+
const taskTitle = text("taskTitle") || task;
|
|
572
|
+
const role = text("role");
|
|
573
|
+
const clear = payload["clear"] === true;
|
|
574
|
+
const agentWords = text("provider") === "" ? "" : `${text("provider")} · ${text("model")}`;
|
|
575
|
+
const size = text("size");
|
|
576
|
+
const sizeLine = size === "" ? "" : `${size}${payload["risky"] === true ? ", risky" : ""} change`;
|
|
577
|
+
const taskLink = html `<a href="${taskHref(task)}">${taskTitle}</a>`;
|
|
578
|
+
const heading = role !== "" && !clear
|
|
579
|
+
? html `Run ${taskLink}'s ${role} on ${mono(agentWords)}`
|
|
580
|
+
: role !== "" && clear
|
|
581
|
+
? html `Let the recommended ${role} stand for ${taskLink}`
|
|
582
|
+
: size !== ""
|
|
583
|
+
? html `Treat ${taskLink} as a ${sizeLine}`
|
|
584
|
+
: html `Change the agents for ${taskLink}`;
|
|
585
|
+
what = html `<h3>${heading}</h3>${text("why") === "" ? "" : html `<p class="proposal-summary">${text("why")}</p>`}${facts(["agents now", text("before")], ["size", size === "" ? "" : sizeConsequence(size, payload["risky"] === true)], ["role", role === "" ? "" : html `${role} → ${clear ? "the recommendation" : mono(agentWords)}`], ["project", mono(repoId)])}<p class="meta proposal-disclosure">Recorded under your name when you confirm. ${text("approval") === "approved" ? "The current approval no longer covers the task afterwards — approve it again on the task." : "The next approval seals these agents."}</p>`;
|
|
586
|
+
}
|
|
587
|
+
else if (view.kind === "scope") {
|
|
588
|
+
what = html `<h3>Rewrite <a href="${taskHref(task)}">${task}</a></h3><p class="proposal-summary">${text("goal")}</p>${
|
|
589
|
+
// One path per line (UI polish 2026-09-13): a comma run broke mid-token on a phone.
|
|
590
|
+
facts(["out of scope", text("not")], ["may touch", monoLines(payload["touches"])], ["project", mono(repoId)])}`;
|
|
591
|
+
}
|
|
592
|
+
else if (view.kind === "repair") {
|
|
593
|
+
const blocker = text("blocker");
|
|
594
|
+
const operation = text("operation");
|
|
595
|
+
const replacement = text("replacement");
|
|
596
|
+
const taskTitle = text("taskTitle") || task;
|
|
597
|
+
const blockerTitle = text("blockerTitle") || blocker;
|
|
598
|
+
const replacementTitle = text("replacementTitle") || replacement;
|
|
599
|
+
const taskLink = html `<a href="${taskHref(task)}">${taskTitle}</a>`;
|
|
600
|
+
const blockerLink = html `<a href="${taskHref(blocker)}">${blockerTitle}</a>`;
|
|
601
|
+
const replacementLink = replacement === "" ? "" : html `<a href="${taskHref(replacement)}">${replacementTitle}</a>`;
|
|
602
|
+
const heading = operation === "retry"
|
|
603
|
+
? html `Try ${blockerLink} again`
|
|
604
|
+
: operation === "replace"
|
|
605
|
+
? html `Have ${taskLink} wait for different work`
|
|
606
|
+
: html `Let ${taskLink} continue without ${blockerLink}`;
|
|
607
|
+
const consequence = operation === "retry"
|
|
608
|
+
? `${taskTitle} will keep waiting while ${blockerTitle} gets another attempt.`
|
|
609
|
+
: operation === "replace"
|
|
610
|
+
? `${taskTitle} will wait for ${replacementTitle} instead.`
|
|
611
|
+
: `${taskTitle} may be ready to run once it no longer waits for ${blockerTitle}.`;
|
|
612
|
+
what = html `<h3>${heading}</h3><p class="proposal-summary">${consequence}</p>${facts(["task that is waiting", taskLink], ["work it needed", html `${blockerLink} · ${text("sawBlockerState")}`], ["wait for instead", replacementLink], ["project", mono(repoId)])}`;
|
|
613
|
+
}
|
|
614
|
+
else if (view.kind === "answer") {
|
|
615
|
+
const decisionId = typeof payload["decision"] === "number" ? payload["decision"] : 0;
|
|
616
|
+
const pick = text("option");
|
|
617
|
+
const options = decision === null
|
|
618
|
+
? html `<p class="meta">The decision is gone</p>`
|
|
619
|
+
: html `<ul class="answer-options">${decision.options.map(one => html `<li${one.id === pick ? html ` class="picked"` : ""}><strong>${one.label}</strong>${one.reversible ? "" : html ` <span class="badge">Irreversible</span>`}${one.id === decision.recommendation ? html ` <span class="meta">— the builder recommends this</span>` : ""}${one.id === pick ? html ` <span class="meta">— proposed</span>` : ""}<p class="meta">${one.consequence}</p></li>`)}</ul>`;
|
|
620
|
+
what = html `<h3>Answer <a href="/d/${decisionId}">decision #${decisionId}</a> on <a href="${taskHref(task)}">${task}</a></h3>${decision === null ? "" : html `<p class="proposal-summary">${decision.question}</p>`}${options}<div class="proposal-rationale"><span class="eyebrow">proposed answer</span><strong>${text("optionLabel")}</strong><p>${text("rationale")}</p></div><p class="meta proposal-disclosure">${payload["readConsequences"] === true ? `${view.by.mate ? "The mate" : "The coordinator"} read every consequence but not the builder's recommendation` : `${view.by.mate ? "The mate" : "The coordinator"} did not read the consequences`}. You see both here${decision !== null && decision.state !== "open" ? " · this decision is no longer open" : ""}.</p>`;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
what = html `<h3>Cancel <a href="${taskHref(task)}">${task}</a></h3><p class="proposal-summary">${text("reason")}</p>${facts(["project", mono(repoId)])}`;
|
|
624
|
+
}
|
|
625
|
+
const outcome = view.outcome;
|
|
626
|
+
const said = outcome !== null && typeof outcome.said === "string" ? outcome.said : null;
|
|
627
|
+
const flowHref = view.state === "confirmed" && outcome !== null && typeof outcome.href === "string" && FLOW_HREF.test(outcome.href) ? outcome.href : null;
|
|
628
|
+
const irreversible = view.kind === "answer" && payload["reversible"] === false;
|
|
629
|
+
const provenance = view.by.mate ? "mate" : html `${view.by.name} · ${view.by.ago}`;
|
|
630
|
+
const dismissForm = (button) => postForm(`${view.actionBase}/${view.id}/dismiss`, button, { attrs: { class: "inline" }, returnTo });
|
|
631
|
+
let acts;
|
|
632
|
+
if (view.state === "pending" && !inert) {
|
|
633
|
+
acts =
|
|
634
|
+
view.kind === "action" && sharedActionPayload(payload) !== null && sharedActionNeedsReview(sharedActionPayload(payload))
|
|
635
|
+
? html `<a class="button-link" href="${sharedActionReviewPath(view.id)}">Review action</a>${dismissForm(html `<button class="quiet">Dismiss</button>`)}`
|
|
636
|
+
: view.kind === "control"
|
|
637
|
+
? (isChatControl(payload["control"]) ? html `<a class="button-link" href="${chatControlHref(payload["control"], task, payload["run"], payload["project"])}">${CHAT_CONTROLS[payload["control"]].label}</a>` : html `<p>Control unavailable.</p>`)
|
|
638
|
+
: view.kind === "cancel"
|
|
639
|
+
? html `<p class="meta">Cancelling is armed on the task itself — <a href="${taskHref(task)}">open ${task}</a></p>${dismissForm(html `<button type="submit" class="quiet">Dismiss</button>`)}`
|
|
640
|
+
: html `<div class="acts">${postForm(`${view.actionBase}/${view.id}/confirm`, html `${irreversible ? html `<label class="arm"><input type="checkbox" name="confirm" value="yes"> I understand this cannot be undone</label>` : ""}<button type="submit">${presentation.action}</button>`, { attrs: { class: "inline" }, returnTo })}${dismissForm(html `<button type="submit" class="quiet">Dismiss</button>`)}</div>`;
|
|
641
|
+
}
|
|
642
|
+
else if (view.state === "pending") {
|
|
643
|
+
acts = html `<p class="meta proposal-wait">${PROPOSAL_WAIT_REASON}</p>`;
|
|
644
|
+
}
|
|
645
|
+
else if (view.state === "confirmed") {
|
|
646
|
+
const filed = outcome !== null && typeof outcome.taskId === "string" ? outcome.taskId : null;
|
|
647
|
+
acts = html `<p class="done">${said ?? "confirmed"}${(view.kind === "scope" || view.kind === "agents" || (view.kind === "review" && text("operation") === "revise")) && filed !== null
|
|
648
|
+
? html ` — <a href="${taskChatHref(filed)}#task-chat-action">review & start in chat</a>`
|
|
649
|
+
: ""}${flowHref === null ? "" : html ` — <a href="${flowHref}">open the flow</a>`}</p>${
|
|
650
|
+
// The created task, by its recorded id (package 2): one clear road
|
|
651
|
+
// into its lens — the same conversation, focused — and the overview.
|
|
652
|
+
filed !== null && view.kind === "task"
|
|
653
|
+
? html `<p class="proposal-filed"><a class="button-link" href="${taskChatHref(filed)}" data-filed-task="${filed}">Open task <span class="mono">${filed}</span> →</a> <a href="${taskHref(filed)}">overview</a></p>`
|
|
654
|
+
: ""}`;
|
|
655
|
+
}
|
|
656
|
+
else if (view.state === "refused") {
|
|
657
|
+
acts = html `<p class="refused">${said ?? "refused"}</p>`;
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
acts = html `<p class="meta">${view.state}</p>`;
|
|
661
|
+
}
|
|
662
|
+
const stateClass = view.state === "confirmed" ? "badge-done" : view.state === "refused" ? "badge-failed" : "";
|
|
663
|
+
const cardHtml = html `<article class="card proposal proposal-${view.kind} ${view.state}" data-card-kind="${view.kind}"><header class="proposal-head"><span class="proposal-icon">${strokeIcon(presentation.icon)}</span><span><strong>${presentation.label}</strong><small>proposed by ${provenance}</small></span><span class="badge ${stateClass}">${sentenceCase(view.state)}</span></header><div class="proposal-body">${what}</div><footer class="proposal-actions">${acts}</footer></article>`;
|
|
664
|
+
// The same card as data (chat cards): one act, in the door's own words.
|
|
665
|
+
const filedTask = outcome !== null && typeof outcome.taskId === "string" ? outcome.taskId : null;
|
|
666
|
+
const reviewAction = view.kind === "action" ? sharedActionPayload(payload) : null;
|
|
667
|
+
const sentence = (words) => words.charAt(0).toUpperCase() + words.slice(1);
|
|
668
|
+
const pending = view.state === "pending" && !inert;
|
|
669
|
+
const primary = !pending ? null
|
|
670
|
+
: reviewAction !== null && sharedActionNeedsReview(reviewAction) ? { kind: "link", label: "Review action", href: sharedActionReviewPath(view.id) }
|
|
671
|
+
: view.kind === "control" ? (isChatControl(payload["control"]) ? { kind: "link", label: CHAT_CONTROLS[payload["control"]].label, href: chatControlHref(payload["control"], task, payload["run"], payload["project"]) } : null)
|
|
672
|
+
: view.kind === "cancel" ? { kind: "link", label: "Open task", href: taskHref(task) }
|
|
673
|
+
: { kind: "confirm", label: sentence(presentation.action), irreversible, native: view.kind === "task_action" && payload["operation"] === "resume" };
|
|
674
|
+
const card = {
|
|
675
|
+
id: view.id, kind: view.kind, label: presentation.label, state: view.state, body: htmlString(what),
|
|
676
|
+
said: view.state === "confirmed" || view.state === "refused" ? said : null,
|
|
677
|
+
links: view.state !== "confirmed" ? [] : [
|
|
678
|
+
...(filedTask === null ? [] : [{ label: view.kind === "task" ? "Open the new task" : "Open the task", href: taskHref(filedTask) }]),
|
|
679
|
+
...(flowHref === null ? [] : [{ label: "Open the flow", href: flowHref }]),
|
|
680
|
+
],
|
|
681
|
+
primary,
|
|
682
|
+
dismissable: pending && view.kind !== "control",
|
|
683
|
+
note: view.state === "pending" && inert ? PROPOSAL_WAIT_REASON
|
|
684
|
+
: pending && view.kind === "cancel" ? "Cancelling is confirmed on the task itself."
|
|
685
|
+
: view.state === "dismissed" ? "Dismissed." : view.state === "expired" ? "Expired — this conversation moved on." : null,
|
|
686
|
+
};
|
|
687
|
+
return { html: cardHtml, card };
|
|
688
|
+
}
|
|
689
|
+
export function mateProposalCard(proposal, inert, decision, returnTo = null) {
|
|
690
|
+
return mateProposalCardParts(proposal, inert, decision, returnTo).html;
|
|
691
|
+
}
|
|
692
|
+
export function mateProposalCardParts(proposal, inert, decision, returnTo = null) {
|
|
693
|
+
return proposalCardParts({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: true }, actionBase: "/chat/proposal" }, inert, decision, returnTo);
|
|
694
|
+
}
|
|
695
|
+
/** Review and inline team cards share the same server-decided controls and reasons. The team API renders these
|
|
696
|
+
* outside a console request, so the session's own `csrf` is given to the forms here. */
|
|
697
|
+
export function teamProposalCardParts(store, actor, proposal, snapshot, csrf, decision, now, provider) {
|
|
698
|
+
const gate = proposalActGate(store, actor, proposal.thread, now, provider);
|
|
699
|
+
const reason = !gate.ok ? gate.said : null;
|
|
700
|
+
const back = '/chat?conversation=' + encodeURIComponent(snapshot.selected.id);
|
|
701
|
+
const parts = withFormToken(csrf, () => mateProposalCardParts(proposal, reason !== null, decision, back));
|
|
702
|
+
if (reason !== null && reason !== PROPOSAL_WAIT_REASON && proposal.state === 'pending') {
|
|
703
|
+
parts.html = replaceMarkup(parts.html, new RegExp(htmlString(html `${PROPOSAL_WAIT_REASON}`).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), () => html `${reason}`);
|
|
704
|
+
parts.card = { ...parts.card, note: reason };
|
|
705
|
+
}
|
|
706
|
+
return parts;
|
|
707
|
+
}
|
|
708
|
+
export function coordinatorProposalCard(proposal, now, decision, returnTo = null) {
|
|
709
|
+
return proposalCard({ id: proposal.id, kind: proposal.kind, payload: proposal.payload, state: proposal.state, outcome: proposal.outcome, by: { mate: false, name: proposal.name, ago: relativeAge(proposal.createdAt, now) }, actionBase: "/proposals" }, false, decision, returnTo);
|
|
710
|
+
}
|
|
711
|
+
/** The section shared by /chat (both modes) and the task page: pending coordinator proposals as cards. */
|
|
712
|
+
export function coordinatorProposalsSection(proposals, decisions, now, heading = true, returnTo = null) {
|
|
713
|
+
if (proposals.length === 0)
|
|
714
|
+
return html ``;
|
|
715
|
+
return html `${heading ? html `<h2>Proposed by coordinators <span class="meta">${proposals.length}</span></h2>` : ""}<div class="coordinator-proposals">${proposals.map(one => coordinatorProposalCard(one, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo))}</div>`;
|
|
716
|
+
}
|
|
717
|
+
export const REPLACED_THREAD_DIVIDER = "New conversation — the projects I can reach changed";
|
|
718
|
+
/** The replaced thread as the React conversation renders it: words and card outcomes, no buttons. */
|
|
719
|
+
export function replacedBrowserMessages(previous) {
|
|
720
|
+
return mateBrowserMessages({ ...previous, pending: null, ask: null, asks: new Map() }, null).map(message => ({
|
|
721
|
+
...message, cardsHtml: "",
|
|
722
|
+
cards: (message.cards ?? []).map(card => ({ ...card, primary: null, dismissable: false })),
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
/** The replaced thread for the server-rendered page: read only, then the divider. */
|
|
726
|
+
export function replacedThreadHtml(previous) {
|
|
727
|
+
if (previous == null)
|
|
728
|
+
return html ``;
|
|
729
|
+
return html `<section class="thread chat-previous" aria-label="Earlier conversation" data-previous-thread>${previous.messages.map(one => one.role === "operator"
|
|
730
|
+
? html `<div class="msg op" data-previous-message="${one.id}"><p style="white-space:pre-wrap">${one.text}</p></div>`
|
|
731
|
+
: html `<div class="msg mate" data-previous-message="${one.id}">${renderChatText(one.text)}</div>`)}<p class="chat-previous-divider" role="separator" data-thread-divider>${REPLACED_THREAD_DIVIDER}</p></section>`;
|
|
732
|
+
}
|
|
733
|
+
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
734
|
+
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
735
|
+
export function mateAskHtml(ask, target, composer, open = true) {
|
|
736
|
+
if (!open)
|
|
737
|
+
return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p></div>`;
|
|
738
|
+
const hidden = target.task !== null ? { task: target.task } : target.project !== null ? { project: target.project } : {};
|
|
739
|
+
return html `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${ask.question}</strong></p><div class="so-owner-ask-options" role="group" aria-label="Answer options">${ask.options.map(option => postForm("/chat", html `<button type="submit" name="message" value="${option}" class="so-suggestion quiet">${option}</button>`, { attrs: { class: "inline" }, hidden }))}<label for="${composer}" class="so-suggestion so-owner-ask-other">${MATE_ASK_OTHER}</label></div></div>`;
|
|
740
|
+
}
|
|
741
|
+
/** The thread's messages as the React conversation renders them, each
|
|
742
|
+
* with the proposal cards its turn produced; `back` is where a card's
|
|
743
|
+
* confirm or dismiss returns. */
|
|
744
|
+
export function mateBrowserMessages(rows, back, target = { task: null, project: null }) {
|
|
745
|
+
const open = rows.pending === null ? rows.ask?.turn ?? null : null;
|
|
746
|
+
const asked = (turn) => {
|
|
747
|
+
const ask = turn === null ? undefined : rows.asks?.get(turn);
|
|
748
|
+
return ask === undefined ? "" : mateAskHtml(ask, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
|
|
749
|
+
};
|
|
750
|
+
// The owner's message each reply answers: when it asked for ids, the reply keeps them.
|
|
751
|
+
const askedBy = new Map();
|
|
752
|
+
rows.messages.reduce((last, message) => { if (message.role === 'operator')
|
|
753
|
+
return message.text; if (last !== undefined)
|
|
754
|
+
askedBy.set(message.id, last); return last; }, undefined);
|
|
755
|
+
return rows.messages.map(message => ({
|
|
756
|
+
id: message.id, role: message.role, text: message.text,
|
|
757
|
+
html: htmlString(message.role === 'operator' ? html `<p>${message.text}</p>` : html `${renderChatText(message.text, askedBy.get(message.id))}${asked(message.turn)}`),
|
|
758
|
+
activity: message.activity, createdAt: message.createdAt,
|
|
759
|
+
...(() => {
|
|
760
|
+
// Under the lead's message only, as the server-rendered thread does: the person's message shares the turn.
|
|
761
|
+
const parts = message.turn === null || message.role === 'operator' ? [] : rows.proposals.filter(one => one.turn === message.turn)
|
|
762
|
+
.map(one => mateProposalCardParts(one, rows.pending !== null, rows.decisions.get(typeof one.payload['decision'] === 'number' ? one.payload['decision'] : -1) ?? null, back));
|
|
763
|
+
return { cardsHtml: htmlString(joinHtml(parts.map(one => one.html))), cards: parts.map(one => one.card) };
|
|
764
|
+
})(),
|
|
765
|
+
}));
|
|
766
|
+
}
|
|
767
|
+
/** The version of the DISPLAYED conversation (package 2): every fact a
|
|
768
|
+
* thread or task fragment renders — message identities, card states and
|
|
769
|
+
* outcomes, the live turn and its step count, the last turn's state, the
|
|
770
|
+
* decisions the cards name, and the task lens's own facts — and nothing
|
|
771
|
+
* that merely ticks: no relative age, no freshly minted nonce, no csrf.
|
|
772
|
+
* Equal versions mean equal fragments; the poll fetches nothing else. */
|
|
773
|
+
export function mateChatVersion(rows) {
|
|
774
|
+
const focus = rows.focusTask;
|
|
775
|
+
const facts = [
|
|
776
|
+
rows.messages.map(one => [one.id, one.role, one.turn]),
|
|
777
|
+
rows.previous == null ? null : rows.previous.messages.map(one => one.id),
|
|
778
|
+
rows.proposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
779
|
+
rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
780
|
+
[...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
|
|
781
|
+
rows.pending === null ? null : [rows.pending.id, rows.pending.steps],
|
|
782
|
+
rows.ask?.turn ?? null,
|
|
783
|
+
rows.recent[0] === undefined ? null : [rows.recent[0].id, rows.recent[0].state, rows.recent[0].failureReason],
|
|
784
|
+
focus === null
|
|
785
|
+
? null
|
|
786
|
+
: [
|
|
787
|
+
focus.id, focus.executionId, htmlString(focus.history), focus.title, focus.state, focus.scope, focus.plan, focus.claimed,
|
|
788
|
+
focus.approval === null ? null : [focus.approval.digest, focus.approval.nonce === "" ? "closed" : "open", focus.approval.revision !== null && "problem" in focus.approval.revision ? focus.approval.revision.problem : null],
|
|
789
|
+
focus.liveRun === null ? null : [focus.liveRun.id, focus.liveRun.phase, focus.liveRun.runner],
|
|
790
|
+
focus.control,
|
|
791
|
+
focus.dispatch === null ? null : [focus.dispatch.code, focus.dispatch.summary, focus.dispatch.detail],
|
|
792
|
+
focus.decisions.map(one => [one.id, one.state]),
|
|
793
|
+
focus.result === null ? null : [focus.result.runId, focus.result.outcome, focus.result.verdict],
|
|
794
|
+
focus.publication === null ? null : [focus.publication.id, focus.publication.state, focus.publication.lastCheckState],
|
|
795
|
+
focus.milestoneProgress === null ? null : focus.milestoneProgress.map(one => [one.id, one.state, one.note]),
|
|
796
|
+
focus.planRevisions === null ? null : [focus.planRevisions.current?.sha256 ?? null, focus.planRevisions.current?.status ?? null, focus.planRevisions.pending?.sha256 ?? null, focus.planRevisions.pending?.status ?? null, focus.planRevisions.history.length],
|
|
797
|
+
focus.route === null ? null : [focus.route.kind, focus.route.digest, focus.route.editable, focus.route.problem],
|
|
798
|
+
],
|
|
799
|
+
];
|
|
800
|
+
return createHash("sha256").update(JSON.stringify(facts)).digest("hex").slice(0, 16);
|
|
801
|
+
}
|
|
802
|
+
/** The thread region — the one safe region the live refresh reconciles
|
|
803
|
+
* (package 2): coordinator cards, the operator/assistant messages with
|
|
804
|
+
* their cards, the reply-in-progress card, and the starters that follow a
|
|
805
|
+
* populated thread. Every child carries a stable `data-key` so the page
|
|
806
|
+
* keeps an unchanged node (its open disclosures, its focus) and replaces
|
|
807
|
+
* only what the server rendered differently. The composer is never here. */
|
|
808
|
+
export function mateThreadHtml(data) {
|
|
809
|
+
const returnTo = data.focusTask === null ? "/chat" : taskChatHref(data.focusTask.id);
|
|
810
|
+
const parts = [];
|
|
811
|
+
if (data.problem !== null)
|
|
812
|
+
parts.push(html `<div class="problem" data-key="said">${data.problem}</div>`);
|
|
813
|
+
const byTurn = new Map();
|
|
814
|
+
for (const one of data.proposals) {
|
|
815
|
+
const list = byTurn.get(one.turn) ?? [];
|
|
816
|
+
list.push(one);
|
|
817
|
+
byTurn.set(one.turn, list);
|
|
818
|
+
}
|
|
819
|
+
const inert = data.pending !== null;
|
|
820
|
+
const lastMessage = data.messages.at(-1);
|
|
821
|
+
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
822
|
+
if (data.coordinatorProposals.length > 0)
|
|
823
|
+
parts.push(html `<div data-key="coordinators" data-chat-list>${coordinatorProposalsSection(data.coordinatorProposals, data.decisions, data.now, true, data.focusTask === null ? null : returnTo)}</div>`);
|
|
824
|
+
const thread = [];
|
|
825
|
+
if (data.messages.length === 0) {
|
|
826
|
+
thread.push(html `<div class="chat-empty" data-key="empty"><strong>${data.focusTask === null ? "What do you want to get done?" : "What do you want to understand or change?"}</strong><p class="meta">${data.focusTask === null ? "Describe a task or ask about your projects." : "Ask about progress, review results, or adjust the plan."}</p></div>`);
|
|
827
|
+
}
|
|
828
|
+
let asked;
|
|
829
|
+
for (const message of data.messages) {
|
|
830
|
+
if (message.role === "operator") {
|
|
831
|
+
asked = message.text;
|
|
832
|
+
thread.push(html `<div class="msg op" data-message-role="operator" data-key="m${message.id}"><p style="white-space:pre-wrap">${message.text}</p></div>`);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
|
|
836
|
+
const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
|
|
837
|
+
thread.push(html `<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? html ` id="latest"` : ""}>${renderChatText(message.text, asked)}${ask === undefined ? "" : mateAskHtml(ask, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn)}${cards.map(one => mateProposalCard(one, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo))}<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${message.createdAt}">${relativeAge(message.createdAt, data.now)}</time></div></div>`);
|
|
838
|
+
}
|
|
839
|
+
parts.push(html `<div class="thread" data-key="thread" data-chat-list>\n${joinHtml(thread, "\n")}\n</div>`);
|
|
840
|
+
if (data.pending !== null) {
|
|
841
|
+
const subscription = data.pending.reservedMicrousd === 0;
|
|
842
|
+
parts.push(chatWorkingHtml({ keyed: true,
|
|
843
|
+
details: `Turn #${data.pending.id} · ${data.pending.steps} step${data.pending.steps === 1 ? "" : "s"}${subscription ? " · subscription" : ` · up to ${chatMoney(data.pending.reservedMicrousd)} reserved`}`,
|
|
844
|
+
stopForm: postForm("/chat/mate/stop", html `<button type="submit" class="quiet" aria-label="Stop chat response">Stop</button>`, { attrs: { class: "inline" }, returnTo, hidden: { turn: data.pending.id } }),
|
|
845
|
+
}));
|
|
846
|
+
}
|
|
847
|
+
// Suggestions help start a conversation; repeating them after every
|
|
848
|
+
// response competes with the actual result and its available actions.
|
|
849
|
+
return html `<div id="chat-thread" data-chat-region="thread">${joinHtml(parts, "\n")}</div>`;
|
|
850
|
+
}
|
|
851
|
+
/** What follows the composer (package 2): the starters of an EMPTY thread
|
|
852
|
+
* sit under the box; once the first message lands they move above it. */
|
|
853
|
+
export function mateAfterComposerHtml(data) {
|
|
854
|
+
return html `<div id="chat-after-composer" data-chat-region="after">${data.messages.length === 0 && data.pending === null ? matePromptStarters(data.focusTask) : ""}</div>`;
|
|
855
|
+
}
|
|
856
|
+
export function matePage(chrome, data) {
|
|
857
|
+
const subscription = isSubscriptionChatProvider(data.config.provider);
|
|
858
|
+
const chatProject = data.focusTask === null ? data.chatProject ?? null : null;
|
|
859
|
+
const returnTo = data.focusTask !== null ? taskChatHref(data.focusTask.id) : chatProject !== null ? projectChatHref(chatProject) : "/chat";
|
|
860
|
+
const controlsHtml = joinHtml([
|
|
861
|
+
html `<details class="lead-follow"><summary>Automatic crew updates${data.follow?.enabled ? ' · On' : ''}</summary><p class="meta">${data.follow?.detail ?? 'Automatic crew updates are off.'}</p>${postForm("/chat/mate/follow", html `${data.follow?.enabled ? '' : html `<p>The lead responds when results or decisions arrive. Uses this conversation’s ${subscription ? 'membership usage, with no dollar maximum' : 'remaining spend allowance'} and daily turn limit. Existing task approvals still apply.</p>`}<button type="submit" class="quiet">${data.follow?.enabled ? 'Pause updates' : 'Enable updates'}</button>`, { returnTo, hidden: { enabled: data.follow?.enabled ? 'no' : 'yes' } })}</details>`,
|
|
862
|
+
html `<details class="chat-limits chat-session-details"><summary>Conversation details<span class="meta">${subscription ? "membership" : data.config.provider}</span></summary>\n${joinHtml([
|
|
863
|
+
html `<div class="chat-budget"><span class="mono">${data.config.provider} · ${data.config.model}</span><span>${data.turnsToday} / ${data.config.dailyTurns} turns today</span>${subscription
|
|
864
|
+
? html `<span>membership login · no dollar ceiling</span>`
|
|
865
|
+
: html `<span>this conversation: ${chatMoney(data.session.spentMicrousd)} of ${chatMoney(data.session.ceilingMicrousd)}</span><span>this week ${chatMoney(data.weeklySpent)} of ${chatMoney(data.config.weeklyCeilingMicrousd)}</span>`}</div>`,
|
|
866
|
+
html `<p class="meta">Started ${data.session.mintedAt.slice(0, 16).replace("T", " ")}Z. It stays open until you end it; only bounded recent context is sent to the model.</p>`,
|
|
867
|
+
postForm("/chat/mate/end", html `<button type="submit" class="quiet">End the conversation and forget the thread</button>`, { attrs: { class: "inline" }, returnTo }),
|
|
868
|
+
data.recent.length === 0
|
|
869
|
+
? ""
|
|
870
|
+
: html `<p class="meta">Recent turns: ${joinHtml(data.recent
|
|
871
|
+
.map(turn => html `<span class="mono">#${turn.id}</span> ${turn.state}${turn.failureReason === null ? "" : html ` · ${turn.failureReason}`} · ${subscription ? "membership" : chatMoney(turn.settledMicrousd ?? turn.reservedMicrousd)}`), " · ")}</p>`,
|
|
872
|
+
html `<p class="meta"><a href="/settings/lead">Lead settings</a> · provider, model and limits</p>`,
|
|
873
|
+
], "\n")}\n</details>`,
|
|
874
|
+
], "\n");
|
|
875
|
+
const conversation = [
|
|
876
|
+
data.focusTask === null
|
|
877
|
+
? chatHeading("", data.projects.length)
|
|
878
|
+
: taskChatHeading(data.focusTask),
|
|
879
|
+
data.focusTask === null ? (data.catchUp ?? "") : taskChatLiveRegion(data.focusTask, data.csrf, false, data.pending !== null),
|
|
880
|
+
// The DB catch-up replaces the older, duplicate portfolio summary.
|
|
881
|
+
data.focusTask === null && !data.catchUp ? html `<details class="chat-fleet-context">${chatOverviewSummaryHtml(data.projects, data.fleetSnapshot?.attentionCount)}${chatFleetOverview(data.fleetSnapshot, data.projects, data.pending === null)}</details>` : "",
|
|
882
|
+
];
|
|
883
|
+
if (data.problem !== null)
|
|
884
|
+
conversation.push(html `<div class="problem">${data.problem}</div>`);
|
|
885
|
+
for (const turn of data.latched) {
|
|
886
|
+
conversation.push(html `<div class="problem"><strong>Chat is paused.</strong> An earlier reply stopped before its cost was known; it may have cost up to ${chatMoney(turn.reservedMicrousd)}. <a href="/chat/ack/${turn.id}">Confirm that cost</a> to turn chat back on.</div>`);
|
|
887
|
+
}
|
|
888
|
+
const lastMessage = data.messages.at(-1);
|
|
889
|
+
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
890
|
+
conversation.push(replacedThreadHtml(data.previous));
|
|
891
|
+
conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
|
|
892
|
+
conversation.push(
|
|
893
|
+
// The New update action (package 2): hidden until a live update lands
|
|
894
|
+
// while the reader is above the latest message; a real button, so the
|
|
895
|
+
// keyboard reaches it. Only this act moves the reader.
|
|
896
|
+
html `<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, postForm("/chat", joinHtml([
|
|
897
|
+
html `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${projectName(chatProject)}…` : "Describe what you want done…"}"></textarea></label>`,
|
|
898
|
+
html `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : html ` disabled`}>Send</button>`,
|
|
899
|
+
], "\n"), {
|
|
900
|
+
attrs: { class: "card composer", id: latestReply === null && data.pending === null ? "latest" : "chat-composer", "aria-label": "message the mate", "data-chat-session": data.session.id, "data-chat-task": data.focusTask?.id ?? "", "data-chat-user": data.session.approver, "data-chat-busy": data.pending === null ? "0" : "1", "data-chat-version": mateChatVersion(data), "data-chat-approval": data.focusTask?.approval?.digest ?? "" },
|
|
901
|
+
hidden: { request: randomBytes(16).toString("hex"), "request-session": data.session.id, task: data.focusTask?.id, project: chatProject, result: data.resultRunId },
|
|
902
|
+
}),
|
|
903
|
+
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
904
|
+
// a state to report — a reply in progress here, the connection from
|
|
905
|
+
// the continuity script — and the composer carries no second intro.
|
|
906
|
+
html `<p class="meta composer-hint" id="chat-connection" role="status" aria-live="polite">${data.pending === null ? "" : "Reply in progress. You can draft your next message or come back later."}</p>`,
|
|
907
|
+
// The explicit reconnection (package 2): shown only once the session
|
|
908
|
+
// or sign-in changed under this page; it reloads on the reader's act.
|
|
909
|
+
html `<p class="meta composer-hint" id="chat-reconnect" hidden><button type="button" class="quiet">Reconnect</button></p>`, mateAfterComposerHtml(data), controlsHtml);
|
|
910
|
+
return screen("chat", chatWorkspace(joinHtml(conversation, "\n"), data.projects, false, data.focusTask, data.resultPanel ?? null), { chrome, functional: { script: CHAT_CONTINUITY_SCRIPT + CHAT_UI_SCRIPT + (data.focusTask === null ? "" : RESULT_REVIEW_SCRIPT), fetches: true },
|
|
911
|
+
workspace: {
|
|
912
|
+
conversation: {
|
|
913
|
+
sessionId: data.session.id, user: data.session.approver, version: mateChatVersion(data),
|
|
914
|
+
messages: mateBrowserMessages(data, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
915
|
+
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
916
|
+
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
917
|
+
...(data.previous == null ? {} : { previous: { messages: replacedBrowserMessages(data.previous) } }),
|
|
918
|
+
},
|
|
919
|
+
focus: data.focusTask === null ? null : { id: data.focusTask.id, title: data.focusTask.title,
|
|
920
|
+
html: htmlString(taskChatLiveRegion(data.focusTask, data.csrf, requestContext.getStore()?.workspaceRead === true, data.pending !== null)) },
|
|
921
|
+
result: data.resultPanel && data.resultRunId != null ? { runId: data.resultRunId, html: htmlString(data.resultPanel) } : null,
|
|
922
|
+
catchUpHtml: html `${data.focusTask === null ? data.catchUp ?? '' : ''}${coordinatorProposalsSection(data.coordinatorProposals, data.decisions, data.now, true, data.focusTask === null ? null : returnTo)}${data.latched.map(turn => html `<p class="problem">Chat is paused because usage is unconfirmed. <a href="/chat/ack/${turn.id}">Inspect turn #${turn.id}</a>.</p>`)}`,
|
|
923
|
+
controlsHtml,
|
|
924
|
+
notices: data.problem === null ? [] : [data.problem], pageHtml: null,
|
|
925
|
+
},
|
|
926
|
+
});
|
|
927
|
+
}
|