toolroll 0.9.101 → 0.9.103
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 +13 -0
- package/dist/browser/terminal.js +9 -9
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-workspace.d.ts +51 -14
- package/dist/browser-workspace.js +5 -3
- package/dist/flow-engine.d.ts +3 -0
- package/dist/flow-engine.js +4 -1
- package/dist/flows-ui.d.ts +31 -1
- package/dist/flows-ui.js +40 -12
- package/dist/page-work-contract.d.ts +21 -0
- package/dist/page-work-contract.js +5 -0
- package/dist/project-knowledge.d.ts +9 -9
- package/dist/release-approval.d.ts +9 -20
- package/dist/release-approval.js +14 -53
- package/dist/release-refusal.d.ts +32 -0
- package/dist/release-refusal.js +73 -0
- package/dist/run-efficiency.d.ts +3 -2
- package/dist/run-efficiency.js +4 -3
- package/dist/scope.js +6 -0
- package/dist/serve.js +51 -9
- package/dist/server/chrome.d.ts +10 -1
- package/dist/server/chrome.js +9 -0
- package/dist/server/console.css +19 -4
- package/dist/server/home-asks.d.ts +45 -0
- package/dist/server/home-asks.js +108 -0
- package/dist/server/home.d.ts +37 -14
- package/dist/server/home.js +113 -63
- package/dist/server/http.d.ts +6 -0
- package/dist/server/http.js +8 -0
- package/dist/server/in-place.d.ts +15 -0
- package/dist/server/in-place.js +32 -0
- package/dist/server/pages.js +36 -3
- package/dist/server/render-tasks.js +14 -3
- package/dist/server/request-context.d.ts +14 -0
- package/dist/server/request-context.js +3 -0
- package/dist/server/task-views.js +6 -3
- package/dist/server/tasks.d.ts +8 -2
- package/dist/server/tasks.js +31 -17
- package/dist/work-index.d.ts +19 -1
- package/dist/work-index.js +40 -14
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a release can't be approved now, from the plane's own state (release-approval.ts re-exports all of it): the
|
|
3
|
+
* coordinator's GitHub CI reading on the exact candidate, and the one list of refusals the release sheet shows and every
|
|
4
|
+
* approval road applies (scope.ts approve() reads `releaseApprovalRefusal` inside its transaction). Kept apart from
|
|
5
|
+
* release-approval.ts so scope.ts reads it without importing the card machinery (an import cycle through store.ts).
|
|
6
|
+
*/
|
|
7
|
+
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
8
|
+
import { taskArchived } from "./task-archive.js";
|
|
9
|
+
import { cleanTerminalLine } from "./terminal-text.js";
|
|
10
|
+
/** The ledger act holding the coordinator's latest reading of the pull request's CI on the exact candidate. */
|
|
11
|
+
export const RELEASE_CI_SEEN_ACTION = "release CI observed";
|
|
12
|
+
export const ciName = (value) => cleanTerminalLine(String(value ?? "")).trim().slice(0, 120);
|
|
13
|
+
/** The coordinator's latest CI reading for this task on exactly `candidate`, or null (not read yet, or another commit). */
|
|
14
|
+
export function releaseCiOf(store, taskId, candidate) {
|
|
15
|
+
const row = store.handle.prepare("SELECT detail, at FROM action_ledger WHERE task_id = ? AND action = ? ORDER BY id DESC LIMIT 1").get(taskId, RELEASE_CI_SEEN_ACTION);
|
|
16
|
+
if (row === undefined)
|
|
17
|
+
return null;
|
|
18
|
+
let parsed;
|
|
19
|
+
try {
|
|
20
|
+
parsed = JSON.parse(String(row["detail"]));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const one = parsed;
|
|
26
|
+
if (one.v !== 1 || one.candidate !== candidate || !["passed", "running", "failed"].includes(String(one.state)) || !Array.isArray(one.failed) || !Array.isArray(one.runs))
|
|
27
|
+
return null;
|
|
28
|
+
return { candidate, at: String(row["at"]), state: one.state, failed: one.failed.map(ciName),
|
|
29
|
+
runs: one.runs.map(run => { const r = run; return { id: typeof r["id"] === "number" ? r["id"] : null, name: ciName(r["name"]), status: ciName(r["status"]), conclusion: r["conclusion"] == null ? null : ciName(r["conclusion"]) }; }) };
|
|
30
|
+
}
|
|
31
|
+
/** GitHub CI in one line, as the sheet and the CLI say it. */
|
|
32
|
+
export function ciWords(ci, candidate) {
|
|
33
|
+
const short = candidate.slice(0, 7);
|
|
34
|
+
if (ci === null)
|
|
35
|
+
return `GitHub CI on ${short} hasn't been read yet.`;
|
|
36
|
+
if (ci.state === "passed")
|
|
37
|
+
return `GitHub CI passed on ${short}.`;
|
|
38
|
+
if (ci.state === "failed")
|
|
39
|
+
return `GitHub CI failed on ${short}: ${ci.failed.join(", ") || "a check"}.`;
|
|
40
|
+
const finished = ci.runs.filter(run => run.status === "completed").length;
|
|
41
|
+
return `GitHub CI is running on ${short}: ${finished} of ${ci.runs.length} checks finished.`;
|
|
42
|
+
}
|
|
43
|
+
/** The release check failed on this task already (the coordinator's record): nobody approves it again here. */
|
|
44
|
+
function checkFailed(store, taskId) {
|
|
45
|
+
return store.handle.prepare("SELECT 1 FROM release_step WHERE task_id = ? AND step = 'check' AND state = 'failed' LIMIT 1").get(taskId) !== undefined;
|
|
46
|
+
}
|
|
47
|
+
/** Why a release can't be approved now, from the plane's own state, or null. */
|
|
48
|
+
export function refusalOf(store, taskId, scope, ci) {
|
|
49
|
+
if (taskArchived(store, taskId))
|
|
50
|
+
return "This release is put away. Restore it before approving.";
|
|
51
|
+
const hold = releaseCiHoldOf(store, taskId);
|
|
52
|
+
if (hold !== null)
|
|
53
|
+
return releaseCiHoldWords(hold);
|
|
54
|
+
if (ci?.state === "failed")
|
|
55
|
+
return `${ciWords(ci, scope.candidate)} Fix the branch or rerun the failed job on GitHub, then release again.`;
|
|
56
|
+
if (checkFailed(store, taskId))
|
|
57
|
+
return "The release check already failed on this commit. Fix the branch and release the new commit.";
|
|
58
|
+
if (scope.approvedAt !== null && scope.approvedDigest !== scope.digest)
|
|
59
|
+
return "The release's terms changed after an earlier approval. Read them again.";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The release refusals every approval road shares (scope.ts approve(), read inside its transaction): for a scope
|
|
64
|
+
* sealing release terms or a release check's purpose, the sheet's own reasons in the sheet's own words, so the task
|
|
65
|
+
* page, Home's in-place yes, chat and `toolroll task approve` refuse exactly what `/t/<id>/release` refuses. Null for
|
|
66
|
+
* every other scope, and for a release nothing stops. The sheet's card digest binding stays the sheet's.
|
|
67
|
+
*/
|
|
68
|
+
export function releaseApprovalRefusal(store, taskId, scope) {
|
|
69
|
+
// A sealed form, as release-card.ts releaseSealOf reads it (release terms, or a release check's purpose).
|
|
70
|
+
if (scope.releaseTerms == null && scope.releaseCheck == null)
|
|
71
|
+
return null;
|
|
72
|
+
return refusalOf(store, taskId, scope, scope.candidate == null ? null : releaseCiOf(store, taskId, scope.candidate));
|
|
73
|
+
}
|
package/dist/run-efficiency.d.ts
CHANGED
|
@@ -68,6 +68,7 @@ export declare function deriveRunEfficiency(run: Run & {
|
|
|
68
68
|
}, evidenceRoot: string, now: Date, commands?: boolean): RunEfficiency;
|
|
69
69
|
/**
|
|
70
70
|
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
71
|
-
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved
|
|
71
|
+
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved, and
|
|
72
|
+
* nothing is saved when `save` is false (a page read ahead of its link writes nothing).
|
|
72
73
|
*/
|
|
73
|
-
export declare function runEfficiencyOf(store: Store, evidenceRoot: string, runId: number, now: Date): RunEfficiency | null;
|
|
74
|
+
export declare function runEfficiencyOf(store: Store, evidenceRoot: string, runId: number, now: Date, save?: boolean): RunEfficiency | null;
|
package/dist/run-efficiency.js
CHANGED
|
@@ -199,9 +199,10 @@ export function deriveRunEfficiency(run, evidenceRoot, now, commands = false) {
|
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
201
201
|
* A finished run's How it ran: the saved one when it was derived by this version, else derived now and saved (once:
|
|
202
|
-
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved
|
|
202
|
+
* a row whose steps were complete or have expired is final). An unfinished run is derived but never saved, and
|
|
203
|
+
* nothing is saved when `save` is false (a page read ahead of its link writes nothing).
|
|
203
204
|
*/
|
|
204
|
-
export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
205
|
+
export function runEfficiencyOf(store, evidenceRoot, runId, now, save = true) {
|
|
205
206
|
const run = store.getRun(runId);
|
|
206
207
|
if (run === null)
|
|
207
208
|
return null;
|
|
@@ -227,7 +228,7 @@ export function runEfficiencyOf(store, evidenceRoot, runId, now) {
|
|
|
227
228
|
const payload = JSON.stringify(derived);
|
|
228
229
|
// A row too large for its column (it can't be: the counts are bounded) is shown, never saved.
|
|
229
230
|
// Saving is a convenience: a store that can't take the row (read-only, busy) still shows what was derived.
|
|
230
|
-
if (payload.length <= 16_384) {
|
|
231
|
+
if (save && payload.length <= 16_384) {
|
|
231
232
|
try {
|
|
232
233
|
store.handle.prepare(`INSERT INTO run_efficiency (run, parser_version, steps, steps_reason, payload_json, derived_at) VALUES (?, ?, ?, ?, ?, ?)
|
|
233
234
|
ON CONFLICT(run) DO UPDATE SET parser_version = excluded.parser_version, steps = excluded.steps, steps_reason = excluded.steps_reason,
|
package/dist/scope.js
CHANGED
|
@@ -54,6 +54,7 @@ import { parseContract } from "./contracts/contract.js";
|
|
|
54
54
|
import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
|
|
55
55
|
import { taskArchived, taskCancelled } from "./task-archive.js";
|
|
56
56
|
import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
|
|
57
|
+
import { releaseApprovalRefusal } from "./release-refusal.js";
|
|
57
58
|
import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
|
|
58
59
|
/** Same shape as a runner's credential, for the same reasons. */
|
|
59
60
|
function mintToken() {
|
|
@@ -1117,6 +1118,11 @@ chat) {
|
|
|
1117
1118
|
const scope = store.getScope(taskId);
|
|
1118
1119
|
if (scope === null)
|
|
1119
1120
|
return { ok: false, reason: "no-scope" };
|
|
1121
|
+
// A release takes no yes the release sheet would refuse (put away, CI held or failed, its check failed on this
|
|
1122
|
+
// commit, terms changed after an earlier yes), in the sheet's words, on every road: read here, before any vote.
|
|
1123
|
+
const releaseRefusal = releaseApprovalRefusal(store, taskId, scope);
|
|
1124
|
+
if (releaseRefusal !== null)
|
|
1125
|
+
return { ok: false, reason: "policy", message: releaseRefusal };
|
|
1120
1126
|
// A put-away task (v145) is restored first; sealScopeApproval refuses it too, on every road.
|
|
1121
1127
|
if (taskArchived(store, taskId))
|
|
1122
1128
|
return { ok: false, reason: "archived" };
|
package/dist/serve.js
CHANGED
|
@@ -27,6 +27,7 @@ const PROXY_HEADER = /^(forwarded|via|x-forwarded-.+|x-real-ip|x-client-ip|x-ori
|
|
|
27
27
|
const BACKGROUND_CHECKS_MS = 30_000;
|
|
28
28
|
const LOCAL_AGENTS_EVERY_MS = 2 * 60_000;
|
|
29
29
|
import { browserCrewFromIndex, browserCrewOf, browserNavigationOf, browserProjectsOf, needsYouLabelOf } from './browser-workspace.js';
|
|
30
|
+
import { PAGE_WORK_HEADER, PAGE_WORK_VERSION } from './page-work-contract.js';
|
|
30
31
|
import { sharedActionPayload } from './chat-actions.js';
|
|
31
32
|
import { prepareCodingContext } from './coding-context.js';
|
|
32
33
|
import { CodingWorkspace } from './coding-workspace.js';
|
|
@@ -155,7 +156,8 @@ import { createFlowsHandlers } from './server/flows.js';
|
|
|
155
156
|
import { createPagesHandlers } from './server/pages.js';
|
|
156
157
|
import { createPeopleHandlers } from './server/people-tokens.js';
|
|
157
158
|
import { createSettingsHandlers } from './server/settings.js';
|
|
158
|
-
import { requestContext } from "./server/request-context.js";
|
|
159
|
+
import { readPurpose, requestContext } from "./server/request-context.js";
|
|
160
|
+
import { inPlaceRoute, prefetchRoute } from "./server/in-place.js";
|
|
159
161
|
import { asLocalOwner, localModeOn, localOwnerRefusal, localOwnerStandsNow } from "./local-owner.js";
|
|
160
162
|
import { defaultAgentOf, fillDefaultRouting } from "./model-choices.js";
|
|
161
163
|
import { chromeScript, DEMO_BANNER, DEMO_BANNER_SHORT, focusDocument, KBD_HELP, pinnedTheme, QUEUE_VIEW, refuse, screen, SENSITIVE_INPUT, shell, sidebarScript } from "./server/chrome.js";
|
|
@@ -878,9 +880,22 @@ export function createDecisionServer(options) {
|
|
|
878
880
|
// Passive polls authenticate without extending the browser session.
|
|
879
881
|
const fragmentName = url.searchParams.get("fragment");
|
|
880
882
|
const workspaceRead = method === "GET" && url.searchParams.get('format') === 'workspace';
|
|
881
|
-
|
|
883
|
+
// A workspace read's purpose (live-pages-and-home.md A1.4): bookkeeping only. Read on workspace GETs alone; on any other
|
|
884
|
+
// request the header means nothing. No authorisation decision below reads it.
|
|
885
|
+
const purpose = workspaceRead ? readPurpose(request.headers['x-toolroll-read']) : 'poll';
|
|
886
|
+
// The reads that never extend the session, whatever their header says.
|
|
887
|
+
const noTouch = edgeRoute?.domain === 'live' || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname) || url.pathname === '/term/state'
|
|
882
888
|
// The Agents view's Live grid only watches: its polls never extend or write the session.
|
|
883
|
-
|| url.pathname === '/agents/live' || /^\/agents\/live\/[0-9]{1,15}$/.test(url.pathname) || (url.pathname === '/agents' && url.searchParams.get('view') === 'live')
|
|
889
|
+
|| url.pathname === '/agents/live' || /^\/agents\/live\/[0-9]{1,15}$/.test(url.pathname) || (url.pathname === '/agents' && url.searchParams.get('view') === 'live');
|
|
890
|
+
// A followed link (`navigate`) extends the session exactly as the HTML GET of the same address would; every other
|
|
891
|
+
// workspace read (a re-read, a prefetch) does not.
|
|
892
|
+
const touch = !(method === "GET" && (noTouch || (workspaceRead && purpose !== 'navigate')));
|
|
893
|
+
// A read ahead is answered only for a browser session, only for the prefetch allowlist (in-place.ts): anything else
|
|
894
|
+
// hears 204 before any sign-in bookkeeping (a token's last use, a sign-in charge) is written.
|
|
895
|
+
if (purpose === 'prefetch' && (request.headers.authorization !== undefined || !prefetchRoute(matchedRoute?.id, url))) {
|
|
896
|
+
request.resume();
|
|
897
|
+
return respond(response, 204, "text/plain; charset=utf-8", "");
|
|
898
|
+
}
|
|
884
899
|
const refuseBudget = (admitted) => {
|
|
885
900
|
if (admitted.ok)
|
|
886
901
|
return false;
|
|
@@ -906,6 +921,11 @@ export function createDecisionServer(options) {
|
|
|
906
921
|
}
|
|
907
922
|
if (who?.via === "bearer" && refuseBudget(admitBearer(request, who)))
|
|
908
923
|
return;
|
|
924
|
+
// A read ahead from anything but a signed-in browser session (signed out, a token): 204, never the sign-in page.
|
|
925
|
+
if (purpose === 'prefetch' && who?.via !== 'cookie') {
|
|
926
|
+
request.resume();
|
|
927
|
+
return respond(response, 204, "text/plain; charset=utf-8", "");
|
|
928
|
+
}
|
|
909
929
|
const passwordProof = who?.via === 'cookie' ? { name: who.name, generation: who.session.generation } : null;
|
|
910
930
|
const fresh = freshFactsOf(request, who, matchedRoute?.freshness === 'allowlist');
|
|
911
931
|
if (edgeRoute?.domain === 'people')
|
|
@@ -940,6 +960,9 @@ export function createDecisionServer(options) {
|
|
|
940
960
|
get fresh() { return freshFacts.getStore() != null && ssoNowAvailable(who.name); } } : undefined,
|
|
941
961
|
refusal: (answer, status, body) => sendScreen(answer, status, screen(status === 404 ? "Not found" : "Request refused", body, { chrome: chromeFor(who.via === "cookie" ? who.session.project : null, "work") })),
|
|
942
962
|
workspaceRead,
|
|
963
|
+
readPurpose: purpose,
|
|
964
|
+
...(workspaceRead && request.headers[PAGE_WORK_HEADER] === String(PAGE_WORK_VERSION) ? { appWork: true } : {}),
|
|
965
|
+
...(matchedRoute?.stage === 'console' ? { routeId: matchedRoute.id } : {}),
|
|
943
966
|
freshWork: ["request", "task", "result"].some(name => url.searchParams.has(name)),
|
|
944
967
|
...(workspaceRead && typeof request.headers['if-none-match'] === 'string' ? { workspaceIfNoneMatch: request.headers['if-none-match'] } : {}),
|
|
945
968
|
workspaceRequest: url.searchParams.get('request'),
|
|
@@ -952,7 +975,9 @@ export function createDecisionServer(options) {
|
|
|
952
975
|
? { localStepUp: localOwnerRefusal(false, method === "GET" ? passwordSetNext(pagePathOf(url)) : "/").href } : {}),
|
|
953
976
|
reconnect: browserKeys.stale(who, method === "GET" ? safeReturn(pagePathOf(url)) : "/"),
|
|
954
977
|
};
|
|
955
|
-
|
|
978
|
+
// A read ahead writes nothing, not even a slow request's record.
|
|
979
|
+
if (purpose !== 'prefetch')
|
|
980
|
+
response.once("finish", () => keepSlowRequest(store, timeline, matchedRoute?.id ?? "unmatched", response.statusCode, clock()));
|
|
956
981
|
if (method === "GET" || method === "POST")
|
|
957
982
|
return freshFacts.run(fresh, () => provenPasswordAccount.run(passwordProof, () => requestContext.run(requestFacts, async () => {
|
|
958
983
|
if (edgeRoute?.domain === 'live')
|
|
@@ -1669,8 +1694,29 @@ export function createDecisionServer(options) {
|
|
|
1669
1694
|
}
|
|
1670
1695
|
const conversation = extras.conversation ?? null;
|
|
1671
1696
|
const request = requestFacts.workspaceRequest;
|
|
1697
|
+
// React owns the conversation and its refresh/draft lifecycle. Native
|
|
1698
|
+
// guarded forms keep their current behavior after React inserts them.
|
|
1699
|
+
// A page with its own view keeps its script; the chat page runs the result script.
|
|
1700
|
+
// The exact page script the document would run: the JSON arm and the document arm read this one value.
|
|
1701
|
+
const ownScript = conversation === null || extras.view;
|
|
1702
|
+
const pageScript = ownScript ? (s.functional?.script ?? '') : RESULT_REVIEW_SCRIPT;
|
|
1703
|
+
const routeInPlace = inPlaceRoute(requestFacts.routeId, path);
|
|
1704
|
+
// The app runs the page's behaviour itself (docs/plans/task-page-in-app.md §2) only when all of these hold: the
|
|
1705
|
+
// app covers the WHOLE script, no password field (a password page keeps its string path), an in-place route, and a
|
|
1706
|
+
// reader that can run it: a document load (its bundle is this server's, served no-store) or a workspace read that
|
|
1707
|
+
// says so. Anything else is the page exactly as before: the string, no work, a document.
|
|
1708
|
+
const covered = ownScript ? s.functional?.app : undefined;
|
|
1709
|
+
const work = pageScript !== '' && covered !== undefined && covered.length > 0 && !sensitive && routeInPlace
|
|
1710
|
+
&& (!requestFacts.workspaceRead || requestFacts.appWork === true) ? covered : null;
|
|
1711
|
+
const functional = work === null ? pageScript : '';
|
|
1712
|
+
// May replace another page without a document load (live-pages-and-home.md A1.1): an allowlisted route, no password
|
|
1713
|
+
// field, no page script (a page script runs once per document, so its page always loads as one).
|
|
1714
|
+
const inPlace = routeInPlace && !sensitive && functional === '';
|
|
1672
1715
|
const workspace = {
|
|
1673
|
-
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
|
|
1716
|
+
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive, inPlace,
|
|
1717
|
+
...(work === null ? {} : { work }),
|
|
1718
|
+
// Said on the document that ships it: this document's page script listens for as long as the document lives.
|
|
1719
|
+
...(functional !== '' && !requestFacts.workspaceRead ? { scripted: true } : {}),
|
|
1674
1720
|
// The project picker names the open project; the settings frame carries its breadcrumb.
|
|
1675
1721
|
// Only a project this person may see now: never the raw saved path (settings-kit.ts openProjectFor).
|
|
1676
1722
|
openProject: openProjectFor(requestFacts.openProject ?? null, s.chrome.project, s.chrome.projects ?? []),
|
|
@@ -1761,10 +1807,6 @@ export function createDecisionServer(options) {
|
|
|
1761
1807
|
return respond(response, status, 'application/json; charset=utf-8', body);
|
|
1762
1808
|
}
|
|
1763
1809
|
const nonce = randomBytes(16).toString('base64');
|
|
1764
|
-
// React owns the conversation and its refresh/draft lifecycle. Native
|
|
1765
|
-
// guarded forms keep their current behavior after React inserts them.
|
|
1766
|
-
// A page with its own view keeps its script; the chat page runs the result script.
|
|
1767
|
-
const functional = conversation === null || extras.view ? (s.functional?.script ?? '') : RESULT_REVIEW_SCRIPT;
|
|
1768
1810
|
const document = shell(s.title, s.body, { chrome: s.chrome, sensitive });
|
|
1769
1811
|
// The keys and their list (keymap.ts) work here as on every chromed page; the list sits beside the app React owns.
|
|
1770
1812
|
// A sensitive page keeps to its own script.
|
package/dist/server/chrome.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type BrowserSignIn, type BrowserUpdateNotice, type BrowserWorkspace } from "../browser-workspace.js";
|
|
1
|
+
import { type BrowserSignIn, type BrowserUpdateNotice, type BrowserWorkspace, type PageWork } from "../browser-workspace.js";
|
|
2
2
|
import { type Crumb } from "../settings-kit.js";
|
|
3
3
|
import { type Decision, type Run } from "../store.js";
|
|
4
4
|
import { type tally } from "../summary.js";
|
|
@@ -176,6 +176,9 @@ export type Screen = {
|
|
|
176
176
|
functional?: {
|
|
177
177
|
script: string;
|
|
178
178
|
fetches?: boolean;
|
|
179
|
+
/** The app's own version of this WHOLE script (task-page-in-app.md §1): where the app can run it, the document
|
|
180
|
+
* ships no script string and the page may change in place. Added to only through withScript. */
|
|
181
|
+
app?: readonly PageWork[];
|
|
179
182
|
};
|
|
180
183
|
refreshSeconds?: number;
|
|
181
184
|
/** Render sensitive even when no password field is visible — one-time
|
|
@@ -317,3 +320,9 @@ export declare function oneLineOf(text: string, cap: number): string;
|
|
|
317
320
|
* minutes are the answer without script.
|
|
318
321
|
*/
|
|
319
322
|
export declare function boardTickScript(regionId?: string): string;
|
|
323
|
+
/**
|
|
324
|
+
* A page's script with one more added (docs/plans/task-page-in-app.md §1). All or nothing: `app` survives only when
|
|
325
|
+
* every part has it, so a script the app does not cover keeps the whole page a document, and a page never gets half
|
|
326
|
+
* its behaviour from the string and half from the app.
|
|
327
|
+
*/
|
|
328
|
+
export declare function withScript(base: Screen["functional"], add: NonNullable<Screen["functional"]>): NonNullable<Screen["functional"]>;
|
package/dist/server/chrome.js
CHANGED
|
@@ -807,3 +807,12 @@ export function boardTickScript(regionId = "board-region") {
|
|
|
807
807
|
`run();setInterval(function(){if(!document.hidden)run();},1000);document.addEventListener("visibilitychange",function(){if(!document.hidden)run();});` +
|
|
808
808
|
`if(region&&typeof MutationObserver==="function")new MutationObserver(run).observe(region,{childList:true});})();`);
|
|
809
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* A page's script with one more added (docs/plans/task-page-in-app.md §1). All or nothing: `app` survives only when
|
|
812
|
+
* every part has it, so a script the app does not cover keeps the whole page a document, and a page never gets half
|
|
813
|
+
* its behaviour from the string and half from the app.
|
|
814
|
+
*/
|
|
815
|
+
export function withScript(base, add) {
|
|
816
|
+
const app = base === undefined ? add.app : base.app !== undefined && add.app !== undefined ? [...base.app, ...add.app] : undefined;
|
|
817
|
+
return { script: (base?.script ?? "") + add.script, fetches: (base?.fetches ?? false) || (add.fetches ?? false), ...(app === undefined ? {} : { app }) };
|
|
818
|
+
}
|
package/dist/server/console.css
CHANGED
|
@@ -2662,14 +2662,29 @@ button { min-height: 44px; }
|
|
|
2662
2662
|
the same on both, swaps in place instead of fading (each shell names its
|
|
2663
2663
|
own, so a page can never carry the name twice). */
|
|
2664
2664
|
::view-transition-old(root) { animation: so-page-leave 80ms ease-in both; }
|
|
2665
|
-
::view-transition-new(root) { animation: so-page-arrive
|
|
2665
|
+
::view-transition-new(root) { animation: so-page-arrive 160ms ease-out 60ms both; }
|
|
2666
2666
|
@keyframes so-page-leave { to { opacity: 0; } }
|
|
2667
2667
|
@keyframes so-page-arrive { from { opacity: 0; } }
|
|
2668
2668
|
.side { view-transition-name: so-nav-page; }
|
|
2669
2669
|
.so-sidebar { view-transition-name: so-nav-app; }
|
|
2670
|
-
|
|
2670
|
+
.so-ph { view-transition-name: so-phone-header; }
|
|
2671
|
+
::view-transition-group(so-nav-page), ::view-transition-group(so-nav-app), ::view-transition-group(so-phone-header), ::view-transition-group(so-message-box),
|
|
2671
2672
|
::view-transition-old(so-nav-page), ::view-transition-new(so-nav-page),
|
|
2672
|
-
::view-transition-old(so-nav-app), ::view-transition-new(so-nav-app)
|
|
2673
|
+
::view-transition-old(so-nav-app), ::view-transition-new(so-nav-app),
|
|
2674
|
+
::view-transition-old(so-phone-header), ::view-transition-new(so-phone-header),
|
|
2675
|
+
::view-transition-old(so-message-box), ::view-transition-new(so-message-box) { animation: none; }
|
|
2676
|
+
/* A page changed in place (docs/plans/live-pages-and-home.md A2.2, browser/transition.ts): only the page content
|
|
2677
|
+
moves; everything else (the root, the sidebar, the phone header, the message box) swaps where it stands. The same
|
|
2678
|
+
level fades through like a document load (80 ms out, 160 ms in); deeper, the new page also moves 8px in from the
|
|
2679
|
+
right over 200 ms; shallower, from the left. Nothing runs past 220 ms. */
|
|
2680
|
+
html.so-nav #workspace-main { view-transition-name: so-page; }
|
|
2681
|
+
html.so-nav::view-transition-old(root), html.so-nav::view-transition-new(root), html.so-nav::view-transition-group(so-page) { animation: none; }
|
|
2682
|
+
html.so-nav::view-transition-old(so-page) { animation: so-page-leave 80ms ease-in both; }
|
|
2683
|
+
html.so-nav-same::view-transition-new(so-page) { animation: so-page-arrive 160ms ease-out 60ms both; }
|
|
2684
|
+
html.so-nav-deeper::view-transition-new(so-page) { animation: so-page-deeper 200ms cubic-bezier(.2, .8, .2, 1) both; }
|
|
2685
|
+
html.so-nav-shallower::view-transition-new(so-page) { animation: so-page-shallower 200ms cubic-bezier(.2, .8, .2, 1) both; }
|
|
2686
|
+
@keyframes so-page-deeper { from { opacity: 0; transform: translateX(8px); } }
|
|
2687
|
+
@keyframes so-page-shallower { from { opacity: 0; transform: translateX(-8px); } }
|
|
2673
2688
|
/* UI polish 2026-09-13 — the motion contract. Feedback transitions run
|
|
2674
2689
|
140–200 ms on color, border, shadow, opacity, and transform only; an
|
|
2675
2690
|
overlay's entrance is at most 220 ms of opacity + transform. Nothing
|
|
@@ -2699,7 +2714,7 @@ button { min-height: 44px; }
|
|
|
2699
2714
|
@keyframes fade { from { opacity: 0; } }
|
|
2700
2715
|
@media (prefers-reduced-motion: reduce) {
|
|
2701
2716
|
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
2702
|
-
::view-transition-group(*), ::view-transition-old(
|
|
2717
|
+
::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
|
|
2703
2718
|
button:active, :is(.button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip):active { transform: none; }
|
|
2704
2719
|
@media (hover: hover) and (pointer: fine) { button:hover, .side nav a:hover, .chat-project-card:hover, .lane-card:hover, .decide-card:hover, .menu-row:hover { transform: none; } }
|
|
2705
2720
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { FirstRunStep } from "../first-run.js";
|
|
2
|
+
import type { Store } from "../store.js";
|
|
3
|
+
import type { WorkSummaryAccess } from "../work-summary.js";
|
|
4
|
+
/** One row Home lists that isn't a task's own row. `id` is namespaced so it never meets a task's id; `at` (when it
|
|
5
|
+
* started waiting) gives its age, or null for a setup step; `repo` its project. */
|
|
6
|
+
export type HomeAskRow = {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
href: string;
|
|
10
|
+
repo: string | null;
|
|
11
|
+
at: string | null;
|
|
12
|
+
line: string | null;
|
|
13
|
+
action: {
|
|
14
|
+
label: string;
|
|
15
|
+
href: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type HomeAsks = {
|
|
19
|
+
/** A release's line for an approver (its version, or why the sheet can't take a yes now); null when it isn't one. */
|
|
20
|
+
release: (taskId: string) => string | null;
|
|
21
|
+
/** Releases whose publication waits for a person, one per family, only where that family isn't already a row. */
|
|
22
|
+
held: HomeAskRow[];
|
|
23
|
+
/** Flow cards waiting on this person. */
|
|
24
|
+
flows: HomeAskRow[];
|
|
25
|
+
/** First-run steps still to do, keyed by step. */
|
|
26
|
+
setup: (HomeAskRow & {
|
|
27
|
+
key: FirstRunStep["key"];
|
|
28
|
+
})[];
|
|
29
|
+
};
|
|
30
|
+
export declare const NO_HOME_ASKS: HomeAsks;
|
|
31
|
+
/** The release sheet: where a release is read and approved, never Home. */
|
|
32
|
+
export declare const releaseSheetHref: (taskId: string) => string;
|
|
33
|
+
/** A release's line (its version, or the sheet's refusal) and every held publication in view. Approvers only: the sheet
|
|
34
|
+
* mints its approval only for them; anyone else reads a release as an ordinary task row. */
|
|
35
|
+
export declare function releaseAsks(store: Store, now: Date, access: WorkSummaryAccess, project: string | null, approver: boolean): Pick<HomeAsks, "release" | "held">;
|
|
36
|
+
/** Every active card, in a flow of a project this person may see, that waits on them (flows-ui.ts flowCardWaitsOn, the
|
|
37
|
+
* flow page's own rule). Newest wait first. */
|
|
38
|
+
export declare function flowAsks(store: Store, repos: readonly string[], viewer: {
|
|
39
|
+
name: string;
|
|
40
|
+
approver: boolean;
|
|
41
|
+
}, project: string | null): HomeAskRow[];
|
|
42
|
+
/** The first-run steps (first-run.ts, with the builder step) still to do, for an approver. A step that is done, or only
|
|
43
|
+
* being checked (no answer yet, so no action), is not a row. Null steps (a restricted console, the demo, after the first
|
|
44
|
+
* success) are none. */
|
|
45
|
+
export declare function setupAsks(steps: readonly FirstRunStep[] | null, approver: boolean): HomeAsks["setup"];
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What else waits on a person at Home (docs/plans/home-other-asks.md): a release's line and a held publication, a flow
|
|
3
|
+
* card waiting on them, and the first-run steps left to do. Reads only, each through the same admission as the page it
|
|
4
|
+
* links to; nothing here acts: every row's one link opens its own page, whose own checks decide what this person may do
|
|
5
|
+
* there. `homeView` (home.ts) assembles these with the work index's rows.
|
|
6
|
+
*/
|
|
7
|
+
import { flowCardHref, flowDefinitionOf } from "../flow-engine.js";
|
|
8
|
+
import { browserViewOf } from "../flow-browser.js";
|
|
9
|
+
import { flowCardWaitsOn } from "../flows-ui.js";
|
|
10
|
+
import { releaseAskOf, releaseTaskOf } from "../release-approval.js";
|
|
11
|
+
import { autoPublishHeld, PUBLISH_ALLOWED_ACTION, PUBLISH_HELD_ACTION } from "../release-auto.js";
|
|
12
|
+
import { taskArchived } from "../task-archive.js";
|
|
13
|
+
import { workStateRows } from "../work-index.js";
|
|
14
|
+
export const NO_HOME_ASKS = { release: () => null, held: [], flows: [], setup: [] };
|
|
15
|
+
/** The release sheet: where a release is read and approved, never Home. */
|
|
16
|
+
export const releaseSheetHref = (taskId) => `/t/${encodeURIComponent(taskId)}/release`;
|
|
17
|
+
const REVIEW_RELEASE = "Review the release";
|
|
18
|
+
/** A release's line (its version, or the sheet's refusal) and every held publication in view. Approvers only: the sheet
|
|
19
|
+
* mints its approval only for them; anyone else reads a release as an ordinary task row. */
|
|
20
|
+
export function releaseAsks(store, now, access, project, approver) {
|
|
21
|
+
if (!approver)
|
|
22
|
+
return { release: () => null, held: [] };
|
|
23
|
+
const release = (taskId) => {
|
|
24
|
+
const ask = releaseAskOf(store, taskId);
|
|
25
|
+
return ask === null ? null : ask.refusal ?? `Version ${ask.version} waits for your approval.`;
|
|
26
|
+
};
|
|
27
|
+
const shown = (repo) => (access.repos === null || access.repos.includes(repo)) && (project === null || repo === project);
|
|
28
|
+
// Only what is still held: the newest held-or-allowed act per task is the held one (release-auto.ts autoPublishHeld's
|
|
29
|
+
// own rule, read in one statement). Each is then a release this person may see in this scope, before anything heavier.
|
|
30
|
+
const marked = store.handle.prepare(`SELECT a.task_id, a.at, a.detail FROM action_ledger a WHERE a.action = $held AND a.task_id IS NOT NULL
|
|
31
|
+
AND a.id = (SELECT MAX(b.id) FROM action_ledger b WHERE b.task_id = a.task_id AND b.action IN ($held, $allowed)) ORDER BY a.at DESC, a.id DESC`)
|
|
32
|
+
.all({ $held: PUBLISH_HELD_ACTION, $allowed: PUBLISH_ALLOWED_ACTION });
|
|
33
|
+
const candidates = marked.flatMap(row => {
|
|
34
|
+
const taskId = String(row["task_id"]);
|
|
35
|
+
const found = releaseTaskOf(store, taskId);
|
|
36
|
+
return found === null || !shown(found.ref.repo) || taskArchived(store, taskId) ? []
|
|
37
|
+
: [{ taskId, ref: found.ref.id, repo: found.ref.repo, at: String(row["at"]), why: autoPublishHeld(store, taskId) ?? String(row["detail"] ?? "") }];
|
|
38
|
+
});
|
|
39
|
+
if (candidates.length === 0)
|
|
40
|
+
return { release, held: [] };
|
|
41
|
+
// Every family in this person's view, read once (the Tasks list's own projection): a held release's family that is
|
|
42
|
+
// already a row that needs them (in any group) is not listed twice, and one the view doesn't admit is not listed.
|
|
43
|
+
const familyOf = new Map();
|
|
44
|
+
for (const family of workStateRows(store, now, access))
|
|
45
|
+
for (const member of family.members)
|
|
46
|
+
familyOf.set(member, { rootId: family.rootId, needs: family.needs });
|
|
47
|
+
const families = new Set();
|
|
48
|
+
const held = [];
|
|
49
|
+
for (const one of candidates) {
|
|
50
|
+
const family = familyOf.get(one.ref);
|
|
51
|
+
if (family === undefined || family.needs || families.has(family.rootId))
|
|
52
|
+
continue;
|
|
53
|
+
families.add(family.rootId);
|
|
54
|
+
held.push({ id: `release:${family.rootId}`, title: store.getTask(one.taskId)?.title ?? one.taskId, href: releaseSheetHref(one.taskId), repo: one.repo, at: one.at,
|
|
55
|
+
line: one.why, action: { label: REVIEW_RELEASE, href: releaseSheetHref(one.taskId) } });
|
|
56
|
+
}
|
|
57
|
+
return { release, held };
|
|
58
|
+
}
|
|
59
|
+
/** Every active card, in a flow of a project this person may see, that waits on them (flows-ui.ts flowCardWaitsOn, the
|
|
60
|
+
* flow page's own rule). Newest wait first. */
|
|
61
|
+
export function flowAsks(store, repos, viewer, project) {
|
|
62
|
+
if (!viewer.approver)
|
|
63
|
+
return [];
|
|
64
|
+
const rows = [];
|
|
65
|
+
for (const flow of store.listFlows(repos)) {
|
|
66
|
+
if (flow.state !== "active" || (project !== null && flow.repo !== project) || !store.accountCanAccess(viewer.name, flow.repo))
|
|
67
|
+
continue;
|
|
68
|
+
const stages = flowDefinitionOf(flow)?.stages ?? [];
|
|
69
|
+
// The flow's browser standing, read once and only when a card waits in a zone that starts a browser.
|
|
70
|
+
let browser;
|
|
71
|
+
for (const card of store.flowCards(flow.id, false)) {
|
|
72
|
+
if (card.state !== "active")
|
|
73
|
+
continue;
|
|
74
|
+
// A card whose zone was removed still has its subagent's open question (the flow page still takes the answer).
|
|
75
|
+
const stage = stages.find(one => one.id === card.stage);
|
|
76
|
+
if (stage?.kind === "check" && stage.browser === true && browser === undefined)
|
|
77
|
+
browser = browserViewOf(store, flow);
|
|
78
|
+
const waits = flowCardWaitsOn(store, flow, card, viewer, { stage, ...(browser === undefined ? {} : { browser }) });
|
|
79
|
+
if (!waits.decide && !waits.choose && !waits.answer && !waits.browser && !waits.reply)
|
|
80
|
+
continue;
|
|
81
|
+
const asked = waits.answer ? store.openSubagentQuestionOn(card.id, card.entry)?.question ?? null : null;
|
|
82
|
+
// The card's own waiting words (a browser wait without the command the page offers as a button).
|
|
83
|
+
const waiting = card.waiting?.replace(/ Approve it with: toolroll flows browser \d+ on$/, "") ?? null;
|
|
84
|
+
const said = waiting ?? asked ?? (waits.choose ? "Waits for your choice." : "Waits for your decision.");
|
|
85
|
+
const href = flowCardHref(flow.id, card.id);
|
|
86
|
+
rows.push({ id: `flow:${card.id}`, title: card.title, href, repo: flow.repo, at: store.flowCardEnteredAt(card.id) ?? card.updatedAt,
|
|
87
|
+
line: `${flow.name}: ${said}`, action: { label: "Open the card", href } });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return rows.sort((a, b) => (b.at ?? "").localeCompare(a.at ?? ""));
|
|
91
|
+
}
|
|
92
|
+
const SETUP_TITLES = {
|
|
93
|
+
agent: "Sign in to a coding agent", project: "Add a project", builder: "Connect a builder", task: "File your first task",
|
|
94
|
+
};
|
|
95
|
+
/** The first-run steps (first-run.ts, with the builder step) still to do, for an approver. A step that is done, or only
|
|
96
|
+
* being checked (no answer yet, so no action), is not a row. Null steps (a restricted console, the demo, after the first
|
|
97
|
+
* success) are none. */
|
|
98
|
+
export function setupAsks(steps, approver) {
|
|
99
|
+
if (steps === null || !approver)
|
|
100
|
+
return [];
|
|
101
|
+
return steps.flatMap(step => {
|
|
102
|
+
if (step.done || step.checking === true || step.action === null)
|
|
103
|
+
return [];
|
|
104
|
+
const action = step.action.kind === "link" ? { label: step.action.label, href: step.action.href } : { label: "Open setup", href: "/welcome" };
|
|
105
|
+
return [{ key: step.key, id: `setup:${step.key}`, title: SETUP_TITLES[step.key], href: action.href, repo: null, at: null,
|
|
106
|
+
line: step.action.kind === "command" ? `Run ${step.action.command} in a terminal.` : null, action }];
|
|
107
|
+
});
|
|
108
|
+
}
|
package/dist/server/home.d.ts
CHANGED
|
@@ -1,14 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Home ("/",
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Home ("/", docs/plans/live-pages-and-home.md B1, home-other-asks.md): what needs this person, grouped by what is asked
|
|
3
|
+
* (Approve a release, Approve a plan, Answer a question, Flow decisions, Something went wrong, Review results, Finish
|
|
4
|
+
* setting up, and Needs you for the rest), each group with its exact count; one quiet line for what runs and waits; with nothing waiting, what runs and what finished today. The figures
|
|
5
|
+
* are read from the same work index the Tasks list filters, so a count and the list it opens always agree. Reads
|
|
6
|
+
* only: every act keeps its own route and rules (an agent's question is answered through POST /d/<id>/answer, the
|
|
7
|
+
* same one-click act as its own page).
|
|
6
8
|
*/
|
|
7
|
-
import type { BrowserHomeView } from "../browser-workspace.js";
|
|
9
|
+
import type { BrowserHomeAsk, BrowserHomeView } from "../browser-workspace.js";
|
|
8
10
|
import type { Store } from "../store.js";
|
|
9
11
|
import type { WorkSummaryAccess } from "../work-summary.js";
|
|
10
|
-
|
|
12
|
+
import { type HomeAsks } from "./home-asks.js";
|
|
13
|
+
/** How many waiting rows Home lists; each group says how many more open in Tasks. */
|
|
11
14
|
export declare const HOME_NEEDS_SHOWN = 30;
|
|
15
|
+
/** How many rows finished today Home lists; its figure counts them all. */
|
|
16
|
+
export declare const HOME_DONE_SHOWN = 8;
|
|
17
|
+
/** Each group's heading, in the order Home shows them. */
|
|
18
|
+
export declare const HOME_ASK_LABEL: Readonly<Record<BrowserHomeAsk, string>>;
|
|
19
|
+
/** The groups in Home's one order. */
|
|
20
|
+
export declare const HOME_ASK_ORDER: readonly BrowserHomeAsk[];
|
|
12
21
|
export type HomeFacts = {
|
|
13
22
|
store: Store;
|
|
14
23
|
now: Date;
|
|
@@ -16,7 +25,7 @@ export type HomeFacts = {
|
|
|
16
25
|
evidenceRoot?: string;
|
|
17
26
|
/** The project in view (already admitted), or null for every project. */
|
|
18
27
|
project: string | null;
|
|
19
|
-
/** Every admitted project, for the
|
|
28
|
+
/** Every admitted project, for the project filter. */
|
|
20
29
|
projects: readonly {
|
|
21
30
|
name: string;
|
|
22
31
|
path: string;
|
|
@@ -25,18 +34,32 @@ export type HomeFacts = {
|
|
|
25
34
|
chat: boolean;
|
|
26
35
|
/** This person can answer an agent's question (an approver in a browser session). */
|
|
27
36
|
canAnswer: boolean;
|
|
37
|
+
/** An approver in a browser session (default: `canAnswer`): a release is then read in Approve a release, never
|
|
38
|
+
* approved here; for anyone else it stays an ordinary task row. */
|
|
39
|
+
approver?: boolean;
|
|
40
|
+
/** What else waits on this person (home-asks.ts), already read through each page's own admission. */
|
|
41
|
+
asks?: HomeAsks;
|
|
28
42
|
/** Why this task's plan can't take a yes right now (render-tasks.ts consentDoorOf), or null when it can. */
|
|
29
43
|
closedDoor: (taskId: string) => {
|
|
30
44
|
title: string;
|
|
31
45
|
why: string;
|
|
32
46
|
road: "scope" | "agents";
|
|
33
47
|
} | null;
|
|
48
|
+
/** The lead's last message in this person's main thread, already read through the chat's own access check
|
|
49
|
+
* (pages.ts leadLastMessage); null when there is none or this person may not read that thread. */
|
|
50
|
+
leadMessage?: {
|
|
51
|
+
text: string;
|
|
52
|
+
at: string;
|
|
53
|
+
} | null;
|
|
34
54
|
};
|
|
35
|
-
/** "
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
/** "19 tasks need you", "3 tasks and 2 other things need you", "2 things need you": the heading, said once. Tasks
|
|
56
|
+
* are the same total as the rail's count; `others` are the rows that aren't tasks (a held publication, a flow card, a
|
|
57
|
+
* setup step). */
|
|
58
|
+
export declare function homeSentence(needs: number, any: boolean, others?: number): string;
|
|
59
|
+
/** The lead's words as one plain line: no markdown marks, no line breaks, cut to about one line. */
|
|
60
|
+
export declare function leadLineOf(text: string): string;
|
|
61
|
+
/** Midnight today on this computer's clock: "finished today" starts here. */
|
|
62
|
+
export declare function startOfToday(now: Date): Date;
|
|
63
|
+
/** Home's view for one person and scope, from four reads of the work index: what waits on you (with exact per-ask
|
|
64
|
+
* totals), what runs, what waits its turn, and what was completed today (filtered before its limit). */
|
|
42
65
|
export declare function homeView(facts: HomeFacts): BrowserHomeView;
|