toolroll 0.9.85 → 0.9.87
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 +83 -0
- package/dist/agent-fence.d.ts +6 -2
- package/dist/agent-fence.js +3 -2
- package/dist/agents-board.d.ts +12 -0
- package/dist/agents-board.js +26 -0
- package/dist/approval-rules-ui.d.ts +4 -1
- package/dist/approval-rules-ui.js +31 -15
- package/dist/assignment-ui.js +1 -1
- package/dist/assignment.js +4 -3
- package/dist/backup-ui.d.ts +5 -4
- package/dist/backup-ui.js +65 -32
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +57 -54
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +35 -68
- package/dist/browser-crew.js +1 -1
- package/dist/browser-workspace.d.ts +363 -177
- package/dist/browser-workspace.js +10 -9
- package/dist/chat-actions.d.ts +11 -1
- package/dist/chat-actions.js +53 -1
- package/dist/chat-channel.js +1 -0
- package/dist/chat-controls.d.ts +1 -1
- package/dist/chat-controls.js +1 -1
- package/dist/chat-quiet.js +3 -2
- package/dist/chat-settings-page.d.ts +29 -9
- package/dist/chat-settings-page.js +59 -28
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +67 -58
- package/dist/claim.d.ts +3 -0
- package/dist/claim.js +4 -1
- package/dist/contracts/chat-actions.d.ts +9 -0
- package/dist/contracts/chat-actions.js +3 -0
- package/dist/contracts/cli.d.ts +43 -0
- package/dist/contracts/cli.js +4 -0
- package/dist/contracts/console-api.d.ts +4 -1
- package/dist/contracts/console-api.js +4 -1
- package/dist/contracts/flow-propose.d.ts +4 -1
- package/dist/contracts/flow-propose.js +1 -1
- package/dist/contracts/flow.d.ts +41 -0
- package/dist/contracts/flow.js +4 -2
- package/dist/contracts/lead-tools.d.ts +21 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/store-json.d.ts +2 -0
- package/dist/contracts/trigger-payloads.d.ts +20 -0
- package/dist/contracts/trigger-payloads.js +4 -0
- package/dist/coordinator.js +1 -0
- package/dist/credentials-ui.d.ts +5 -3
- package/dist/credentials-ui.js +58 -31
- package/dist/deploy-gate.js +4 -0
- package/dist/design/kit.css +685 -0
- package/dist/design/kit.d.ts +208 -0
- package/dist/design/kit.js +177 -0
- package/dist/design/tokens.css +19 -1
- package/dist/discord-settings.js +8 -4
- package/dist/dispatch.d.ts +3 -0
- package/dist/dispatch.js +4 -1
- package/dist/export-ui.d.ts +6 -4
- package/dist/export-ui.js +18 -8
- package/dist/flow-browser.d.ts +103 -0
- package/dist/flow-browser.js +204 -0
- package/dist/flow-changed.d.ts +14 -0
- package/dist/flow-changed.js +53 -0
- package/dist/flow-code.d.ts +5 -1
- package/dist/flow-code.js +4 -1
- package/dist/flow-engine.js +16 -0
- package/dist/flow-gallery-ui.d.ts +41 -4
- package/dist/flow-gallery-ui.js +95 -63
- package/dist/flow-live.js +9 -1
- package/dist/flow-pull-request.js +15 -7
- package/dist/flow-share.js +3 -1
- package/dist/flow-starters-ui.d.ts +3 -9
- package/dist/flow-starters-ui.js +12 -23
- package/dist/flow-steps.d.ts +9 -1
- package/dist/flow-steps.js +55 -5
- package/dist/flow-triggers.js +55 -0
- package/dist/flow-unattended-words.d.ts +20 -0
- package/dist/flow-unattended-words.js +24 -0
- package/dist/flow-unattended.d.ts +9 -5
- package/dist/flow-unattended.js +45 -23
- package/dist/flows-cli.d.ts +19 -0
- package/dist/flows-cli.js +54 -5
- package/dist/flows-ui.d.ts +12 -6
- package/dist/flows-ui.js +152 -32
- package/dist/flows.js +5 -2
- package/dist/integrations-ui.d.ts +6 -4
- package/dist/integrations-ui.js +34 -40
- package/dist/integrations.d.ts +2 -2
- package/dist/integrations.js +4 -4
- package/dist/kits-ui.d.ts +8 -10
- package/dist/kits-ui.js +29 -33
- package/dist/kits.js +1 -1
- package/dist/knowledge-ui.js +76 -19
- package/dist/lead-tools.js +35 -6
- package/dist/lead-voice.js +16 -5
- package/dist/ledger-view.d.ts +2 -0
- package/dist/ledger-view.js +41 -38
- package/dist/models-ui.d.ts +8 -3
- package/dist/models-ui.js +82 -59
- package/dist/monitoring-ui.d.ts +4 -2
- package/dist/monitoring-ui.js +44 -13
- package/dist/needs-you.d.ts +1 -1
- package/dist/needs-you.js +8 -0
- package/dist/operate-remote-arguments.js +1 -0
- package/dist/operate-remote.js +1 -1
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +249 -6
- package/dist/passwordless.d.ts +1 -1
- package/dist/passwordless.js +1 -1
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +25 -23
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +27 -21
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +14 -1
- package/dist/project-delete-ui.d.ts +4 -2
- package/dist/project-delete-ui.js +135 -126
- package/dist/project-delete.js +7 -3
- package/dist/project-knowledge.d.ts +10 -10
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +16 -3
- package/dist/pull-request-flow.js +35 -4
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +39 -28
- package/dist/recipe-ui.d.ts +4 -2
- package/dist/recipe-ui.js +14 -4
- package/dist/release-adapters.d.ts +3 -0
- package/dist/release-adapters.js +22 -2
- package/dist/release-cli.js +81 -9
- package/dist/release.d.ts +21 -0
- package/dist/release.js +40 -4
- package/dist/releases.js +1 -1
- package/dist/request-budget-ui.d.ts +4 -2
- package/dist/request-budget-ui.js +30 -19
- package/dist/result-follow-ups.js +4 -0
- package/dist/retention-ui.d.ts +4 -3
- package/dist/retention-ui.js +15 -12
- package/dist/scope.d.ts +8 -0
- package/dist/scope.js +13 -0
- package/dist/serve.d.ts +0 -1
- package/dist/serve.js +51 -7
- package/dist/server/agents-computers.d.ts +9 -0
- package/dist/server/agents-computers.js +67 -0
- package/dist/server/chat.js +20 -18
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +19 -9
- package/dist/server/console.css +31 -2
- package/dist/server/flows.js +79 -36
- package/dist/server/home.d.ts +35 -0
- package/dist/server/home.js +104 -0
- package/dist/server/http.d.ts +3 -0
- package/dist/server/http.js +5 -0
- package/dist/server/kit-standalone.d.ts +22 -0
- package/dist/server/kit-standalone.js +69 -0
- package/dist/server/kit.d.ts +405 -0
- package/dist/server/kit.js +361 -0
- package/dist/server/pages.d.ts +2 -1
- package/dist/server/pages.js +213 -174
- package/dist/server/people-tokens.js +26 -85
- package/dist/server/render-history.d.ts +51 -0
- package/dist/server/render-history.js +228 -0
- package/dist/server/render-new-task.d.ts +29 -0
- package/dist/server/render-new-task.js +96 -0
- package/dist/server/render-next.d.ts +63 -0
- package/dist/server/render-next.js +109 -0
- package/dist/server/render-pages.d.ts +35 -190
- package/dist/server/render-pages.js +154 -563
- package/dist/server/render-people.d.ts +24 -3
- package/dist/server/render-people.js +93 -72
- package/dist/server/render-projects.d.ts +103 -0
- package/dist/server/render-projects.js +247 -0
- package/dist/server/render-results.d.ts +5 -24
- package/dist/server/render-results.js +48 -185
- package/dist/server/render-settings.d.ts +5 -74
- package/dist/server/render-settings.js +102 -493
- package/dist/server/render-tasks.d.ts +43 -5
- package/dist/server/render-tasks.js +131 -91
- package/dist/server/route-table.js +31 -5
- package/dist/server/settings.js +276 -255
- package/dist/server/task-views.d.ts +8 -0
- package/dist/server/task-views.js +90 -2
- package/dist/server/tasks.d.ts +18 -0
- package/dist/server/tasks.js +148 -41
- package/dist/server/terminals.js +1 -1
- package/dist/server/welcome.d.ts +22 -0
- package/dist/server/welcome.js +69 -0
- package/dist/settings-groups-ui.d.ts +279 -0
- package/dist/settings-groups-ui.js +615 -0
- package/dist/settings-kit.d.ts +42 -6
- package/dist/settings-kit.js +69 -7
- package/dist/settings-proofs.js +1 -0
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +59 -7
- package/dist/slack-settings.js +12 -5
- package/dist/spend-ui.d.ts +45 -26
- package/dist/spend-ui.js +124 -56
- package/dist/sso-ui.d.ts +5 -2
- package/dist/sso-ui.js +63 -31
- package/dist/storage-ui.d.ts +3 -2
- package/dist/storage-ui.js +56 -50
- package/dist/store.d.ts +182 -5
- package/dist/store.js +512 -38
- package/dist/surface.js +23 -3
- package/dist/task-archive.d.ts +10 -0
- package/dist/task-archive.js +16 -0
- package/dist/task-control.d.ts +1 -1
- package/dist/teams-settings.d.ts +2 -1
- package/dist/teams-settings.js +24 -41
- package/dist/telegram-settings.d.ts +0 -4
- package/dist/telegram-settings.js +52 -25
- package/dist/tidy.d.ts +130 -0
- package/dist/tidy.js +503 -0
- package/dist/toolroll-update-ui.d.ts +8 -4
- package/dist/toolroll-update-ui.js +55 -45
- package/dist/tools-ui.d.ts +5 -3
- package/dist/tools-ui.js +107 -42
- package/dist/work-index.d.ts +24 -0
- package/dist/work-index.js +56 -30
- package/dist/workspace-ui.d.ts +1 -1
- package/dist/workspace-ui.js +8 -4
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { releaseProgressFor, type ShipChecks } from "../store.js";
|
|
1
2
|
import { type Html } from "../html.js";
|
|
2
3
|
import { type AssignmentSnapshot } from '../assignment.js';
|
|
3
4
|
import { type FailureExplanation } from '../needs-you.js';
|
|
@@ -12,6 +13,7 @@ import { type CompletionReceiptView, type ResultDetail, type TaskPullRequest } f
|
|
|
12
13
|
import { taskBody, type MilestoneProgressView, type PlanContractView, type PlanRevisionLedgerView, type RevisionDocView, type RevisionView, type RouteView } from "./render-tasks.js";
|
|
13
14
|
import { type Who } from "./session.js";
|
|
14
15
|
import type { TaskPageExtras } from "./runtime.js";
|
|
16
|
+
import type { BrowserTaskView } from "../browser-workspace.js";
|
|
15
17
|
import type { PlanRevision, TaskRef } from "../store.js";
|
|
16
18
|
import { type Run, type Store, type Task, type TaskState } from "../store.js";
|
|
17
19
|
import { type ReviewFacts } from "../workspace-ui.js";
|
|
@@ -88,3 +90,9 @@ export declare function createTaskViews(runtime: TaskViewsRuntime): {
|
|
|
88
90
|
progressOf: (taskRef: number, currentDocument: string | null) => MilestoneProgressView[] | null;
|
|
89
91
|
resultDetailOf: (run: Run, who: Who, now: Date) => ResultDetail;
|
|
90
92
|
};
|
|
93
|
+
/** A pull request's checks as Ship shows them (src/design/KIT.md timing rows): each check on one elapsed-time axis
|
|
94
|
+
* from the first one's start, its state in the five words' tones, and one line for the whole. Null: no checks read. */
|
|
95
|
+
export declare function shipViewOf(ship: ShipChecks | null, now: Date): NonNullable<BrowserTaskView["ship"]> | null;
|
|
96
|
+
/** A release's steps as its progress view shows them: every step in order on one time axis, the ones not reached
|
|
97
|
+
* queued, and where it stands in one line. Null: this task is no release. */
|
|
98
|
+
export declare function releaseStepsViewOf(progress: ReturnType<typeof releaseProgressFor>, now: Date): NonNullable<BrowserTaskView["release"]> | null;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { feedbackImageProblem } from "../result-actions.js";
|
|
2
2
|
import { parseFeedbackImage } from "../result-review.js";
|
|
3
3
|
import { releaseViewOf } from "../release-view.js";
|
|
4
|
+
import { RELEASE_STEPS } from "../release.js";
|
|
5
|
+
import { releaseProgressFor, shipChecksForTask } from "../store.js";
|
|
4
6
|
import { releaseCiRunningOf } from "../release-ci-hold.js";
|
|
5
7
|
import { releaseObligationsOf } from "../release-recheck.js";
|
|
6
8
|
import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
|
|
@@ -21,6 +23,7 @@ import { newestPullRequestOf, publishingOf, pullRequestBlocker } from '../pull-r
|
|
|
21
23
|
import { structuredHandoffView, terminalDiffView } from "../result-evidence-readers.js";
|
|
22
24
|
import { resultCheckLevel } from '../result-follow-ups.js';
|
|
23
25
|
import { skillsSnapshotHtml, skillTestFeedbackHtml } from "../skills-ui.js";
|
|
26
|
+
import { archiveOf } from "../store.js";
|
|
24
27
|
import { runActivityOf } from "../task-activity.js";
|
|
25
28
|
import { taskFingerprint } from "../task-live.js";
|
|
26
29
|
import { pullRequestFactOf } from '../task-status.js';
|
|
@@ -792,6 +795,10 @@ export function createTaskViews(runtime) {
|
|
|
792
795
|
const presentedData = { ...data, rootId: family?.root.id ?? taskId, rootTitle: family?.root.title ?? data.task.title,
|
|
793
796
|
...(family === null || data.assignment != null ? {} : { history: familyHistory(family) }),
|
|
794
797
|
versionLabel: family !== null && family.current.id !== taskId ? `Viewing ${family.versions.findIndex(one => one.id === taskId) === 0 ? "Original" : `Revision ${family.versions.findIndex(one => one.id === taskId)}`} · ${family.current.state === "running" ? "A newer revision is running" : "A newer revision is current"}` : null };
|
|
798
|
+
// Put away (v145, task-tidy.md): who or what put it away and why, and whether this person can put it away or restore it.
|
|
799
|
+
const archived = archiveOf(runtime.store, taskId);
|
|
800
|
+
presentedData.archived = archived;
|
|
801
|
+
presentedData.canPutAway = who.via === "cookie" && who.role === "approver";
|
|
795
802
|
if (scopeDraft !== undefined)
|
|
796
803
|
presentedData.scopeDraft = scopeDraft;
|
|
797
804
|
if (cancelDraft !== undefined)
|
|
@@ -831,8 +838,12 @@ export function createTaskViews(runtime) {
|
|
|
831
838
|
} })() } } : {}) } : page.workspace?.view;
|
|
832
839
|
// A refusal that keeps a draft (a scope edit, an armed cancel) or asks for the plan editor reopens on Plan, where the
|
|
833
840
|
// draft and its error are; anything else on Activity unless the address named a tab.
|
|
834
|
-
const tab = extras.tab ??
|
|
835
|
-
const
|
|
841
|
+
const tab = extras.tab ?? "plan";
|
|
842
|
+
const onePage = taskView?.kind === "task" ? onePageView(taskView, family, presentedData.family?.runs ?? data.runs.map(run => ({ ...run, taskId: data.task.id })), { ...extras, tab }) : taskView;
|
|
843
|
+
// Ship (its pull request's checks, each on one time axis) and a release's own steps, read from the store now.
|
|
844
|
+
const rootId = family?.root.id ?? taskId;
|
|
845
|
+
const oneTaskView = onePage?.kind === "task" ? { ...onePage, ship: shipViewOf(shipChecksForTask(runtime.store, rootId), runtime.clock()),
|
|
846
|
+
release: releaseStepsViewOf(releaseProgressFor(runtime.store, rootId), runtime.clock()) } : onePage;
|
|
836
847
|
// The Result tab's no-script page, under the task's own (the same HTML its result page serves), and its script.
|
|
837
848
|
if (extras.result != null) {
|
|
838
849
|
page.body = html `${page.body}<section class="task-result-tab" id="task-result" aria-label="Result">${extras.result.html}</section>`;
|
|
@@ -1158,3 +1169,80 @@ function autoReleaseOf(scope) {
|
|
|
1158
1169
|
return scope.approvalBasis === "auto-green" ? { kind: "approved", candidate: scope.candidate }
|
|
1159
1170
|
: scope.approvalBasis === "auto-check" ? { kind: "checking", candidate: scope.candidate } : null;
|
|
1160
1171
|
}
|
|
1172
|
+
/** A pull request's checks as Ship shows them (src/design/KIT.md timing rows): each check on one elapsed-time axis
|
|
1173
|
+
* from the first one's start, its state in the five words' tones, and one line for the whole. Null: no checks read. */
|
|
1174
|
+
export function shipViewOf(ship, now) {
|
|
1175
|
+
if (ship === null || ship.checks.length === 0)
|
|
1176
|
+
return null;
|
|
1177
|
+
const at = (iso) => iso === null ? null : Date.parse(iso);
|
|
1178
|
+
const observed = Date.parse(ship.observedAt);
|
|
1179
|
+
const end = Number.isFinite(observed) ? observed : now.getTime();
|
|
1180
|
+
const starts = ship.checks.map(one => at(one.startedAt)).filter((one) => one !== null && Number.isFinite(one));
|
|
1181
|
+
const origin = starts.length === 0 ? end : Math.min(...starts);
|
|
1182
|
+
const st = (state) => state === "passed" ? "ok" : state === "failed" || state === "cancelled" ? "bad" : state === "running" ? "run" : state === "queued" ? "q" : "ok";
|
|
1183
|
+
const rows = ship.checks.map(one => {
|
|
1184
|
+
const start = at(one.startedAt), done = at(one.completedAt);
|
|
1185
|
+
const from = start ?? origin;
|
|
1186
|
+
return { name: one.name, ...(one.workflow === null || one.workflow === one.name ? {} : { sub: one.workflow }),
|
|
1187
|
+
...(one.state === "skipped" ? { sub: "skipped" } : one.state === "neutral" ? { sub: "neutral" } : {}),
|
|
1188
|
+
st: st(one.state), start: Math.max(0, (from - origin) / 1000), dur: start === null ? 0 : Math.max(0, ((done ?? end) - start) / 1000) };
|
|
1189
|
+
});
|
|
1190
|
+
const passed = ship.checks.filter(one => one.state === "passed" || one.state === "skipped" || one.state === "neutral").length;
|
|
1191
|
+
const failed = ship.checks.filter(one => one.state === "failed" || one.state === "cancelled").length;
|
|
1192
|
+
const running = ship.checks.length - passed - failed;
|
|
1193
|
+
// The overall status is the observation's own (the one merges read); the rows only time each check.
|
|
1194
|
+
// `none` (the pull request closed or merged, or GitHub reported no checks) is never painted as passed.
|
|
1195
|
+
const state = ship.observed === "failing" ? "failed" : ship.observed === "passing" ? "passed" : ship.observed === "running" ? "running" : "none";
|
|
1196
|
+
return {
|
|
1197
|
+
pr: ship.prNumber, head: ship.headSha.slice(0, 7), state, rows,
|
|
1198
|
+
summary: state === "failed" ? (failed > 0 ? `${failed} of ${ship.checks.length} ${ship.checks.length === 1 ? "check" : "checks"} failed` : "Checks are failing on GitHub")
|
|
1199
|
+
: state === "running" ? `${passed} of ${ship.checks.length} checks passed` : state === "passed" ? `All ${ship.checks.length} ${ship.checks.length === 1 ? "check" : "checks"} passed`
|
|
1200
|
+
: "No current checks: the pull request is closed or merged, or GitHub reported none",
|
|
1201
|
+
observedAt: ship.observedAt,
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
/** A release step's name in words (release.ts RELEASE_STEPS). */
|
|
1205
|
+
const RELEASE_STEP_WORDS = {
|
|
1206
|
+
gate: "Release gate", approval: "Approval", check: "Release check", complete: "Complete the task", quiet: "Wait for quiet", deploy: "Deploy to this Mac",
|
|
1207
|
+
link: "Point the CLI at it", ci: "Pull request checks", merge: "Merge into main", tree: "Verify the merged tree", tag: "Tag", publish: "Publish to npm", homebrew: "Homebrew",
|
|
1208
|
+
};
|
|
1209
|
+
/** A release's steps as its progress view shows them: every step in order on one time axis, the ones not reached
|
|
1210
|
+
* queued, and where it stands in one line. Null: this task is no release. */
|
|
1211
|
+
export function releaseStepsViewOf(progress, now) {
|
|
1212
|
+
if (progress === null)
|
|
1213
|
+
return null;
|
|
1214
|
+
const origin = Date.parse(progress.startedAt);
|
|
1215
|
+
const recorded = new Map(progress.steps.map(one => [one.step, one]));
|
|
1216
|
+
const all = RELEASE_STEPS.filter(step => step !== "done");
|
|
1217
|
+
// A release that started part way (one carried under an earlier approval, or resumed from an older journal) shows
|
|
1218
|
+
// the steps from where its own record starts; a step missing between two recorded ones is a write that was lost.
|
|
1219
|
+
const firstAt = all.findIndex(step => recorded.has(step));
|
|
1220
|
+
const lastAt = all.reduce((at, step, index) => recorded.has(step) ? index : at, -1);
|
|
1221
|
+
const steps = firstAt <= 0 ? all : all.slice(firstAt);
|
|
1222
|
+
const rows = steps.map(step => {
|
|
1223
|
+
const one = recorded.get(step);
|
|
1224
|
+
// A step with no record is never painted as done: between recorded ones it says so, after them it is queued.
|
|
1225
|
+
if (one === undefined)
|
|
1226
|
+
return all.indexOf(step) < lastAt ? { name: RELEASE_STEP_WORDS[step] ?? step, sub: "not recorded", st: "q", start: 0, dur: 0 }
|
|
1227
|
+
: { name: RELEASE_STEP_WORDS[step] ?? step, st: "q", start: 0, dur: 0 };
|
|
1228
|
+
const start = Date.parse(one.startedAt), end = one.endedAt === null ? now.getTime() : Date.parse(one.endedAt);
|
|
1229
|
+
const st = one.state === "done" ? "ok" : one.state === "failed" ? "bad" : one.state === "waiting" ? "wait" : "run";
|
|
1230
|
+
return { name: RELEASE_STEP_WORDS[step] ?? step, ...(one.detail === null || one.state === "done" ? {} : { sub: one.detail.slice(0, 160) }), st,
|
|
1231
|
+
start: Number.isFinite(start) && Number.isFinite(origin) ? Math.max(0, (start - origin) / 1000) : 0, dur: Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, (end - start) / 1000) : 0 };
|
|
1232
|
+
});
|
|
1233
|
+
const current = rows.findIndex(row => row.st === "run" || row.st === "wait" || row.st === "bad");
|
|
1234
|
+
// Released only on its own evidence: the last step (Homebrew, after publication) recorded done. A merge (authority
|
|
1235
|
+
// `merged`) is not a release: verifying the tree, the tag and publication still follow it.
|
|
1236
|
+
const done = progress.stopped === null && recorded.get("homebrew")?.state === "done";
|
|
1237
|
+
const at = current === -1 ? null : rows[current];
|
|
1238
|
+
const last = lastAt === -1 ? null : RELEASE_STEP_WORDS[all[lastAt]] ?? all[lastAt];
|
|
1239
|
+
return {
|
|
1240
|
+
tag: progress.tag, version: progress.version, pr: progress.pr,
|
|
1241
|
+
rows,
|
|
1242
|
+
state: progress.stopped !== null ? "failed" : done ? "done" : at?.st === "wait" ? "waiting" : "running",
|
|
1243
|
+
headline: done ? `Released ${progress.version}` : progress.stopped !== null ? `Stopped at ${RELEASE_STEP_WORDS[progress.stopped.step] ?? progress.stopped.step}`
|
|
1244
|
+
: at !== null ? `${at.name} · Step ${current + 1} of ${rows.length}` : last === null ? "Starting" : `${last} done · the next step starts on its own`,
|
|
1245
|
+
stopped: progress.stopped === null ? null : { step: RELEASE_STEP_WORDS[progress.stopped.step] ?? progress.stopped.step, message: progress.stopped.message, at: progress.stopped.at },
|
|
1246
|
+
startedAt: progress.startedAt,
|
|
1247
|
+
};
|
|
1248
|
+
}
|
package/dist/server/tasks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { type Html } from "../html.js";
|
|
3
3
|
import { type ServerResponse } from "node:http";
|
|
4
4
|
import { type Artifact, type Run, type Store } from "../store.js";
|
|
5
|
+
import type { HandlerContext } from './handler-context.js';
|
|
5
6
|
import type { ServerRuntime } from './runtime.js';
|
|
6
7
|
import { type PeekAdmission } from "./render-pages.js";
|
|
7
8
|
import { type CompletedWorkRow, type RankedReviewRow, type ReviewCockpitView } from "./render-results.js";
|
|
@@ -36,3 +37,20 @@ export declare function createTasksHandlers(runtime: ServerRuntime): {
|
|
|
36
37
|
};
|
|
37
38
|
liveRunIds: (rows: readonly (Pick<Run, "id" | "outcome" | "leaseId" | "taskRef">)[]) => Set<number>;
|
|
38
39
|
};
|
|
40
|
+
/** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
|
|
41
|
+
* putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
|
|
42
|
+
* re-proves the person's standing inside its own transaction. */
|
|
43
|
+
export declare function createTidyHandlers(runtime: ServerRuntime): {
|
|
44
|
+
"work.put-away": (ctx: HandlerContext) => Promise<void>;
|
|
45
|
+
"work.put-away.apply": (ctx: HandlerContext) => Promise<void>;
|
|
46
|
+
"task.act.archive": (ctx: HandlerContext) => Promise<void>;
|
|
47
|
+
"task.act.restore": (ctx: HandlerContext) => Promise<void>;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
|
|
51
|
+
* Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
|
|
52
|
+
* address still answers itself.
|
|
53
|
+
*/
|
|
54
|
+
export declare function retiredTasksAddress(route: "board" | "done", url: URL): string | null;
|
|
55
|
+
/** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
|
|
56
|
+
export declare const BOARD_COLUMN_LIMIT = 40;
|
package/dist/server/tasks.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseFeedbackImage, sameFeedbackImage } from "../result-review.js";
|
|
2
|
-
import { dropFeedbackImage, releaseAuthorityOfTask, revokeRelease, runLiveCommands } from "../store.js";
|
|
2
|
+
import { archiveTaskFamily, dropFeedbackImage, releaseAuthorityOfTask, restoreTaskFamily, revokeRelease, runLiveCommands } from "../store.js";
|
|
3
|
+
import { applyTidy, approverStandsNow, familyRepoOf, releaseFactsOf, tidyDays, tidyDigest, tidyPreview, TIDY_DEFAULT_DAYS } from "../tidy.js";
|
|
3
4
|
import { steerCapabilityOf } from "../steer-capabilities.js";
|
|
4
5
|
import { canReveal, localCheckoutOf, revealInFinder } from "../checkout-proof.js";
|
|
5
6
|
/** A form's way back to a flow card ("flow:<flow>:<card>"), read strictly: numbers only, never a path. */
|
|
@@ -10,7 +11,7 @@ function returnedFlow(value) {
|
|
|
10
11
|
import { originOf } from "../task-source.js";
|
|
11
12
|
import { ACP_AGENTS, isAcpProvider } from "../acp-agents.js";
|
|
12
13
|
import { handlersOf } from './handler-registry.js';
|
|
13
|
-
import { closedQuestionWords } from "../decision.js";
|
|
14
|
+
import { closedQuestionWords, LIMITS } from "../decision.js";
|
|
14
15
|
import { DEPRECATED_PAGE } from "../deprecations.js";
|
|
15
16
|
/** tasks handlers, moved without changing their route bodies. */
|
|
16
17
|
import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
|
|
@@ -29,8 +30,11 @@ import { CodingActionError } from '../coding-workspace.js';
|
|
|
29
30
|
import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
30
31
|
import { hasForbiddenControls, validateNote } from "../decision.js";
|
|
31
32
|
import { builderReadyFor, diagnoseTaskDispatch, firstRunBuilderRepo } from "../dispatch.js";
|
|
32
|
-
import { withBuilderStep } from "../first-run.js";
|
|
33
|
-
import {
|
|
33
|
+
import { firstTaskSuggestions, withBuilderStep } from "../first-run.js";
|
|
34
|
+
import { newTaskPage } from "./render-new-task.js";
|
|
35
|
+
import { nextPage } from "./render-next.js";
|
|
36
|
+
import { evidencePack } from "../evidence-pack.js";
|
|
37
|
+
import { decisionPage, evidenceRecordPage, runPage } from "./render-history.js";
|
|
34
38
|
import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../evidence.js";
|
|
35
39
|
import { computeGaps } from "../gaps.js";
|
|
36
40
|
import { limitsView } from "../limits-ui.js";
|
|
@@ -67,11 +71,11 @@ import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
|
|
|
67
71
|
import { requestContext } from "./request-context.js";
|
|
68
72
|
import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
|
|
69
73
|
import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
|
|
70
|
-
import { boardBody, boardWorkers,
|
|
74
|
+
import { putAwayPage, boardBody, boardWorkers, inboxFingerprints, inboxPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
|
|
71
75
|
import { runEfficiencyOf } from "../run-efficiency.js";
|
|
72
76
|
import { timingFor } from "../latency-timeline.js";
|
|
73
|
-
import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge,
|
|
74
|
-
import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf,
|
|
77
|
+
import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, withRefusal } from "./render-results.js";
|
|
78
|
+
import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
|
|
75
79
|
import {} from "./session.js";
|
|
76
80
|
export function createTasksHandlers(runtime) {
|
|
77
81
|
const { identities, store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
|
|
@@ -250,7 +254,7 @@ export function createTasksHandlers(runtime) {
|
|
|
250
254
|
// the database alone: a row reads no check log. A live build's row says which step it is on, or the step it is
|
|
251
255
|
// stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
|
|
252
256
|
// current plan (one saved file, for live rows only), and without it the row names the step by number.
|
|
253
|
-
|
|
257
|
+
const enrich = (page) => ({ ...page, items: page.items.map((item) => {
|
|
254
258
|
if (item.liveRunId !== null) {
|
|
255
259
|
const ref = store.lookupRef(item.activeTaskId);
|
|
256
260
|
const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
|
|
@@ -269,15 +273,25 @@ export function createTasksHandlers(runtime) {
|
|
|
269
273
|
return item;
|
|
270
274
|
const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
|
|
271
275
|
return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
|
|
272
|
-
}) };
|
|
276
|
+
}) });
|
|
277
|
+
work = enrich(work);
|
|
278
|
+
// The Board view: the same index as four columns (needs you, queued, running, finished), never a second classifier.
|
|
279
|
+
const board = url.searchParams.get("layout") !== "board" ? null : (() => {
|
|
280
|
+
const column = (view) => enrich(workIndexPage(store, now, workAccess(), { view, limit: BOARD_COLUMN_LIMIT, project, root: evidenceRoot, ...(state === null ? {} : { state }) }));
|
|
281
|
+
return { needs: column("needs-you"), queued: column("queued"), running: column("running"), finished: column("completed") };
|
|
282
|
+
})();
|
|
273
283
|
const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
|
|
274
284
|
view,
|
|
285
|
+
lens: project,
|
|
286
|
+
all: url.searchParams.get("all") === "1",
|
|
287
|
+
board,
|
|
275
288
|
...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
|
|
276
289
|
work,
|
|
277
290
|
previous: url.searchParams.has('cursor'),
|
|
278
291
|
multiProject: new Set(work.items.map(task => task.repo ?? "")).size > 1,
|
|
279
292
|
now,
|
|
280
293
|
state,
|
|
294
|
+
canPutAway: who.via === "cookie" && who.role === "approver",
|
|
281
295
|
// v105: subscription windows and monthly budgets, for whoever runs the installation.
|
|
282
296
|
limits: store.isInstanceOperator(who.name)
|
|
283
297
|
? limitsView(store.providerLimits(), store.budgets().length === 0 ? [] : store.monthSpendCached(now).budgets, { project: projectName, subagent: id => subagentNamesOf(store.handle).get(id) ?? `Subagent ${id}` }, now)
|
|
@@ -332,6 +346,9 @@ export function createTasksHandlers(runtime) {
|
|
|
332
346
|
deliverable: ref?.deliverable ?? "branch",
|
|
333
347
|
route,
|
|
334
348
|
origin: originOf(store, item.approval.taskId, repo => store.accountCanAccess(who.name, repo)),
|
|
349
|
+
// Whether a remembered password may approve these exact terms: the task page's own preview (task-views.ts
|
|
350
|
+
// approvalFresh), read without minting a second nonce. Display only; the approve POST proves it again.
|
|
351
|
+
remembered: nonce !== "" && runtime.taskViewData(item.approval.taskId, who, null, { mintNonce: false })?.approvalFresh != null,
|
|
335
352
|
csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
|
|
336
353
|
}));
|
|
337
354
|
}
|
|
@@ -586,9 +603,12 @@ export function createTasksHandlers(runtime) {
|
|
|
586
603
|
reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
|
|
587
604
|
return sendScreen(response, 200, reviewPage);
|
|
588
605
|
}
|
|
606
|
+
/** Finished tasks live in the Tasks list's Finished view now (UI revamp wave B): the old address keeps answering, as a
|
|
607
|
+
* GET alias, with a named project carried along. */
|
|
589
608
|
async function showDone(ctx) {
|
|
590
|
-
const {
|
|
591
|
-
|
|
609
|
+
const { url, response } = ctx;
|
|
610
|
+
const named = url.searchParams.get("project");
|
|
611
|
+
return redirectGet(response, `/work?view=completed${named === null || named === "" ? "" : `&project=${encodeURIComponent(named)}`}`);
|
|
592
612
|
}
|
|
593
613
|
/** The old task list, retired into /work (docs/plans/reui-polish.md slice 7): its address keeps answering, as a
|
|
594
614
|
* redirect, so links and bookmarks still land. `state` carries over (an unknown one is still refused); `template`
|
|
@@ -622,7 +642,9 @@ export function createTasksHandlers(runtime) {
|
|
|
622
642
|
const prefill = picked !== null && picked.kind === "task"
|
|
623
643
|
? { title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "), acceptance: acceptanceToLines(picked.acceptance).join("\n") }
|
|
624
644
|
: null;
|
|
625
|
-
|
|
645
|
+
// An idea's link (or any ?text=) puts words in the box: bounded to what the box takes, nothing filed.
|
|
646
|
+
const text = ctx.url.searchParams.get("text");
|
|
647
|
+
return newTaskScreen(ctx, 200, null, prefill, text === null ? null : text.slice(0, 200));
|
|
626
648
|
}
|
|
627
649
|
async function showTask(ctx) {
|
|
628
650
|
const { url, who, response } = ctx;
|
|
@@ -645,7 +667,8 @@ export function createTasksHandlers(runtime) {
|
|
|
645
667
|
// One page per task (docs/plans/one-page-task-and-agent.md): the tab named, and the Result tab's result — the shown
|
|
646
668
|
// version's own, or the exact run `run=` names, which must be a result of that very version.
|
|
647
669
|
const tabWord = url.searchParams.get("tab");
|
|
648
|
-
|
|
670
|
+
// The first tab (Plan, or Result once there is one) unless the address names Activity or Attempts.
|
|
671
|
+
const tab = tabWord === "result" || tabWord === "activity" || tabWord === "attempts" ? tabWord : "plan";
|
|
649
672
|
const runWord = url.searchParams.get("run");
|
|
650
673
|
const named = runWord === null ? null : /^[1-9][0-9]{0,14}$/.test(runWord) ? Number(runWord) : -1;
|
|
651
674
|
if (named === -1)
|
|
@@ -693,22 +716,12 @@ export function createTasksHandlers(runtime) {
|
|
|
693
716
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
694
717
|
return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
|
|
695
718
|
}
|
|
696
|
-
return sendScreen(response, 200,
|
|
719
|
+
return sendScreen(response, 200, evidenceRecordPage(chromeFor(family.root.repo, "tasks"), pack));
|
|
697
720
|
}
|
|
721
|
+
/** Build history lives on Agents now ("Finished in the last …"), and each task keeps its own attempts: the old address
|
|
722
|
+
* keeps answering, as a GET alias. */
|
|
698
723
|
async function showRuns(ctx) {
|
|
699
|
-
|
|
700
|
-
const raw = url.searchParams.get("before");
|
|
701
|
-
let before = null;
|
|
702
|
-
if (raw !== null) {
|
|
703
|
-
if (!/^[1-9][0-9]{0,14}$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
|
|
704
|
-
return refuse(response, who, 400, "that cursor is not a page", "/runs");
|
|
705
|
-
}
|
|
706
|
-
before = Number(raw);
|
|
707
|
-
}
|
|
708
|
-
const rows = store.listRunsBefore(before, RUNS_PAGE, project);
|
|
709
|
-
const verdicts = store.proofVerdictsFor(rows.map(one => one.id));
|
|
710
|
-
const accepted = new Set(rows.filter(one => store.proofAcceptance(one.id) !== null).map(one => one.id));
|
|
711
|
-
return sendScreen(response, 200, runsPage(chromeFor(project, "runs"), rows, liveRunIds(rows), rows.length === RUNS_PAGE ? rows[rows.length - 1]?.id ?? null : null, verdicts, accepted));
|
|
724
|
+
return redirectGet(ctx.response, "/agents");
|
|
712
725
|
}
|
|
713
726
|
async function showRun(ctx) {
|
|
714
727
|
const { url, who, response, now, project } = ctx;
|
|
@@ -871,21 +884,12 @@ export function createTasksHandlers(runtime) {
|
|
|
871
884
|
return redirect(ctx.response, "/flows");
|
|
872
885
|
}
|
|
873
886
|
async function showDecision(ctx) {
|
|
874
|
-
const { url, who, response, now
|
|
887
|
+
const { url, who, response, now } = ctx;
|
|
875
888
|
const one = /^\/d\/([0-9]{1,15})$/.exec(url.pathname);
|
|
876
889
|
if (one === null)
|
|
877
890
|
return noPage(who, response);
|
|
878
|
-
const decision = store.getDecision(Number(one[1]));
|
|
879
|
-
if (decision === null)
|
|
880
|
-
return refuse(response, who, 404, "no such decision");
|
|
881
|
-
const run = store.getRun(decision.run);
|
|
882
|
-
if (run !== null && !visible(taskRepoOf(run.taskRef))) {
|
|
883
|
-
return refuse(response, who, 404, "no such decision");
|
|
884
|
-
}
|
|
885
|
-
const taskId = taskOf(store, decision);
|
|
886
891
|
const decisionReturn = url.searchParams.get("return");
|
|
887
|
-
|
|
888
|
-
return sendScreen(response, 200, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), who, now, back));
|
|
892
|
+
return decisionScreen(ctx, Number(one[1]), decisionReturn === null ? null : safeChatReturn(decisionReturn), null, 200, now, null);
|
|
889
893
|
}
|
|
890
894
|
async function showDecisionEvidence(ctx) {
|
|
891
895
|
const { url, who, response } = ctx;
|
|
@@ -1250,7 +1254,12 @@ export function createTasksHandlers(runtime) {
|
|
|
1250
1254
|
// Irreversible options never ride one accidental tap: the form arms
|
|
1251
1255
|
// them behind an explicit confirmation field, and the server checks —
|
|
1252
1256
|
// the client rendering is convenience, this is the rule.
|
|
1257
|
+
// The question page's own form hears a refusal in its box, with nothing changed; /next keeps its refusal page.
|
|
1258
|
+
const inPlace = requestedReturn !== "next";
|
|
1259
|
+
const pageBack = requestedReturn === null ? null : decisionBack;
|
|
1253
1260
|
if (chosen !== undefined && !chosen.reversible && body.get("confirm") !== "yes") {
|
|
1261
|
+
if (inPlace)
|
|
1262
|
+
return decisionScreen(ctx, id, pageBack, `“${chosen.label}” can't be undone: tick the box to confirm it`, 400, now, { choice, note: body.get("note") });
|
|
1254
1263
|
return refuse(response, who, 400, "an irreversible choice must be confirmed", requestedReturn === null ? `/d/${id}` : `/d/${id}?return=${encodeURIComponent(decisionBack)}`);
|
|
1255
1264
|
}
|
|
1256
1265
|
const note = body.get("note");
|
|
@@ -1265,6 +1274,9 @@ export function createTasksHandlers(runtime) {
|
|
|
1265
1274
|
const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
|
|
1266
1275
|
const closed = answered.reason === "closed" ? store.getDecision(id)?.supersededReason ?? null : null;
|
|
1267
1276
|
const why = closed !== null ? closedQuestionWords(closed) : answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
|
|
1277
|
+
if (inPlace && answered.reason !== "closed" && answered.reason !== "already-answered") {
|
|
1278
|
+
return decisionScreen(ctx, id, pageBack, answered.reason === "bad-option" ? "Choose one of the options" : answered.reason === "bad-note" ? `The note is too long or can't be sent (up to ${LIMITS.note} characters)` : why, status, now, { choice, note });
|
|
1279
|
+
}
|
|
1268
1280
|
return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
|
|
1269
1281
|
}
|
|
1270
1282
|
return redirect(response, decisionBack);
|
|
@@ -3057,7 +3069,24 @@ export function createTasksHandlers(runtime) {
|
|
|
3057
3069
|
live.add(run.id);
|
|
3058
3070
|
return live;
|
|
3059
3071
|
}
|
|
3072
|
+
const tidyHandlers = createTidyHandlers(runtime);
|
|
3073
|
+
/** The question page, with a refusal in its box when an answer was refused (the box keeps the choice to make). */
|
|
3074
|
+
function decisionScreen(ctx, id, back, refusal, code, now, draft) {
|
|
3075
|
+
const { who, response, project } = ctx;
|
|
3076
|
+
const decision = store.getDecision(id);
|
|
3077
|
+
if (decision === null)
|
|
3078
|
+
return refuse(response, who, 404, "no such decision");
|
|
3079
|
+
const run = store.getRun(decision.run);
|
|
3080
|
+
if (run !== null && !visible(taskRepoOf(run.taskRef))) {
|
|
3081
|
+
return refuse(response, who, 404, "no such decision");
|
|
3082
|
+
}
|
|
3083
|
+
const taskId = taskOf(store, decision);
|
|
3084
|
+
const ref = taskId === "?" ? null : store.lookupRef(taskId);
|
|
3085
|
+
const place = { title: ref === null ? null : store.getTask(taskId)?.title ?? null, repo: ref?.repo ?? null, run: run === null ? null : { id: run.id, role: run.role } };
|
|
3086
|
+
return sendScreen(response, code, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), now, place, back, refusal, draft));
|
|
3087
|
+
}
|
|
3060
3088
|
const registrations = handlersOf("tasks", {
|
|
3089
|
+
...tidyHandlers,
|
|
3061
3090
|
"code.page": showCode("workspace"),
|
|
3062
3091
|
"code.session": showCode("session"),
|
|
3063
3092
|
"code.state": showCode("state"),
|
|
@@ -3066,12 +3095,13 @@ export function createTasksHandlers(runtime) {
|
|
|
3066
3095
|
"code.changes-read": showCode("changes", true),
|
|
3067
3096
|
"code.ship-preview": showCode("ship", true),
|
|
3068
3097
|
"code.other": showCode("unknown"),
|
|
3098
|
+
// Retired into Tasks and the task page (src/design/KIT.md): each old address still answers, as a redirect.
|
|
3069
3099
|
"inbox": showInbox,
|
|
3070
3100
|
"work": showWork,
|
|
3071
3101
|
"next": showNext,
|
|
3072
3102
|
"board": showBoard,
|
|
3073
3103
|
"review": showReview,
|
|
3074
|
-
"done": showDone,
|
|
3104
|
+
"done": async (ctx) => { const to = retiredTasksAddress("done", ctx.url); return to === null ? showDone(ctx) : redirect(ctx.response, to); },
|
|
3075
3105
|
"tasks": showTasks,
|
|
3076
3106
|
"queue": showQueue,
|
|
3077
3107
|
"tasks.new": showNewTask,
|
|
@@ -3138,7 +3168,7 @@ export function createTasksHandlers(runtime) {
|
|
|
3138
3168
|
});
|
|
3139
3169
|
/** The add page, once more: blank, from a shipped template (`?template=`, as the old task list did), or with a refused
|
|
3140
3170
|
* filing's own words and status (the draft keeps the project revision it was written against). */
|
|
3141
|
-
function newTaskScreen(ctx, status, problem, draft) {
|
|
3171
|
+
function newTaskScreen(ctx, status, problem, draft, text = null) {
|
|
3142
3172
|
const { who, response, project } = ctx;
|
|
3143
3173
|
const chainable = store
|
|
3144
3174
|
.listTasksScoped(project, undefined, 100, null)
|
|
@@ -3150,7 +3180,84 @@ export function createTasksHandlers(runtime) {
|
|
|
3150
3180
|
chainable.unshift({ id: after, title: "(as submitted)" });
|
|
3151
3181
|
const chrome = chromeFor(project, "tasks");
|
|
3152
3182
|
const revision = draft?.values !== undefined ? undefined : who.via === "cookie" ? who.session.projectRevision : 0;
|
|
3153
|
-
|
|
3183
|
+
// Ideas for the project the box starts on: what the first run found there (issues, TODOs), else the safe generic
|
|
3184
|
+
// ones while the first run is open; none once someone has filed work and nothing was found.
|
|
3185
|
+
const startsOn = draft?.values?.get("repo") ?? project ?? chrome.projects?.[0]?.path ?? null;
|
|
3186
|
+
const found = startsOn === null ? null : runtime.firstTasks.get(startsOn)?.found ?? null;
|
|
3187
|
+
const ideas = found ?? (runtime.firstRunStepsNow(ctx.now) !== null && !store.hasAnyWork() ? firstTaskSuggestions({ issues: [], todos: [] }) : []);
|
|
3188
|
+
return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft, text, ideas));
|
|
3154
3189
|
}
|
|
3155
3190
|
return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
|
|
3156
3191
|
}
|
|
3192
|
+
/** Put away and Restore one task, and Put away old tasks (docs/plans/task-tidy.md): the preview reads only (GET-quiet);
|
|
3193
|
+
* putting away many is an approver's step-up bound to the digest of exactly what the preview showed. Each write
|
|
3194
|
+
* re-proves the person's standing inside its own transaction. */
|
|
3195
|
+
export function createTidyHandlers(runtime) {
|
|
3196
|
+
const { store, sendScreen, chromeFor, authenticateApprover, workAccess, bustBadge } = runtime;
|
|
3197
|
+
async function showPutAway(ctx) {
|
|
3198
|
+
const { url, who, response, now, project } = ctx;
|
|
3199
|
+
const days = tidyDays(url.searchParams.get("days"));
|
|
3200
|
+
if (days === null)
|
|
3201
|
+
return refuse(response, who, 400, "Choose a number of days from 1 to 365.", "/work/put-away");
|
|
3202
|
+
const releases = releaseFactsOf(store);
|
|
3203
|
+
const items = tidyPreview(store, now, { releases, days, access: workAccess() });
|
|
3204
|
+
return sendScreen(response, 200, putAwayPage(chromeFor(project, "work"), { items, days, digest: tidyDigest(items, days), now,
|
|
3205
|
+
problem: url.searchParams.get("problem"), releasesSkipped: releases.problem }));
|
|
3206
|
+
}
|
|
3207
|
+
async function applyPutAway(ctx) {
|
|
3208
|
+
const { who, response, now, posted } = ctx;
|
|
3209
|
+
const body = readForm(posted, CONSOLE_FORMS.tasksPutAway);
|
|
3210
|
+
const days = tidyDays(body.get("days"));
|
|
3211
|
+
const back = (problem) => redirect(response, `/work/put-away?${new URLSearchParams({ ...(days === null || days === TIDY_DEFAULT_DAYS ? {} : { days: String(days) }), problem })}`);
|
|
3212
|
+
if (days === null)
|
|
3213
|
+
return back("Choose a number of days from 1 to 365.");
|
|
3214
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
3215
|
+
return back("Enter your Toolroll password to put these away.");
|
|
3216
|
+
const done = applyTidy(store, now, { by: who.name, days, digest: body.get("digest") ?? "", only: body.getAll("task"), releases: releaseFactsOf(store), access: workAccess() });
|
|
3217
|
+
if (!done.ok)
|
|
3218
|
+
return back(done.message);
|
|
3219
|
+
bustBadge();
|
|
3220
|
+
return redirect(response, "/work?view=archived");
|
|
3221
|
+
}
|
|
3222
|
+
const oneTask = (kind) => async (ctx) => {
|
|
3223
|
+
const { url, who, response, now, posted } = ctx;
|
|
3224
|
+
const act = matchTaskPath(url.pathname, `/${kind}$`);
|
|
3225
|
+
if (act === null)
|
|
3226
|
+
return refuse(response, who, 404, "There's no page at this address.", "/work");
|
|
3227
|
+
const body = readForm(posted, kind === "archive" ? CONSOLE_FORMS.taskArchive : CONSOLE_FORMS.taskRestore);
|
|
3228
|
+
const standing = (refs) => approverStandsNow(store, who.name, familyRepoOf(store, refs));
|
|
3229
|
+
const done = kind === "archive"
|
|
3230
|
+
? archiveTaskFamily(store, act.taskId, { by: who.name, byKind: "person", rule: null, reason: (body.get("reason") ?? "").trim() || "Put away by hand" }, now, { guard: standing })
|
|
3231
|
+
: restoreTaskFamily(store, act.taskId, who.name, now, standing);
|
|
3232
|
+
if (!done.ok)
|
|
3233
|
+
return refuse(response, who, done.reason === "unknown-task" ? 404 : 409, done.message, taskHref(act.taskId));
|
|
3234
|
+
bustBadge();
|
|
3235
|
+
const to = safeReturn(body.get("return"));
|
|
3236
|
+
return redirect(response, to === "/" ? taskHref(act.taskId) : to);
|
|
3237
|
+
};
|
|
3238
|
+
return { "work.put-away": showPutAway, "work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
|
|
3239
|
+
}
|
|
3240
|
+
/** A retired page's GET alias: a plain 302 (the shared 303 helper belongs to POST landings), as pages.ts morningAlias. */
|
|
3241
|
+
function redirectGet(response, to) {
|
|
3242
|
+
response.writeHead(302, { ...SAFETY, Location: to });
|
|
3243
|
+
response.end();
|
|
3244
|
+
}
|
|
3245
|
+
/**
|
|
3246
|
+
* Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
|
|
3247
|
+
* Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
|
|
3248
|
+
* address still answers itself.
|
|
3249
|
+
*/
|
|
3250
|
+
export function retiredTasksAddress(route, url) {
|
|
3251
|
+
// (The Board keeps its own page for now: Tasks' Board view is where the app links; see the wave A report.)
|
|
3252
|
+
const project = url.searchParams.get("project");
|
|
3253
|
+
const keep = project === null || url.searchParams.getAll("project").length !== 1 ? "" : `project=${encodeURIComponent(project)}&`;
|
|
3254
|
+
switch (route) {
|
|
3255
|
+
case "done": return `/work?${keep}view=completed`;
|
|
3256
|
+
case "board":
|
|
3257
|
+
if (url.searchParams.get("view") === "order" || url.searchParams.get("fragment") === "1")
|
|
3258
|
+
return null;
|
|
3259
|
+
return url.searchParams.get("scope") === "all" ? "/work?all=1&layout=board" : `/work?${keep}layout=board`;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
/** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
|
|
3263
|
+
export const BOARD_COLUMN_LIMIT = 40;
|
package/dist/server/terminals.js
CHANGED
|
@@ -78,7 +78,7 @@ export function createTerminalHandlers(runtime, deps) {
|
|
|
78
78
|
const id = TERMINAL_ID.exec(url.pathname)?.[1] ?? "";
|
|
79
79
|
const nonce = randomBytes(16).toString("base64");
|
|
80
80
|
const host = request.headers.host ?? "";
|
|
81
|
-
const document = html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"><meta name="theme-color" content="#0C0E12"><meta name="apple-mobile-web-app-capable" content="yes"><title>
|
|
81
|
+
const document = html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"><meta name="theme-color" content="#0C0E12"><meta name="apple-mobile-web-app-capable" content="yes"><title>Terminals — Toolroll</title><link rel="icon" href="/icon.svg"><link rel="manifest" href="/manifest.webmanifest"><link rel="stylesheet" href="/assets/terminal.css"></head><body><div id="so-term"></div><div id="so-term-boot" hidden data-csrf="${who.session.csrf}" data-session="${id}" data-passwordless="${passwordlessOn(store) ? "1" : ""}"></div><script type="module" src="/assets/terminal.js" nonce="${nonce}"></script></body></html>`;
|
|
82
82
|
response.writeHead(200, {
|
|
83
83
|
"Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer",
|
|
84
84
|
// The socket is this page's own host: named outright, since older WebKit doesn't read 'self' as ws:/wss:.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FirstRunStep } from "../first-run.js";
|
|
2
|
+
import { type CheckStep } from "./kit.js";
|
|
3
|
+
import type { ServerRuntime } from "./runtime.js";
|
|
4
|
+
import { type Chrome, type Screen } from "./chrome.js";
|
|
5
|
+
export type WelcomeFacts = {
|
|
6
|
+
steps: readonly FirstRunStep[];
|
|
7
|
+
projects: number;
|
|
8
|
+
/** This person has a live push enrollment on some device. */
|
|
9
|
+
alerts: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** The five steps as the page shows them, and which one leads. */
|
|
12
|
+
export declare function welcomeSteps(facts: WelcomeFacts): {
|
|
13
|
+
steps: CheckStep[];
|
|
14
|
+
primary: {
|
|
15
|
+
label: string;
|
|
16
|
+
href: string;
|
|
17
|
+
} | null;
|
|
18
|
+
};
|
|
19
|
+
export declare function welcomePage(chrome: Chrome, facts: WelcomeFacts): Screen;
|
|
20
|
+
export declare function createWelcomeHandlers(runtime: ServerRuntime): {
|
|
21
|
+
registrations: import("./handler-registry.js").Registration[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first run's own page, /welcome (Wave E, src/design/KIT.md): five guided steps to a first result, each derived
|
|
3
|
+
* from live facts on every render (never a stored cursor), each with the one action that does it. The next required
|
|
4
|
+
* step's action is the screen's one primary (on a fresh install: Add a project); alerts on the phone are optional and
|
|
5
|
+
* never lead. It only reads: every step's action is a link to the page that does it. Once the first result has
|
|
6
|
+
* arrived (or for a project-limited account, or in the demo) the first run is over and the address leads Home.
|
|
7
|
+
*/
|
|
8
|
+
import { builderReadyFor, firstRunBuilderRepo } from "../dispatch.js";
|
|
9
|
+
import { withBuilderStep } from "../first-run.js";
|
|
10
|
+
import { html } from "../html.js";
|
|
11
|
+
import { START_BUILDER_HREF } from "../needs-you.js";
|
|
12
|
+
import { handlersOf } from "./handler-registry.js";
|
|
13
|
+
import { redirect } from "./http.js";
|
|
14
|
+
import { btn, checklist, dock, pageHeader } from "./kit.js";
|
|
15
|
+
import { screen } from "./chrome.js";
|
|
16
|
+
const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
|
|
17
|
+
/** The five steps as the page shows them, and which one leads. */
|
|
18
|
+
export function welcomeSteps(facts) {
|
|
19
|
+
const step = (key) => facts.steps.find(one => one.key === key);
|
|
20
|
+
const agent = step("agent"), builder = step("builder"), task = step("task");
|
|
21
|
+
const projectDone = facts.projects > 0;
|
|
22
|
+
const agentDone = (agent?.done ?? false) && (builder?.done ?? true);
|
|
23
|
+
const command = agent?.action?.kind === "command" ? agent.action.command : null;
|
|
24
|
+
const agentState = agentDone ? "done" : agent?.checking === true ? "checking" : "open";
|
|
25
|
+
const agentText = agentDone ? "An agent is signed in and a builder is ready."
|
|
26
|
+
: agent?.checking === true ? "Checking for a signed-in agent on this computer…"
|
|
27
|
+
: agent !== undefined && !agent.done && command !== null ? html `On the computer running Toolroll, run <code class="k-mono">${command}</code>.`
|
|
28
|
+
: "Start a builder so tasks can run.";
|
|
29
|
+
const agentLink = agentDone || agent?.checking === true ? null
|
|
30
|
+
: agent !== undefined && !agent.done ? { label: "Agent settings", href: "/settings/models" } : { label: "Start a builder", href: START_BUILDER_HREF };
|
|
31
|
+
const taskDone = task?.done ?? false;
|
|
32
|
+
// The next required step leads (alerts are optional and never do).
|
|
33
|
+
const order = [
|
|
34
|
+
{ key: "project", open: !projectDone, link: { label: "Add a project", href: "/projects" } },
|
|
35
|
+
{ key: "agent", open: agentState === "open", link: agentLink },
|
|
36
|
+
{ key: "task", open: !taskDone && projectDone, link: { label: "New task", href: "/tasks/new" } },
|
|
37
|
+
];
|
|
38
|
+
const lead = order.find(one => one.open && one.link !== null) ?? null;
|
|
39
|
+
const action = (key, link) => link === null ? null : btn(link.label, { sm: true, href: link.href, ...(lead?.key === key ? { variant: "primary", docked: true } : {}) });
|
|
40
|
+
const steps = [
|
|
41
|
+
{ key: "password", title: "Your password", text: "You're signed in. Your first login is saved on the computer running Toolroll.", state: "done" },
|
|
42
|
+
{ key: "project", title: "Add a project", text: projectDone ? plural(facts.projects, "project added.", "projects added.") : "A git repository agents work in.", state: projectDone ? "done" : "open", action: action("project", order[0].link) },
|
|
43
|
+
{ key: "agent", title: "Agent and models", text: agentText, state: agentState, action: action("agent", agentLink) },
|
|
44
|
+
{ key: "phone", title: "Alerts on your phone", text: facts.alerts ? "Alerts are on." : "Optional. Hear when something needs you.", state: facts.alerts ? "done" : "open", action: btn("Set up alerts", { sm: true, href: "/settings/alerts#this-device" }) },
|
|
45
|
+
{ key: "task", title: "Your first task", text: taskDone ? "Filed. Its plan waits for your approval." : "Describe a change and approve its plan.", state: taskDone ? "done" : "open",
|
|
46
|
+
action: projectDone ? action("task", order[2].link) : null },
|
|
47
|
+
];
|
|
48
|
+
return { steps, primary: lead?.link ?? null };
|
|
49
|
+
}
|
|
50
|
+
export function welcomePage(chrome, facts) {
|
|
51
|
+
const { steps, primary } = welcomeSteps(facts);
|
|
52
|
+
const left = steps.filter(one => one.state !== "done").length;
|
|
53
|
+
const body = html `${pageHeader("Welcome")}<div class="k-doc"><div class="k-hello"><h2>Get to your first result.</h2><p>${left === 0 ? "Setup is done." : `${plural(left, "step", "steps")} left.`}</p></div>\
|
|
54
|
+
${checklist(steps, "Setup steps")}</div>${dock(primary === null ? null : btn(primary.label, { variant: "primary", href: primary.href }))}`;
|
|
55
|
+
return screen("Welcome", body, { chrome });
|
|
56
|
+
}
|
|
57
|
+
export function createWelcomeHandlers(runtime) {
|
|
58
|
+
const { store, sendScreen, chromeFor, firstRunStepsNow, managedRepos, visible } = runtime;
|
|
59
|
+
async function showWelcome(ctx) {
|
|
60
|
+
const { who, response, now, project } = ctx;
|
|
61
|
+
const steps = withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, managedRepos()), now));
|
|
62
|
+
// The first run is over (a first result, a project-limited account, the demo): the address leads Home.
|
|
63
|
+
if (steps === null)
|
|
64
|
+
return redirect(response, "/");
|
|
65
|
+
const alerts = who.via === "cookie" && store.listPushSubscriptions(who.name).some(one => one.retiredAt === null);
|
|
66
|
+
return sendScreen(response, 200, welcomePage(chromeFor(project, "work"), { steps, projects: managedRepos().length, alerts }));
|
|
67
|
+
}
|
|
68
|
+
return { registrations: handlersOf("tasks", { welcome: showWelcome }) };
|
|
69
|
+
}
|