toolroll 0.9.14 → 0.9.15
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 +15 -0
- package/dist/assignment-ui.d.ts +1 -0
- package/dist/assignment-ui.js +6 -4
- package/dist/assignment.d.ts +32 -0
- package/dist/assignment.js +81 -7
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-crew.d.ts +2 -1
- package/dist/browser-crew.js +2 -1
- package/dist/browser-workspace.d.ts +88 -18
- package/dist/chat-actions.d.ts +1 -1
- package/dist/chat-actions.js +6 -4
- package/dist/chat-delivery-state.d.ts +1 -1
- package/dist/chat-voice.d.ts +3 -3
- package/dist/chat-voice.js +3 -3
- package/dist/demo.js +82 -2
- package/dist/disclosure.d.ts +6 -0
- package/dist/disclosure.js +13 -0
- package/dist/lead-status.js +2 -2
- package/dist/needs-you.d.ts +48 -0
- package/dist/needs-you.js +93 -1
- package/dist/result-acts.d.ts +49 -5
- package/dist/result-acts.js +34 -3
- package/dist/result-review.d.ts +4 -3
- package/dist/result-review.js +7 -3
- package/dist/serve.d.ts +3 -0
- package/dist/serve.js +1121 -677
- package/dist/slack-chat.js +1 -1
- package/dist/task-status.d.ts +21 -4
- package/dist/task-status.js +41 -16
- package/dist/test-temp.js +1 -1
- package/dist/transitions-recipes.d.ts +1 -1
- package/dist/transitions-recipes.js +5 -9
- package/dist/when-html.d.ts +19 -2
- package/dist/when-html.js +78 -11
- package/dist/work-index.d.ts +3 -1
- package/dist/work-index.js +13 -3
- package/dist/workspace-motion.js +3 -3
- package/dist/workspace-ui.d.ts +43 -7
- package/dist/workspace-ui.js +59 -15
- package/package.json +1 -1
package/dist/slack-chat.js
CHANGED
|
@@ -360,7 +360,7 @@ export async function deliverSlackPart(options) {
|
|
|
360
360
|
}))
|
|
361
361
|
: []),
|
|
362
362
|
...linkButton(options.origin(), content.link),
|
|
363
|
-
// [Look first] beside [Merge] or [
|
|
363
|
+
// [Look first] beside [Merge] or [Accept and finish]: each button's own action id.
|
|
364
364
|
...(content.also ?? []).flatMap((one, index) => linkButton(options.origin(), one).map(button => ({ ...button, action_id: `toolroll_link_${index + 2}` }))),
|
|
365
365
|
];
|
|
366
366
|
const target = content.edit ?? row.message;
|
package/dist/task-status.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export declare const HEADLINES: readonly ["Queued", "Planning", "Needs you", "Wa
|
|
|
20
20
|
export type Headline = (typeof HEADLINES)[number];
|
|
21
21
|
export type HeadlineTone = "neutral" | "live" | "attention" | "ready" | "success" | "danger";
|
|
22
22
|
export declare const HEADLINE_TONE: Readonly<Record<Headline, HeadlineTone>>;
|
|
23
|
+
/** The project's own checks and the pull request's CI are two different things, never one word (Checks beside CI). */
|
|
24
|
+
export declare const CHECKS_LABEL = "Project checks";
|
|
25
|
+
export declare const PR_CI_LABEL = "PR CI";
|
|
26
|
+
/** The one verb for opening a saved result, on every surface. */
|
|
27
|
+
export declare const OPEN_RESULT = "Open result";
|
|
23
28
|
export type DetailKey = "checks" | "pull-request" | "requirements" | "evidence";
|
|
24
29
|
/** The row's small icon: the only coloured part of a detail. `failed` is red
|
|
25
30
|
* and only ever appears under the Failed headline; `note` is amber. */
|
|
@@ -86,11 +91,13 @@ export type TaskStatusFacts = {
|
|
|
86
91
|
report?: boolean;
|
|
87
92
|
checks?: ChecksFact | null;
|
|
88
93
|
pullRequest?: PullRequestFact | null;
|
|
89
|
-
/** `unverified`: the report is refuted, so no requirement counts as met however it was marked.
|
|
94
|
+
/** `unverified`: the report is refuted, so no requirement counts as met however it was marked. `missed`: the ones it
|
|
95
|
+
* failed or left unanswered. */
|
|
90
96
|
requirements?: {
|
|
91
97
|
met: number;
|
|
92
98
|
total: number;
|
|
93
99
|
yours: number;
|
|
100
|
+
missed?: number;
|
|
94
101
|
unverified?: boolean;
|
|
95
102
|
} | null;
|
|
96
103
|
evidence?: {
|
|
@@ -153,14 +160,19 @@ export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "
|
|
|
153
160
|
need?: TaskStatusFacts["need"];
|
|
154
161
|
wait?: WaitKey;
|
|
155
162
|
};
|
|
156
|
-
|
|
157
|
-
export declare function requirementsOf(matrix: readonly {
|
|
163
|
+
type RequirementRow = {
|
|
158
164
|
state: string;
|
|
159
165
|
assessment?: {
|
|
160
166
|
evidenceState?: string;
|
|
161
167
|
} | undefined;
|
|
162
168
|
review?: unknown;
|
|
163
|
-
}
|
|
169
|
+
};
|
|
170
|
+
/** One requirement's state in words: the card's Requirements row and the Checks tab read this one source, so they
|
|
171
|
+
* can't disagree. A refuted report verifies none of its requirements, so each reads Unverified, as the card does. */
|
|
172
|
+
export type RequirementWord = "Met" | "You check" | "Not shown yet" | "Not met" | "Unverified";
|
|
173
|
+
export declare function requirementWordOf(row: RequirementRow, verdict?: string | null): RequirementWord;
|
|
174
|
+
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
175
|
+
export declare function requirementsOf(matrix: readonly RequirementRow[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
|
|
164
176
|
/** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
|
|
165
177
|
export declare function unverifiedWhenRefuted(requirements: NonNullable<TaskStatusFacts["requirements"]> | null, verdict: string | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
|
|
166
178
|
/** Saved evidence health from the receipt's artifact list and its caveats. */
|
|
@@ -188,6 +200,8 @@ export declare function headlineEmoji(headline: Headline): string;
|
|
|
188
200
|
export declare const statusIconSvg: (mark: DetailMark) => string;
|
|
189
201
|
/** The detail rows as server HTML: quiet rows, colour only on the icon. */
|
|
190
202
|
export declare function statusDetailsHtml(status: TaskStatus): string;
|
|
203
|
+
/** The status card's fold of exact reasons: "More", never a third thing called Details. */
|
|
204
|
+
export declare const STATUS_MORE = "More";
|
|
191
205
|
/** The technical reasons, one tap away. */
|
|
192
206
|
export declare function statusWhyHtml(status: TaskStatus, extra?: readonly string[], diagnostics?: readonly {
|
|
193
207
|
token: string;
|
|
@@ -211,3 +225,6 @@ export declare function stageOfDispatch(d: {
|
|
|
211
225
|
stage: TaskStage;
|
|
212
226
|
need?: TaskStatusFacts["need"];
|
|
213
227
|
};
|
|
228
|
+
/** The demo runs no checks: a Checks row that would offer to run one (or open one that never ran) says so instead. */
|
|
229
|
+
export declare const DEMO_CHECKS = "Can't run in the demo";
|
|
230
|
+
export declare function demoChecksOf(status: TaskStatus): TaskStatus;
|
package/dist/task-status.js
CHANGED
|
@@ -8,6 +8,11 @@ export const HEADLINE_TONE = {
|
|
|
8
8
|
Queued: "neutral", Planning: "live", "Needs you": "attention", Waiting: "neutral", Building: "live",
|
|
9
9
|
"Ready for review": "ready", Complete: "success", Failed: "danger", Stopped: "neutral",
|
|
10
10
|
};
|
|
11
|
+
/** The project's own checks and the pull request's CI are two different things, never one word (Checks beside CI). */
|
|
12
|
+
export const CHECKS_LABEL = "Project checks";
|
|
13
|
+
export const PR_CI_LABEL = "PR CI";
|
|
14
|
+
/** The one verb for opening a saved result, on every surface. */
|
|
15
|
+
export const OPEN_RESULT = "Open result";
|
|
11
16
|
const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
|
|
12
17
|
export function headlineOf(facts) {
|
|
13
18
|
switch (facts.stage) {
|
|
@@ -76,19 +81,20 @@ function detailsOf(headline, facts) {
|
|
|
76
81
|
const runChecks = facts.links?.runChecks === undefined ? null : { label: "Run checks", href: facts.links.runChecks };
|
|
77
82
|
const quick = checks.level === "quick";
|
|
78
83
|
if (checks.running != null)
|
|
79
|
-
row("checks",
|
|
84
|
+
row("checks", CHECKS_LABEL, `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
|
|
80
85
|
else if (checks.status === "passed")
|
|
81
|
-
row("checks",
|
|
86
|
+
row("checks", CHECKS_LABEL, `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
|
|
82
87
|
else if (checks.status === "failed")
|
|
83
|
-
row("checks",
|
|
88
|
+
row("checks", CHECKS_LABEL, `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
|
|
84
89
|
else if (checks.status === "running")
|
|
85
|
-
row("checks",
|
|
90
|
+
row("checks", CHECKS_LABEL, "Running", "running");
|
|
86
91
|
else if (checks.level === "off")
|
|
87
|
-
row("checks",
|
|
92
|
+
row("checks", CHECKS_LABEL, "Off for this project", "none", runChecks === null ? {} : { action: runChecks });
|
|
88
93
|
else if (checks.status === "not-run")
|
|
89
|
-
row("checks",
|
|
94
|
+
row("checks", CHECKS_LABEL, "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
|
|
95
|
+
// A saved check that can't be read is run again where that is possible.
|
|
90
96
|
else
|
|
91
|
-
row("checks",
|
|
97
|
+
row("checks", CHECKS_LABEL, "Couldn't be read", "note", { action: runChecks ?? { label: OPEN_RESULT, href: facts.links?.result ?? null } });
|
|
92
98
|
}
|
|
93
99
|
const pr = facts.pullRequest;
|
|
94
100
|
if (pr != null) {
|
|
@@ -105,16 +111,19 @@ function detailsOf(headline, facts) {
|
|
|
105
111
|
else if (pr.state === "failed")
|
|
106
112
|
row("pull-request", "Pull request", "Couldn't open", "note", { action: pr.compareUrl ? { label: "Open it on GitHub", href: pr.compareUrl } : { label: "See why", href: facts.links?.pullRequest ?? null }, why: pr.error === null ? "Publishing gave up. The commit is safe locally." : `${pr.error} The commit is safe locally.` });
|
|
107
113
|
else if (pr.ci === "running")
|
|
108
|
-
row("pull-request", "Pull request", `${name} open
|
|
114
|
+
row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} running`, "running", { href: github });
|
|
109
115
|
else if (pr.ci === "passing")
|
|
110
|
-
row("pull-request", "Pull request", `${name} open
|
|
116
|
+
row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} passed`, "ok", { href: github });
|
|
111
117
|
else if (pr.ci === "failing")
|
|
112
|
-
row("pull-request", "Pull request", `${name} open
|
|
118
|
+
row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} failing`, "note", { action: { label: "Open it on GitHub", href: github } });
|
|
113
119
|
else
|
|
114
120
|
row("pull-request", "Pull request", `${name} open`, "none", { href: github });
|
|
115
121
|
}
|
|
116
122
|
const req = facts.requirements;
|
|
117
|
-
|
|
123
|
+
// A failed result names how many it missed, refuted or not: that is what failed.
|
|
124
|
+
if (req != null && req.total > 0 && headline === "Failed" && (req.missed ?? 0) > 0)
|
|
125
|
+
row("requirements", "Requirements", `${req.missed} missed`, "failed");
|
|
126
|
+
else if (req != null && req.total > 0 && req.unverified === true)
|
|
118
127
|
row("requirements", "Requirements", "Unverified", "none");
|
|
119
128
|
else if (req != null && req.total > 0) {
|
|
120
129
|
const unmet = req.total - req.met - req.yours;
|
|
@@ -258,13 +267,21 @@ export function assignmentStageOf(assignment, work, planning = false) {
|
|
|
258
267
|
}
|
|
259
268
|
}
|
|
260
269
|
}
|
|
270
|
+
/** Evidence that passed and only awaits the retired assessment step is met. */
|
|
271
|
+
const requirementMet = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
|
|
272
|
+
export function requirementWordOf(row, verdict) {
|
|
273
|
+
if (verdict === "refuted")
|
|
274
|
+
return "Unverified";
|
|
275
|
+
if (requirementMet(row))
|
|
276
|
+
return "Met";
|
|
277
|
+
return row.state === "manual-review" ? "You check" : row.state === "missing" ? "Not shown yet" : "Not met";
|
|
278
|
+
}
|
|
261
279
|
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
262
280
|
export function requirementsOf(matrix) {
|
|
263
281
|
if (!matrix || matrix.length === 0)
|
|
264
282
|
return null;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return { met: matrix.filter(met).length, total: matrix.length, yours: matrix.filter(row => !met(row) && row.state === "manual-review").length };
|
|
283
|
+
return { met: matrix.filter(requirementMet).length, total: matrix.length, yours: matrix.filter(row => requirementWordOf(row) === "You check").length,
|
|
284
|
+
missed: matrix.filter(row => !requirementMet(row) && (row.state === "failed" || row.state === "missing")).length };
|
|
268
285
|
}
|
|
269
286
|
/** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
|
|
270
287
|
export function unverifiedWhenRefuted(requirements, verdict) {
|
|
@@ -347,18 +364,20 @@ export const statusIconSvg = (mark) => `<svg class="status-icon status-icon--${m
|
|
|
347
364
|
export function statusDetailsHtml(status) {
|
|
348
365
|
if (status.details.length === 0)
|
|
349
366
|
return "";
|
|
350
|
-
return `<ul class="status-details" aria-label="
|
|
367
|
+
return `<ul class="status-details" aria-label="Status">` + status.details.map(one => {
|
|
351
368
|
const text = one.href === null ? escapeHtml(one.text) : `<a href="${escapeHtml(one.href)}">${escapeHtml(one.text)}</a>`;
|
|
352
369
|
const action = one.action === null ? "" : one.action.href === null ? `<span class="status-detail-act">${escapeHtml(one.action.label)}</span>` : `<a class="status-detail-act" href="${escapeHtml(one.action.href)}">${escapeHtml(one.action.label)}</a>`;
|
|
353
370
|
return `<li class="status-detail status-detail--${one.mark}" data-status-detail="${one.key}" data-mark="${one.mark}">${statusIconSvg(one.mark)}<span class="status-detail-label">${escapeHtml(one.label)}</span><span class="status-detail-text"><span>${text}</span>${action}</span></li>`;
|
|
354
371
|
}).join("") + `</ul>`;
|
|
355
372
|
}
|
|
373
|
+
/** The status card's fold of exact reasons: "More", never a third thing called Details. */
|
|
374
|
+
export const STATUS_MORE = "More";
|
|
356
375
|
/** The technical reasons, one tap away. */
|
|
357
376
|
export function statusWhyHtml(status, extra = [], diagnostics = []) {
|
|
358
377
|
const lines = [...new Set([...status.details.flatMap(one => one.why === null ? [] : [`${one.label}: ${one.why}`]), ...status.why, ...extra])];
|
|
359
378
|
if (lines.length === 0 && diagnostics.length === 0)
|
|
360
379
|
return "";
|
|
361
|
-
return `<details class="status-why"><summary
|
|
380
|
+
return `<details class="status-why"><summary>${STATUS_MORE}</summary>${lines.map(one => `<p class="meta">${escapeHtml(one)}</p>`).join("")}` +
|
|
362
381
|
diagnostics.map(one => `<p class="meta" data-work-diagnostic="${escapeHtml(one.token)}">${escapeHtml(one.label)} · ${escapeHtml(one.detail)}</p>`).join("") + `</details>`;
|
|
363
382
|
}
|
|
364
383
|
/** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
|
|
@@ -377,3 +396,9 @@ export function stageOfDispatch(d, options = {}) {
|
|
|
377
396
|
return { stage: "needs-you", need: "review-result" };
|
|
378
397
|
return stageOfCode(d.code, { needsPerson: d.condition === "waiting" && d.action !== null, planning: d.condition === "running" && d.role === "planner", operatorHold: d.action === "unhold" });
|
|
379
398
|
}
|
|
399
|
+
/** The demo runs no checks: a Checks row that would offer to run one (or open one that never ran) says so instead. */
|
|
400
|
+
export const DEMO_CHECKS = "Can't run in the demo";
|
|
401
|
+
export function demoChecksOf(status) {
|
|
402
|
+
return { ...status, details: status.details.map(one => one.key !== "checks" || one.mark === "ok" || one.mark === "failed" || one.mark === "running" ? one
|
|
403
|
+
: { ...one, text: DEMO_CHECKS, mark: "none", href: null, action: null, why: null }) };
|
|
404
|
+
}
|
package/dist/test-temp.js
CHANGED
|
@@ -18,7 +18,7 @@ export const TEST_TEMP_PREFIXES = [
|
|
|
18
18
|
"flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
|
|
19
19
|
"proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
|
|
20
20
|
"task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
|
|
21
|
-
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-",
|
|
21
|
+
"needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-",
|
|
22
22
|
// Playwright's browser profiles and artifacts.
|
|
23
23
|
"playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
|
|
24
24
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const TRANSITIONS_CSS = "\n/* Transitions.dev \u2014 05-menu-dropdown.md */\n:root {\n --dropdown-open-dur: 250ms;\n --dropdown-close-dur: 150ms;\n --dropdown-pre-scale: 0.97;\n --dropdown-closing-scale: 0.99;\n --dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.t-dropdown {\n transform-origin: top left;\n transform: scale(var(--dropdown-pre-scale));\n opacity: 0;\n pointer-events: none;\n transition:\n transform var(--dropdown-open-dur) var(--dropdown-ease),\n opacity var(--dropdown-open-dur) var(--dropdown-ease);\n will-change: transform, opacity;\n}\n.t-dropdown[data-origin=\"top-right\"] { transform-origin: top right; }\n.t-dropdown[data-origin=\"top-center\"] { transform-origin: top center; }\n.t-dropdown[data-origin=\"bottom-left\"] { transform-origin: bottom left; }\n.t-dropdown[data-origin=\"bottom-center\"] { transform-origin: bottom center; }\n.t-dropdown[data-origin=\"bottom-right\"] { transform-origin: bottom right; }\n\n.t-dropdown.is-open {\n transform: scale(1);\n opacity: 1;\n pointer-events: auto;\n}\n.t-dropdown.is-closing {\n transform: scale(var(--dropdown-closing-scale));\n opacity: 0;\n pointer-events: none;\n transition:\n transform var(--dropdown-close-dur) var(--dropdown-ease),\n opacity var(--dropdown-close-dur) var(--dropdown-ease);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .t-dropdown { transition: none !important; }\n}\n\n/* Transitions.dev \u2014 07-panel-reveal.md */\n:root {\n --panel-open-dur: 400ms;\n --panel-close-dur: 350ms;\n --panel-translate-y: 100px;\n --panel-blur: 2px;\n --panel-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.t-panel-slide {\n transform: translateY(var(--panel-translate-y));\n opacity: 0;\n filter: blur(var(--panel-blur));\n pointer-events: none;\n transition:\n transform var(--panel-close-dur) var(--panel-ease),\n opacity var(--panel-close-dur) var(--panel-ease),\n filter var(--panel-close-dur) var(--panel-ease);\n will-change: transform, opacity, filter;\n}\n.t-panel-slide[data-open=\"true\"] {\n transform: translateY(0);\n opacity: 1;\n filter: blur(0);\n pointer-events: auto;\n transition:\n transform var(--panel-open-dur) var(--panel-ease),\n opacity var(--panel-open-dur) var(--panel-ease),\n filter var(--panel-open-dur) var(--panel-ease);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .t-panel-slide { transition: none !important; }\n}\n\n/* Transitions.dev \u2014 16-tabs-sliding.md */\n:root {\n --tabs-dur: 250ms;\n --tabs-ease: cubic-bezier(0.22, 1, 0.36, 1);\n --tabs-text-muted: rgba(15, 15, 15, 0.8);\n --tabs-text-active: #0f0f0f;\n --tabs-bar-bg: #f1f1f1;\n --tabs-pill-bg: #ffffff;\n}\n\n/* The bar is just a flex container with padding for the pill\n to sit inside. Tabs sit on z-index: 1, the pill on z-index: 0,\n so labels read above the pill background. */\n.t-tabs {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 3px;\n border-radius: 48px;\n background: var(--tabs-bar-bg);\n}\n.t-tab {\n position: relative;\n appearance: none;\n border: 0;\n background: transparent;\n height: 30px;\n padding: 4px 12px;\n color: var(--tabs-text-muted);\n cursor: pointer;\n border-radius: 48px;\n z-index: 1;\n transition: color var(--tabs-dur) var(--tabs-ease);\n}\n.t-tab:not([aria-selected=\"true\"]):hover,\n.t-tab[aria-selected=\"true\"] {\n color: var(--tabs-text-active);\n}\n\n/* The pill: width + transform are written inline by JS so\n the transition tweens between the previous and next\n measured positions. */\n.t-tabs-pill {\n position: absolute;\n top: 3px;\n left: 0;\n height: 30px;\n width: 0;\n background: var(--tabs-pill-bg);\n border-radius: 48px;\n transform: translateX(0);\n transition:\n transform var(--tabs-dur) var(--tabs-ease),\n width var(--tabs-dur) var(--tabs-ease);\n will-change: transform, width;\n z-index: 0;\n pointer-events: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .t-tabs-pill, .t-tab { transition: none !important; }\n}\n\n/* Transitions.dev \u2014 21-accordion.md */\n:root {\n --acc-expand: 250ms;\n --acc-collapse: 250ms;\n --acc-chevron: 250ms;\n --acc-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n/* grid-template-rows 0fr \u2192 1fr gives a clean height animation\n with no JS measurement; the inner element clips overflow. */\n.t-acc-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--acc-collapse) var(--acc-ease);\n}\n.t-acc[data-open=\"true\"] .t-acc-panel {\n grid-template-rows: 1fr;\n transition: grid-template-rows var(--acc-expand) var(--acc-ease);\n}\n.t-acc-panel-inner {\n overflow: hidden;\n opacity: 0;\n filter: blur(2px);\n transition:\n opacity var(--acc-collapse) var(--acc-ease),\n filter var(--acc-collapse) var(--acc-ease);\n}\n.t-acc[data-open=\"true\"] .t-acc-panel-inner {\n opacity: 1;\n filter: blur(0);\n transition:\n opacity var(--acc-expand) var(--acc-ease),\n filter var(--acc-expand) var(--acc-ease);\n}\n/*
|
|
1
|
+
export declare const TRANSITIONS_CSS = "\n/* Transitions.dev \u2014 05-menu-dropdown.md */\n:root {\n --dropdown-open-dur: 250ms;\n --dropdown-close-dur: 150ms;\n --dropdown-pre-scale: 0.97;\n --dropdown-closing-scale: 0.99;\n --dropdown-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.t-dropdown {\n transform-origin: top left;\n transform: scale(var(--dropdown-pre-scale));\n opacity: 0;\n pointer-events: none;\n transition:\n transform var(--dropdown-open-dur) var(--dropdown-ease),\n opacity var(--dropdown-open-dur) var(--dropdown-ease);\n will-change: transform, opacity;\n}\n.t-dropdown[data-origin=\"top-right\"] { transform-origin: top right; }\n.t-dropdown[data-origin=\"top-center\"] { transform-origin: top center; }\n.t-dropdown[data-origin=\"bottom-left\"] { transform-origin: bottom left; }\n.t-dropdown[data-origin=\"bottom-center\"] { transform-origin: bottom center; }\n.t-dropdown[data-origin=\"bottom-right\"] { transform-origin: bottom right; }\n\n.t-dropdown.is-open {\n transform: scale(1);\n opacity: 1;\n pointer-events: auto;\n}\n.t-dropdown.is-closing {\n transform: scale(var(--dropdown-closing-scale));\n opacity: 0;\n pointer-events: none;\n transition:\n transform var(--dropdown-close-dur) var(--dropdown-ease),\n opacity var(--dropdown-close-dur) var(--dropdown-ease);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .t-dropdown { transition: none !important; }\n}\n\n/* Transitions.dev \u2014 07-panel-reveal.md */\n:root {\n --panel-open-dur: 400ms;\n --panel-close-dur: 350ms;\n --panel-translate-y: 100px;\n --panel-blur: 2px;\n --panel-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n.t-panel-slide {\n transform: translateY(var(--panel-translate-y));\n opacity: 0;\n filter: blur(var(--panel-blur));\n pointer-events: none;\n transition:\n transform var(--panel-close-dur) var(--panel-ease),\n opacity var(--panel-close-dur) var(--panel-ease),\n filter var(--panel-close-dur) var(--panel-ease);\n will-change: transform, opacity, filter;\n}\n.t-panel-slide[data-open=\"true\"] {\n transform: translateY(0);\n opacity: 1;\n filter: blur(0);\n pointer-events: auto;\n transition:\n transform var(--panel-open-dur) var(--panel-ease),\n opacity var(--panel-open-dur) var(--panel-ease),\n filter var(--panel-open-dur) var(--panel-ease);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .t-panel-slide { transition: none !important; }\n}\n\n/* Transitions.dev \u2014 16-tabs-sliding.md */\n:root {\n --tabs-dur: 250ms;\n --tabs-ease: cubic-bezier(0.22, 1, 0.36, 1);\n --tabs-text-muted: rgba(15, 15, 15, 0.8);\n --tabs-text-active: #0f0f0f;\n --tabs-bar-bg: #f1f1f1;\n --tabs-pill-bg: #ffffff;\n}\n\n/* The bar is just a flex container with padding for the pill\n to sit inside. Tabs sit on z-index: 1, the pill on z-index: 0,\n so labels read above the pill background. */\n.t-tabs {\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 3px;\n padding: 3px;\n border-radius: 48px;\n background: var(--tabs-bar-bg);\n}\n.t-tab {\n position: relative;\n appearance: none;\n border: 0;\n background: transparent;\n height: 30px;\n padding: 4px 12px;\n color: var(--tabs-text-muted);\n cursor: pointer;\n border-radius: 48px;\n z-index: 1;\n transition: color var(--tabs-dur) var(--tabs-ease);\n}\n.t-tab:not([aria-selected=\"true\"]):hover,\n.t-tab[aria-selected=\"true\"] {\n color: var(--tabs-text-active);\n}\n\n/* The pill: width + transform are written inline by JS so\n the transition tweens between the previous and next\n measured positions. */\n.t-tabs-pill {\n position: absolute;\n top: 3px;\n left: 0;\n height: 30px;\n width: 0;\n background: var(--tabs-pill-bg);\n border-radius: 48px;\n transform: translateX(0);\n transition:\n transform var(--tabs-dur) var(--tabs-ease),\n width var(--tabs-dur) var(--tabs-ease);\n will-change: transform, width;\n z-index: 0;\n pointer-events: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .t-tabs-pill, .t-tab { transition: none !important; }\n}\n\n/* Transitions.dev \u2014 21-accordion.md */\n:root {\n --acc-expand: 250ms;\n --acc-collapse: 250ms;\n --acc-chevron: 250ms;\n --acc-ease: cubic-bezier(0.22, 1, 0.36, 1);\n}\n\n/* grid-template-rows 0fr \u2192 1fr gives a clean height animation\n with no JS measurement; the inner element clips overflow. */\n.t-acc-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--acc-collapse) var(--acc-ease);\n}\n.t-acc[data-open=\"true\"] .t-acc-panel {\n grid-template-rows: 1fr;\n transition: grid-template-rows var(--acc-expand) var(--acc-ease);\n}\n.t-acc-panel-inner {\n overflow: hidden;\n opacity: 0;\n filter: blur(2px);\n transition:\n opacity var(--acc-collapse) var(--acc-ease),\n filter var(--acc-collapse) var(--acc-ease);\n}\n.t-acc[data-open=\"true\"] .t-acc-panel-inner {\n opacity: 1;\n filter: blur(0);\n transition:\n opacity var(--acc-expand) var(--acc-ease),\n filter var(--acc-expand) var(--acc-ease);\n}\n/* The one disclosure glyph (disclosure.ts): a right chevron that\n turns down when the fold opens; non-scaling-stroke keeps the\n stroke width constant through the turn. */\n.t-acc-chevron {\n display: inline-flex;\n transform: rotate(0deg);\n transform-origin: center;\n transition: transform var(--acc-chevron) var(--acc-ease);\n}\n.t-acc-chevron path { vector-effect: non-scaling-stroke; }\n.t-acc[data-open=\"true\"] .t-acc-chevron {\n transform: rotate(90deg);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .t-acc-panel, .t-acc-panel-inner, .t-acc-chevron {\n transition: none !important;\n }\n}\n";
|
|
@@ -177,22 +177,18 @@ export const TRANSITIONS_CSS = `
|
|
|
177
177
|
opacity var(--acc-expand) var(--acc-ease),
|
|
178
178
|
filter var(--acc-expand) var(--acc-ease);
|
|
179
179
|
}
|
|
180
|
-
/*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
in every browser, unlike CSS \`d:\` morphing (Chromium only).
|
|
184
|
-
The chevron path is symmetric about the 16x16 viewBox
|
|
185
|
-
centre, so the flip lands exactly on the "^"; non-scaling
|
|
186
|
-
-stroke keeps the stroke width constant through the flip. */
|
|
180
|
+
/* The one disclosure glyph (disclosure.ts): a right chevron that
|
|
181
|
+
turns down when the fold opens; non-scaling-stroke keeps the
|
|
182
|
+
stroke width constant through the turn. */
|
|
187
183
|
.t-acc-chevron {
|
|
188
184
|
display: inline-flex;
|
|
189
|
-
transform:
|
|
185
|
+
transform: rotate(0deg);
|
|
190
186
|
transform-origin: center;
|
|
191
187
|
transition: transform var(--acc-chevron) var(--acc-ease);
|
|
192
188
|
}
|
|
193
189
|
.t-acc-chevron path { vector-effect: non-scaling-stroke; }
|
|
194
190
|
.t-acc[data-open="true"] .t-acc-chevron {
|
|
195
|
-
transform:
|
|
191
|
+
transform: rotate(90deg);
|
|
196
192
|
}
|
|
197
193
|
|
|
198
194
|
@media (prefers-reduced-motion: reduce) {
|
package/dist/when-html.d.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export declare function
|
|
1
|
+
/** The viewer's own time zone (a browser's); UTC where there is none to read. */
|
|
2
|
+
export declare function viewerZone(): string;
|
|
3
|
+
/** The one formatter for a time a person reads, on every surface: "16:39" today, "Yesterday 16:39" /
|
|
4
|
+
* "Tomorrow 16:39" beside it, "Sep 28" otherwise ("Sep 28 2025" in another year), counted in `zone`. With `clock`,
|
|
5
|
+
* a date further out keeps its time of day too ("Sep 28 16:39"), for deadlines and histories where the hour matters. */
|
|
6
|
+
export declare function shortWhen(iso: string, now?: Date, zone?: string, clock?: boolean): string;
|
|
7
|
+
/** A flow card's deadline in the viewer's zone, always with its time of day: "No reply by 16:30" today,
|
|
8
|
+
* "No reply by Tomorrow 16:30", "Moves on Oct 2 16:30" further out (a label's trailing " at" reads wrong before a date). */
|
|
9
|
+
export declare function deadlineWords(deadline: {
|
|
10
|
+
at: string;
|
|
11
|
+
label: string;
|
|
12
|
+
}, now?: Date, zone?: string): string;
|
|
13
|
+
/** The exact minute in `zone` ("2026-09-30 16:39", with " UTC" when it is UTC): a time's title, one hover away. */
|
|
14
|
+
export declare function fullWhen(iso: string, zone?: string): string;
|
|
15
|
+
/** Every server-rendered stamp (`whenHtml`'s `<time data-when>`) inside `root`, reworded by the one formatter in the
|
|
16
|
+
* viewer's zone: the same words on a desk and a phone, the exact minute in the title. The server's UTC words stay
|
|
17
|
+
* only without script. Times React writes, relative ages ("3 min ago") and exact stamps (the ledger's seconds) carry
|
|
18
|
+
* no `data-when` and are never touched. */
|
|
19
|
+
export declare function localizeTimes(root: ParentNode, now?: Date, zone?: string): void;
|
|
3
20
|
/** The `<time>` for a stamp: `full` is the words a desk shows ("2026-09-30 16:39 UTC"). Empty for no stamp. */
|
|
4
21
|
export declare function whenHtml(iso: string | null, full: string, now?: Date): string;
|
|
5
22
|
/** "2026-09-30 16:39 UTC" — the desk's words for a stamp. */
|
package/dist/when-html.js
CHANGED
|
@@ -1,34 +1,101 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A time a person reads. A desk shows the full stamp, as before; a phone (760px and narrower) shows the short
|
|
3
3
|
* one — "16:39" today, "Yesterday 16:39", "Sep 28" otherwise — and the full stamp stays in the title. Stamps are
|
|
4
|
-
* UTC to the minute, so the
|
|
4
|
+
* UTC to the minute, so the server counts in UTC; in the browser `localizeTimes` rewords each of these with the same
|
|
5
|
+
* formatter in the viewer's own zone, as the React views do. The CSS that picks one lives in the shared page
|
|
5
6
|
* stylesheet (`.so-when-*` in serve.ts).
|
|
6
7
|
*/
|
|
7
8
|
const escape = (value) => value.replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
8
9
|
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
9
10
|
const DAY_MS = 86_400_000;
|
|
10
|
-
/**
|
|
11
|
-
|
|
11
|
+
/** One `Intl.DateTimeFormat` per zone: building one is the slow part, and every time on a page shares a few zones. */
|
|
12
|
+
const formats = new Map();
|
|
13
|
+
function formatIn(zone) {
|
|
14
|
+
let format = formats.get(zone);
|
|
15
|
+
if (format === undefined) {
|
|
16
|
+
format = new Intl.DateTimeFormat("en-US", { timeZone: zone, year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", hourCycle: "h23" });
|
|
17
|
+
formats.set(zone, format);
|
|
18
|
+
}
|
|
19
|
+
return format;
|
|
20
|
+
}
|
|
21
|
+
/** A stamp's calendar day, clock and month in a time zone ("UTC" on the server, the viewer's own zone in a browser). */
|
|
22
|
+
function partsIn(at, zone) {
|
|
23
|
+
const read = (z) => Object.fromEntries(formatIn(z).formatToParts(at).map(part => [part.type, part.value]));
|
|
24
|
+
let parts;
|
|
25
|
+
try {
|
|
26
|
+
parts = read(zone);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
parts = read("UTC");
|
|
30
|
+
}
|
|
31
|
+
return { year: Number(parts["year"]), month: Number(parts["month"]), day: Number(parts["day"]), clock: `${parts["hour"]}:${parts["minute"]}` };
|
|
32
|
+
}
|
|
33
|
+
const dayNumber = (p) => Math.floor(Date.UTC(p.year, p.month - 1, p.day) / DAY_MS);
|
|
34
|
+
/** The viewer's own time zone (a browser's); UTC where there is none to read. */
|
|
35
|
+
export function viewerZone() {
|
|
36
|
+
try {
|
|
37
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return "UTC";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** The one formatter for a time a person reads, on every surface: "16:39" today, "Yesterday 16:39" /
|
|
44
|
+
* "Tomorrow 16:39" beside it, "Sep 28" otherwise ("Sep 28 2025" in another year), counted in `zone`. With `clock`,
|
|
45
|
+
* a date further out keeps its time of day too ("Sep 28 16:39"), for deadlines and histories where the hour matters. */
|
|
46
|
+
export function shortWhen(iso, now = new Date(), zone = "UTC", clock = false) {
|
|
12
47
|
const at = new Date(iso);
|
|
13
48
|
if (Number.isNaN(at.getTime()))
|
|
14
49
|
return iso;
|
|
15
|
-
const
|
|
16
|
-
const days =
|
|
50
|
+
const then = partsIn(at, zone), today = partsIn(now, zone);
|
|
51
|
+
const days = dayNumber(then) - dayNumber(today);
|
|
17
52
|
if (days === 0)
|
|
18
|
-
return clock;
|
|
53
|
+
return then.clock;
|
|
19
54
|
if (days === -1)
|
|
20
|
-
return `Yesterday ${clock}`;
|
|
55
|
+
return `Yesterday ${then.clock}`;
|
|
21
56
|
if (days === 1)
|
|
22
|
-
return `Tomorrow ${clock}`;
|
|
23
|
-
const date = `${MONTHS[
|
|
24
|
-
return
|
|
57
|
+
return `Tomorrow ${then.clock}`;
|
|
58
|
+
const date = `${MONTHS[then.month - 1]} ${then.day}`;
|
|
59
|
+
return `${then.year === today.year ? date : `${date} ${then.year}`}${clock ? ` ${then.clock}` : ""}`;
|
|
60
|
+
}
|
|
61
|
+
/** A flow card's deadline in the viewer's zone, always with its time of day: "No reply by 16:30" today,
|
|
62
|
+
* "No reply by Tomorrow 16:30", "Moves on Oct 2 16:30" further out (a label's trailing " at" reads wrong before a date). */
|
|
63
|
+
export function deadlineWords(deadline, now = new Date(), zone = viewerZone()) {
|
|
64
|
+
if (Number.isNaN(new Date(deadline.at).getTime()))
|
|
65
|
+
return deadline.label;
|
|
66
|
+
const words = shortWhen(deadline.at, now, zone, true);
|
|
67
|
+
return /^(?:\d|Tomorrow|Yesterday)/.test(words) ? `${deadline.label} ${words}` : `${deadline.label.replace(/ at$/, "")} ${words}`;
|
|
68
|
+
}
|
|
69
|
+
/** The exact minute in `zone` ("2026-09-30 16:39", with " UTC" when it is UTC): a time's title, one hover away. */
|
|
70
|
+
export function fullWhen(iso, zone = "UTC") {
|
|
71
|
+
const at = new Date(iso);
|
|
72
|
+
if (Number.isNaN(at.getTime()))
|
|
73
|
+
return iso;
|
|
74
|
+
const p = partsIn(at, zone);
|
|
75
|
+
return `${p.year}-${String(p.month).padStart(2, "0")}-${String(p.day).padStart(2, "0")} ${p.clock}${zone === "UTC" ? " UTC" : ""}`;
|
|
76
|
+
}
|
|
77
|
+
/** Every server-rendered stamp (`whenHtml`'s `<time data-when>`) inside `root`, reworded by the one formatter in the
|
|
78
|
+
* viewer's zone: the same words on a desk and a phone, the exact minute in the title. The server's UTC words stay
|
|
79
|
+
* only without script. Times React writes, relative ages ("3 min ago") and exact stamps (the ledger's seconds) carry
|
|
80
|
+
* no `data-when` and are never touched. */
|
|
81
|
+
export function localizeTimes(root, now = new Date(), zone = viewerZone()) {
|
|
82
|
+
root.querySelectorAll("time[data-when][datetime]").forEach(node => {
|
|
83
|
+
const iso = node.getAttribute("datetime") ?? "";
|
|
84
|
+
if (iso === "" || Number.isNaN(new Date(iso).getTime()))
|
|
85
|
+
return;
|
|
86
|
+
const words = shortWhen(iso, now, zone), full = fullWhen(iso, zone);
|
|
87
|
+
if (node.title !== full)
|
|
88
|
+
node.title = full;
|
|
89
|
+
if (node.textContent !== words)
|
|
90
|
+
node.textContent = words;
|
|
91
|
+
});
|
|
25
92
|
}
|
|
26
93
|
/** The `<time>` for a stamp: `full` is the words a desk shows ("2026-09-30 16:39 UTC"). Empty for no stamp. */
|
|
27
94
|
export function whenHtml(iso, full, now = new Date()) {
|
|
28
95
|
if (iso === null || iso === "")
|
|
29
96
|
return "";
|
|
30
97
|
const words = escape(full);
|
|
31
|
-
return `<time datetime="${escape(iso)}" title="${words}"><span class="so-when-full">${words}</span><span class="so-when-short">${escape(shortWhen(iso, now))}</span></time>`;
|
|
98
|
+
return `<time data-when datetime="${escape(iso)}" title="${words}"><span class="so-when-full">${words}</span><span class="so-when-short">${escape(shortWhen(iso, now))}</span></time>`;
|
|
32
99
|
}
|
|
33
100
|
/** "2026-09-30 16:39 UTC" — the desk's words for a stamp. */
|
|
34
101
|
export const utcMinute = (iso) => `${iso.slice(0, 16).replace("T", " ")} UTC`;
|
package/dist/work-index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type Store, type TaskState } from './store.js';
|
|
|
2
2
|
import type { AssignmentSnapshot } from './assignment.js';
|
|
3
3
|
import type { WorkAction, WorkSummaryAccess } from './work-summary.js';
|
|
4
4
|
import type { WorkStatus, WorkView } from './workspace-ui.js';
|
|
5
|
-
import { type Ask } from './needs-you.js';
|
|
5
|
+
import { type Ask, type AskChip } from './needs-you.js';
|
|
6
6
|
export type WorkIndexCounts = Record<WorkView, number>;
|
|
7
7
|
export type WorkIndexItem = {
|
|
8
8
|
rootId: string;
|
|
@@ -25,6 +25,8 @@ export type WorkIndexItem = {
|
|
|
25
25
|
status: WorkStatus;
|
|
26
26
|
/** What a Needs you row asks of a person (needs-you.ts); null for every other row. */
|
|
27
27
|
ask: Ask | null;
|
|
28
|
+
/** The row's chip under its group: the specific ask (needs-you.ts askChipOf), or none. */
|
|
29
|
+
chip: AskChip | null;
|
|
28
30
|
primaryAction: WorkAction | null;
|
|
29
31
|
completion: {
|
|
30
32
|
actor: string;
|
package/dist/work-index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { plainReasonOf, replacedWords, stageOfCode, taskStatusOf, workToneOf } f
|
|
|
11
11
|
import { isCheckLevel } from './check-levels.js';
|
|
12
12
|
import { MARKER } from './worktree.js';
|
|
13
13
|
import { withFollowUps } from './result-follow-ups.js';
|
|
14
|
-
import { ASKS, NEED_ASK, NEEDS, failedAttemptSentence, processNeedOf } from './needs-you.js';
|
|
14
|
+
import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf, resultHoldUpSentence } from './needs-you.js';
|
|
15
15
|
import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
|
|
16
16
|
export const WORK_INDEX_PAGE_LIMIT = 40;
|
|
17
17
|
export const WORK_INDEX_MAX_LIMIT = 100;
|
|
@@ -379,7 +379,8 @@ export function workIndexPage(store, now, access, options = {}) {
|
|
|
379
379
|
'proof_verdict',(SELECT COALESCE(machine_verdict,verdict) FROM proof_verdict WHERE run=page_result_id),
|
|
380
380
|
'proof_failed_check',(SELECT verdict='refuted' AND reasons_json LIKE '%approved verification command exited%' FROM proof_verdict WHERE run=page_result_id),'code',code,'needs',needs,'person_need',person_need,'lead_claim',lead_claim,'family_running',family_running,'rank',rank,'ask_rank',ask_rank,'list_order',${ORDER},'sort_at',sort_at,
|
|
381
381
|
'checked_actor',checked_actor,'checked_at',checked_at,'checked_digest',checked_digest,'question_id',question_id,
|
|
382
|
-
'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'
|
|
382
|
+
'dependency_id',dependency_id,'dependency_state',dependency_state,'custody_unresolved',custody_unresolved,'hold_kind',hold_kind,'hold_reason',substr(hold_reason,1,300),'hold_id',hold_id,'live_lease',live_lease IS NOT NULL,'approved',COALESCE(approved,0),
|
|
383
|
+
'plan_changed',scope_digest IS NOT NULL AND digest IS NOT scope_digest,'head_ok',length(head_revision)=40 AND head_revision NOT GLOB '*[^a-f0-9]*','stop_run',stop_run,'unfinished',unfinished,'signed_out',signed_out,
|
|
383
384
|
'question_run',(SELECT run FROM decision WHERE id=page.question_id),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
|
|
384
385
|
'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),
|
|
385
386
|
'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
|
|
@@ -460,7 +461,8 @@ function itemOf(row, principal, probe) {
|
|
|
460
461
|
running: ['Running now', '', null, ''],
|
|
461
462
|
'waiting-decision': ['Needs your decision', 'An unanswered question is waiting.', n(row, 'question_id') ? 'answer-decision' : 'inspect-decisions', 'Answer question'],
|
|
462
463
|
'decision-queue': ['Needs your decision', 'The decision queue is full.', 'inspect-decisions', 'Review open questions'],
|
|
463
|
-
|
|
464
|
+
// Its own reason replaces this below (resultHoldUpSentence).
|
|
465
|
+
'result-needs-attention': ['Needs your decision', '', 'inspect-run', 'Inspect run'],
|
|
464
466
|
'process-needs-attention': ['Needs your decision', 'A process exit is not recorded. Open the attempt to check whether its work has stopped.', 'inspect-run', 'Inspect run'],
|
|
465
467
|
failed: ['Needs your decision', 'The last attempt stopped.', 'retry-task', 'Review and retry'],
|
|
466
468
|
'vanished-run': ['Needs your decision', 'An unfinished attempt has no current live claim.', 'reconcile-run', 'Check the unfinished attempt'],
|
|
@@ -505,6 +507,13 @@ function itemOf(row, principal, probe) {
|
|
|
505
507
|
// A failed task says the latest finished attempt across its whole family, whatever its outcome: its recorded reason, or that none was recorded.
|
|
506
508
|
if (code === 'failed')
|
|
507
509
|
detail = failedAttemptSentence(s(row, 'attempt_reason'));
|
|
510
|
+
// A saved result that can't be accepted yet says its own reason, never one line for every such row.
|
|
511
|
+
const planChanged = code === 'result-needs-attention' && n(row, 'plan_changed') === 1;
|
|
512
|
+
if (code === 'result-needs-attention')
|
|
513
|
+
detail = resultHoldUpSentence({ hold: s(row, 'hold_id') === null ? null : s(row, 'hold_reason') ?? '', planChanged,
|
|
514
|
+
unapproved: n(row, 'approved') !== 1, question: s(row, 'question_id') !== null, running: n(row, 'live_lease') === 1, unfinished: s(row, 'unfinished') !== null,
|
|
515
|
+
noCommit: !['built', 'no-change'].includes(s(row, 'result_outcome') ?? '') || (s(row, 'result_role') === 'builder' && n(row, 'head_ok') !== 1),
|
|
516
|
+
report: s(row, 'result_role') === 'scout' });
|
|
508
517
|
if (code === 'result-needs-attention' && n(row, 'custody_unresolved'))
|
|
509
518
|
detail = 'A process exit is not recorded. Open the result to check whether its work has stopped.';
|
|
510
519
|
if (code === 'result-needs-attention' && ['built', 'no-change'].includes(s(row, 'result_outcome') ?? '')) {
|
|
@@ -573,6 +582,7 @@ function itemOf(row, principal, probe) {
|
|
|
573
582
|
status: { token: `assignment-${assignmentState}`, label, detail, tone: workToneOf(shared.headline),
|
|
574
583
|
action: primaryAction === null ? null : { label: actionLabel, kind: actionCode === 'open-result' ? 'open-result' : 'open-task' }, views, rank: n(row, 'rank') },
|
|
575
584
|
ask: need ? ASKS[n(row, 'ask_rank')] ?? 'unblock' : null,
|
|
585
|
+
chip: need ? askChipOf({ headline: shared.headline, need: shared.need?.key ?? null, planChanged, mismatch: verdict === 'refuted' && n(row, 'proof_failed_check') !== 1, ask: ASKS[n(row, 'ask_rank')] ?? 'unblock' }) : null,
|
|
576
586
|
primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']) } : null,
|
|
577
587
|
evidence: 'recorded' };
|
|
578
588
|
}
|
package/dist/workspace-motion.js
CHANGED
|
@@ -21,7 +21,7 @@ export const WORKSPACE_MOTION_CSS = String.raw `
|
|
|
21
21
|
.t-acc[data-open="false"] > .t-acc-panel { grid-template-rows: 0fr; }
|
|
22
22
|
.t-acc[data-open="false"] > .t-acc-panel > .t-acc-panel-inner { opacity: 0; }
|
|
23
23
|
.t-acc > summary { cursor: pointer; }
|
|
24
|
-
.t-acc > summary .t-acc-chevron { width: 14px; height: 14px; margin-
|
|
24
|
+
.t-acc > summary .t-acc-chevron { width: 14px; height: 14px; margin-right: .375rem; flex: none; }
|
|
25
25
|
.t-acc > summary .t-acc-chevron svg { width: 100%; height: 100%; }
|
|
26
26
|
.t-acc > summary::before, .t-acc > summary::after { content: none !important; }
|
|
27
27
|
.t-acc > summary::-webkit-details-marker { display: none; }
|
|
@@ -60,9 +60,9 @@ export const WORKSPACE_MOTION_SCRIPT = String.raw `
|
|
|
60
60
|
Array.from(details.childNodes).forEach(function(node){if(node!==head)inner.appendChild(node);});
|
|
61
61
|
panel.appendChild(inner);details.appendChild(panel);details.classList.add('t-acc');
|
|
62
62
|
var chevron=document.createElement('span');chevron.className='t-acc-chevron';chevron.setAttribute('aria-hidden','true');
|
|
63
|
-
chevron.innerHTML='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="
|
|
63
|
+
chevron.innerHTML='<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 4L10 8L6 12"/></svg>';
|
|
64
64
|
// Use the existing icon when present, rather than rendering two.
|
|
65
|
-
var old=head.querySelector(':scope > svg');if(old)old.remove();head.
|
|
65
|
+
var old=head.querySelector(':scope > svg');if(old)old.remove();head.prepend(chevron);
|
|
66
66
|
}
|
|
67
67
|
details.dataset.motionReady='true';
|
|
68
68
|
var timer=null,closing=false;
|
package/dist/workspace-ui.d.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* claimed without one.
|
|
18
18
|
*/
|
|
19
19
|
import type { DispatchDiagnosis } from "./dispatch.js";
|
|
20
|
-
import { ACCEPT_NEEDS_REASON } from "./result-acts.js";
|
|
20
|
+
import { ACCEPT_NEEDS_REASON, type AcceptLabel } from "./result-acts.js";
|
|
21
21
|
export { ACCEPT_NEEDS_REASON };
|
|
22
22
|
import { type ProofVerdict } from "./proof.js";
|
|
23
23
|
import type { ReviewRetryState, TaskState } from "./store.js";
|
|
@@ -241,15 +241,15 @@ export declare function lastErrorLineOf(log: string): {
|
|
|
241
241
|
/** The result page's own sentence when the decision is on it. */
|
|
242
242
|
export declare const RESULT_DECISION_SENTENCE = "Review the change, then accept it or ask for changes.";
|
|
243
243
|
/** What the result page's Accept says, from facts it already shows: the
|
|
244
|
-
* recorded checks, the requirements not met, the
|
|
245
|
-
*
|
|
246
|
-
* checks passed; otherwise "Accept without checks" and one line naming
|
|
244
|
+
* recorded checks, the requirements not met, whether the saved proof reads,
|
|
245
|
+
* and what the button posts. "Accept and finish" only when everything is met
|
|
246
|
+
* and the checks passed; otherwise "Accept without checks" and one line naming
|
|
247
|
+
* what. The person's own checks are the page's to add (acceptWithChecksOf).
|
|
247
248
|
* `effect` says what pressing it does, in the action's own terms. */
|
|
248
249
|
export type AcceptFacts = {
|
|
249
250
|
checks: "passed" | "failed" | "not-run" | "off" | "running" | "unavailable" | null;
|
|
250
251
|
unmet: number;
|
|
251
|
-
|
|
252
|
-
/** `complete`: marks the exact result complete; `accept`: records a person's acceptance only. */
|
|
252
|
+
/** `complete`: accepts and finishes the exact result in one request; `accept`: records a person's acceptance only (nothing here can finish it). */
|
|
253
253
|
action: "complete" | "accept";
|
|
254
254
|
/** `pull-request`: one can be opened from the task after; `off`: publishing isn't set up; `other`: neither is said. Accept itself never publishes. */
|
|
255
255
|
publishing: "pull-request" | "off" | "other";
|
|
@@ -257,7 +257,7 @@ export type AcceptFacts = {
|
|
|
257
257
|
proof: boolean;
|
|
258
258
|
};
|
|
259
259
|
export type AcceptWords = {
|
|
260
|
-
label:
|
|
260
|
+
label: AcceptLabel;
|
|
261
261
|
ready: boolean;
|
|
262
262
|
why: string | null;
|
|
263
263
|
effect: string;
|
|
@@ -287,3 +287,39 @@ export type PrimaryDestination = "chat" | "work" | "projects" | "flows" | "setti
|
|
|
287
287
|
export declare function primaryDestinationOf(active: string): PrimaryDestination;
|
|
288
288
|
/** Learning stays in Settings and a useful, closed result disclosure. */
|
|
289
289
|
export declare function learningHtml(view: import('./project-learning.js').LearningView, csrf: string, canManage: boolean, source?: number): string;
|
|
290
|
+
/** The first signed requirement a finished attempt missed (failed or unanswered; a person's own check is not a miss),
|
|
291
|
+
* with the evidence line behind it in plain words. Null when every requirement held. */
|
|
292
|
+
export declare function missedRequirementOf(matrix: readonly {
|
|
293
|
+
id: string;
|
|
294
|
+
statement: string;
|
|
295
|
+
state: string;
|
|
296
|
+
detail: readonly string[];
|
|
297
|
+
}[]): {
|
|
298
|
+
id: string;
|
|
299
|
+
statement: string;
|
|
300
|
+
evidence: string | null;
|
|
301
|
+
} | null;
|
|
302
|
+
/** One milestone of a live build's plan, as the agent last reported it; its words are null where only the recorded
|
|
303
|
+
* progress was read (a list row reads no plan file). */
|
|
304
|
+
export type BuildStep = {
|
|
305
|
+
description: string | null;
|
|
306
|
+
state: "pending" | "current" | "completed" | "blocked";
|
|
307
|
+
note: string | null;
|
|
308
|
+
};
|
|
309
|
+
/** Where a live build is, in one line: "Step 3 of 6: …", or, when a step is blocked, "Stuck on step 4 of 6: <why>" in
|
|
310
|
+
* its place. `stuck` says which step and why; null while nothing is blocked. */
|
|
311
|
+
export type BuildProgress = {
|
|
312
|
+
step: number;
|
|
313
|
+
total: number;
|
|
314
|
+
line: string;
|
|
315
|
+
stuck: {
|
|
316
|
+
step: number;
|
|
317
|
+
why: string | null;
|
|
318
|
+
line: string;
|
|
319
|
+
} | null;
|
|
320
|
+
};
|
|
321
|
+
/** A live build's progress: the step in progress (else the first one not done) as "Step N of M: <step>"; a blocked
|
|
322
|
+
* step replaces it as "Stuck on step N of M: <why>" (its note, else the step itself). Null with no plan steps. */
|
|
323
|
+
export declare function buildProgressOf(steps: readonly BuildStep[] | null | undefined): BuildProgress | null;
|
|
324
|
+
/** Earlier attempts that stopped before the one now running, in one quiet line. */
|
|
325
|
+
export declare function earlierAttemptsWords(count: number): string;
|