toolroll 0.9.100 → 0.9.102

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.
Files changed (62) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/accent-colors.js +1 -1
  3. package/dist/activity-line.d.ts +1 -1
  4. package/dist/activity-line.js +1 -1
  5. package/dist/browser/terminal.css +1 -1
  6. package/dist/browser/terminal.js +42 -42
  7. package/dist/browser/workspace.css +2 -2
  8. package/dist/browser/workspace.js +40 -40
  9. package/dist/browser-workspace.d.ts +78 -15
  10. package/dist/browser-workspace.js +5 -3
  11. package/dist/design/kit.css +94 -2
  12. package/dist/design/kit.d.ts +12 -4
  13. package/dist/design/kit.js +8 -2
  14. package/dist/design/tokens.css +5 -5
  15. package/dist/desktop-update.d.ts +1 -1
  16. package/dist/first-run.d.ts +10 -6
  17. package/dist/first-run.js +19 -18
  18. package/dist/guides.js +1 -1
  19. package/dist/operate.js +2 -0
  20. package/dist/page-work-contract.d.ts +21 -0
  21. package/dist/page-work-contract.js +5 -0
  22. package/dist/project-knowledge.d.ts +6 -6
  23. package/dist/run-efficiency.d.ts +3 -2
  24. package/dist/run-efficiency.js +4 -3
  25. package/dist/scope.d.ts +5 -0
  26. package/dist/scope.js +8 -1
  27. package/dist/serve.js +51 -9
  28. package/dist/server/chrome.d.ts +11 -2
  29. package/dist/server/chrome.js +10 -1
  30. package/dist/server/console.css +65 -21
  31. package/dist/server/home.d.ts +27 -14
  32. package/dist/server/home.js +78 -63
  33. package/dist/server/http.d.ts +6 -0
  34. package/dist/server/http.js +8 -0
  35. package/dist/server/in-place.d.ts +15 -0
  36. package/dist/server/in-place.js +32 -0
  37. package/dist/server/kit.d.ts +36 -3
  38. package/dist/server/kit.js +33 -4
  39. package/dist/server/pages.js +23 -2
  40. package/dist/server/render-chat.d.ts +2 -0
  41. package/dist/server/render-chat.js +1 -0
  42. package/dist/server/render-pages.js +1 -1
  43. package/dist/server/render-results.js +3 -3
  44. package/dist/server/render-tasks.d.ts +10 -0
  45. package/dist/server/render-tasks.js +245 -65
  46. package/dist/server/request-context.d.ts +14 -0
  47. package/dist/server/request-context.js +3 -0
  48. package/dist/server/task-views.js +11 -4
  49. package/dist/server/tasks.d.ts +8 -2
  50. package/dist/server/tasks.js +31 -17
  51. package/dist/settings-groups-ui.js +1 -1
  52. package/dist/store.js +14 -3
  53. package/dist/task-archive.d.ts +3 -0
  54. package/dist/task-archive.js +5 -0
  55. package/dist/task-status.d.ts +2 -2
  56. package/dist/task-status.js +3 -3
  57. package/dist/tui.js +1 -1
  58. package/dist/work-index.d.ts +14 -1
  59. package/dist/work-index.js +24 -8
  60. package/dist/workspace-ui.d.ts +69 -0
  61. package/dist/workspace-ui.js +103 -0
  62. package/package.json +1 -1
@@ -22,18 +22,19 @@ export type KnowledgeView = ReturnType<typeof knowledgeView>;
22
22
  export declare function knowledgeVersion(store: Store, repo: string, actor: string, revision: number): Knowledge;
23
23
  /** Chat reads only an admitted project. Keep the full source in the project UI. */
24
24
  export declare function conversationKnowledge(store: Store, repo: string, actor: string, reference?: string, decision?: number): {
25
+ title?: never;
26
+ content?: never;
25
27
  history?: never;
26
28
  revision: number;
27
29
  stale: boolean;
28
30
  notice: string;
29
- title?: never;
30
- content?: never;
31
31
  instructions?: never;
32
32
  references?: never;
33
33
  decisions?: never;
34
34
  waiting?: never;
35
35
  } | {
36
- revision?: never;
36
+ title?: never;
37
+ content?: never;
37
38
  history?: never;
38
39
  stale?: never;
39
40
  decision: {
@@ -52,8 +53,7 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
52
53
  };
53
54
  afterwards?: string;
54
55
  notice: string;
55
- title?: never;
56
- content?: never;
56
+ revision?: never;
57
57
  instructions?: never;
58
58
  references?: never;
59
59
  decisions?: never;
@@ -70,9 +70,9 @@ export declare function conversationKnowledge(store: Store, repo: string, actor:
70
70
  decisions?: never;
71
71
  waiting?: never;
72
72
  } | {
73
- stale?: never;
74
73
  title?: never;
75
74
  content?: never;
75
+ stale?: never;
76
76
  revision: number;
77
77
  history: {
78
78
  revision: number;
@@ -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;
@@ -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.d.ts CHANGED
@@ -605,6 +605,11 @@ export type ApproveResult = {
605
605
  | {
606
606
  ok: false;
607
607
  reason: "archived";
608
+ }
609
+ /** The task was cancelled: a cancelled task takes no approval, by any road (a stale page, the CLI). */
610
+ | {
611
+ ok: false;
612
+ reason: "cancelled";
608
613
  };
609
614
  /**
610
615
  * Whether this name-and-token pair is a person the store knows. Shared by
package/dist/scope.js CHANGED
@@ -52,7 +52,7 @@ import { z } from "zod";
52
52
  import {} from "./contracts/acceptance-terms.js";
53
53
  import { parseContract } from "./contracts/contract.js";
54
54
  import { readAcceptance, readScopeTerms, storedRubricSchema } from "./contracts/scope.js";
55
- import { taskArchived } from "./task-archive.js";
55
+ import { taskArchived, taskCancelled } from "./task-archive.js";
56
56
  import { releaseCiHoldOf, releaseCiHoldWords } from "./release-ci-hold.js";
57
57
  import { NO_READINESS, exactModelId, legOf, projectRoute, routeDigestOf, routeFromJson, routeJsonProblem, routeWords, } from "./phase-routing.js";
58
58
  /** Same shape as a runner's credential, for the same reasons. */
@@ -1120,6 +1120,10 @@ chat) {
1120
1120
  // A put-away task (v145) is restored first; sealScopeApproval refuses it too, on every road.
1121
1121
  if (taskArchived(store, taskId))
1122
1122
  return { ok: false, reason: "archived" };
1123
+ // A cancelled task takes no yes either — read here, in the seal's transaction, before any vote or proof is
1124
+ // written: an approval page left open while the task was cancelled elsewhere, or the CLI, approves nothing.
1125
+ if (taskCancelled(store, taskId))
1126
+ return { ok: false, reason: "cancelled" };
1123
1127
  // The digest is required, not optional. An operator reads a scope, somebody
1124
1128
  // rewrites it, and an approval that did not name what it saw would agree to
1125
1129
  // the new one in silence — which is the whole failure this guards.
@@ -1550,6 +1554,9 @@ export function liveReleaseOf(store, taskId, action, now = new Date()) {
1550
1554
  // A put-away task (v145, task-tidy.md), or a put-away anchor, deploys, merges and carries nothing: restore it first.
1551
1555
  if (taskArchived(store, taskId) || taskArchived(store, a.anchorTask))
1552
1556
  return refuse("the release task is put away");
1557
+ // A cancelled task, or a cancelled anchor, the same: cancelling ends what its approval allows.
1558
+ if (taskCancelled(store, taskId) || taskCancelled(store, a.anchorTask))
1559
+ return refuse("the release task was cancelled");
1553
1560
  const newest = newestReleaseGeneration(store, a.repo, a.version, a.pr);
1554
1561
  if (newest === null || newest.id !== a.id)
1555
1562
  return refuse("a newer approval of this release replaced this one");
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
- const touch = !(method === "GET" && (edgeRoute?.domain === 'live' || workspaceRead || (fragmentName !== null && NO_TOUCH_FRAGMENTS.has(fragmentName)) || /^\/code\/[a-f0-9]{32}\/state$/.test(url.pathname) || url.pathname === '/term/state'
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
- response.once("finish", () => keepSlowRequest(store, timeline, matchedRoute?.id ?? "unmatched", response.statusCode, clock()));
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.
@@ -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";
@@ -56,7 +56,7 @@ export declare const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a
56
56
  * a person's accent (accentHead) still wins over the generated default. */
57
57
  export declare const STYLE: string;
58
58
  /** Appearance: a three-way segmented switch, one tap per choice. */
59
- export declare const THEME_CONTROLS_CSS = ".task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed=\"true\"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}";
59
+ export declare const THEME_CONTROLS_CSS = ".task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-neutral-soft);color:var(--so-ink)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed=\"true\"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}";
60
60
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
61
61
  export declare const PAGE_CSS: string;
62
62
  /** The Inbox tabs (console v2): a segmented track of real links; the dot marks a tab with something new, on phones only. */
@@ -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"]>;
@@ -150,7 +150,7 @@ export const STYLE = readFileSync(join(import.meta.dirname, "..", "design", "tok
150
150
  // server pages (server/kit.ts) and React views (browser/kit/) looks the same everywhere.
151
151
  readFileSync(join(import.meta.dirname, "..", "design", "kit.css"), "utf8");
152
152
  /** Appearance: a three-way segmented switch, one tap per choice. */
153
- export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
153
+ export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-neutral-soft);color:var(--so-ink)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
154
154
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
155
155
  export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
156
156
  /** The Inbox tabs (console v2): a segmented track of real links; the dot marks a tab with something new, on phones only. */
@@ -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
+ }
@@ -41,7 +41,7 @@
41
41
  --muted: var(--so-raised); --muted-foreground: var(--so-muted); --border: var(--so-line); --input: var(--so-input-line);
42
42
  --primary: var(--so-accent); --primary-foreground: var(--so-on-accent); --secondary: var(--so-raised);
43
43
  --accent: var(--so-soft); --destructive: var(--so-danger); --destructive-soft: var(--so-danger-soft);
44
- --success: var(--so-success); --success-soft: var(--so-success-soft); --warning: var(--so-warning); --warning-soft: var(--so-warning-soft);
44
+ --success: var(--so-success); --success-soft: var(--so-success-soft); --warning: var(--so-neutral-ink); --warning-soft: var(--so-neutral-soft);
45
45
  --running: var(--so-info); --running-soft: var(--so-info-soft); --ring: var(--so-signal);
46
46
  /* The accent means one thing: a person is needed (approve, answer, decide). */
47
47
  --brand: var(--so-attention); --brand-foreground: var(--so-on-attention); --brand-soft: var(--so-attention-soft);
@@ -141,7 +141,7 @@
141
141
  }
142
142
  .badge-note {
143
143
  background: color-mix(in srgb, var(--muted) 58%, var(--card));
144
- color: var(--so-warning, #ab6400); border-color: var(--border);
144
+ color: var(--so-neutral-ink); border-color: var(--border);
145
145
  }
146
146
  .badge-overdue {
147
147
  background: color-mix(in srgb, var(--muted) 58%, var(--card)); color: var(--foreground);
@@ -173,17 +173,17 @@
173
173
  }
174
174
  .badge-cut { background: var(--muted); }
175
175
  /* The status chip (components/ui/badge.tsx `size="chip"`, after ReUI's light badge with a dot): 20px, 6px
176
- corners, a soft wash, 11.5px words and a 6px dot. Needs you is the amber wash with its words leaned toward ink
177
- (4.5:1); the dot is decoration and the word carries the meaning. */
176
+ corners, a soft wash, 11.5px words and a 6px dot. Needs you is solid ink with paper words and
177
+ no dot; elsewhere the dot is decoration and the word carries the meaning. */
178
178
  .chip {
179
179
  display: inline-flex; align-items: center; gap: .375rem; height: 1.25rem; padding: 0 .375rem; border-radius: var(--so-radius-sm);
180
180
  font-family: var(--font-sans); font-size: .71875rem; font-weight: 500; line-height: 1; white-space: nowrap; vertical-align: middle;
181
181
  background: var(--so-neutral-soft); color: var(--so-ink);
182
182
  }
183
- /* Ink words, the tone in the dot (the Agents → Live chip); Needs you keeps amber words leaned toward ink. */
183
+ /* Ink words, the tone in the dot (the Agents → Live chip); Needs you is solid ink, no dot. */
184
184
  .chip-dot { width: .375rem; height: .375rem; flex-shrink: 0; border-radius: var(--so-radius-full); background: var(--so-neutral-ink); }
185
- .chip-needs { background: var(--so-warning-soft); color: color-mix(in oklab, var(--so-warning) 85%, var(--so-ink)); }
186
- .chip-needs .chip-dot { background: var(--so-warning); }
185
+ .chip-needs { background: var(--so-needs); color: var(--so-on-signal); }
186
+ .chip-needs .chip-dot { display: none; }
187
187
  .chip-live { background: var(--so-info-soft); }
188
188
  .chip-live .chip-dot { background: var(--so-info); }
189
189
  .chip-done { background: var(--so-success-soft); }
@@ -195,7 +195,7 @@
195
195
  .frame { display: flex; flex-direction: column; gap: .25rem; padding: .25rem; border: 1px solid var(--so-line); border-radius: var(--so-radius-card); background: var(--so-raised); }
196
196
  .frame-head { display: flex; align-items: center; justify-content: space-between; gap: .5rem; min-height: 2rem; padding: .25rem .625rem; }
197
197
  .frame-panel { position: relative; border: 1px solid var(--so-line); border-radius: calc(var(--so-radius-card) - .25rem - 1px); background: var(--so-paper); padding: .75rem .875rem; }
198
- .frame-panel.waits { border-color: var(--so-warning); }
198
+ .frame-panel.waits { border-color: var(--so-needs); }
199
199
 
200
200
  .card {
201
201
  border: 1px solid var(--glass-border); border-radius: var(--radius); background: var(--glass);
@@ -1012,7 +1012,7 @@
1012
1012
  .dispatch-status[data-dispatch-status="proof-refuted"] .dispatch-copy > strong::before,
1013
1013
  .dispatch-status[data-dispatch-status="needs-verification"] .dispatch-copy > strong::before {
1014
1014
  content: ""; display: inline-block; width: .45rem; height: .45rem; margin-right: .45rem; border-radius: 50%; vertical-align: .08rem;
1015
- background: var(--so-warning, #ab6400);
1015
+ background: var(--warning);
1016
1016
  }
1017
1017
  .problem.dispatch-status[data-dispatch-status="proof-refuted"] .dispatch-copy > strong::before { background: var(--destructive); }
1018
1018
  .dispatch-status[data-dispatch-status="needs-verification"] .dispatch-copy > strong::before { background: var(--muted-foreground); }
@@ -1110,14 +1110,14 @@
1110
1110
  .contract-panel { margin-top: .8rem; padding: .7rem .85rem; border: 1px solid var(--glass-border); border-radius: calc(var(--radius) - 3px); background: color-mix(in srgb, var(--muted) 45%, transparent); }
1111
1111
  .contract-panel .approval-label { margin: 0 0 .25rem; }
1112
1112
  .contract-panel p { margin: .2rem 0; }
1113
- .contract-amended { border-color: color-mix(in srgb, var(--warning, #c98a1b) 55%, var(--glass-border)); background: color-mix(in srgb, var(--warning, #c98a1b) 9%, transparent); }
1113
+ .contract-amended { border-color: color-mix(in srgb, var(--warning) 55%, var(--glass-border)); background: color-mix(in srgb, var(--warning) 9%, transparent); }
1114
1114
  .contract-reason { font-size: .9rem; }
1115
1115
  .contract-changes { margin: .5rem 0 0; padding-left: 1.1rem; display: grid; gap: .45rem; }
1116
1116
  .contract-changes li { overflow-wrap: anywhere; }
1117
1117
  .contract-change-kind { display: inline-block; margin-right: .3rem; padding: .05rem .4rem; border-radius: 999px; font: 600 .62rem/1.5 var(--font-mono); letter-spacing: .06em; text-transform: uppercase; background: color-mix(in srgb, var(--muted) 70%, transparent); }
1118
1118
  .contract-change-added { background: color-mix(in srgb, var(--success, #2f9e5f) 18%, transparent); }
1119
1119
  .contract-change-removed { background: color-mix(in srgb, var(--danger, #c8453d) 18%, transparent); }
1120
- .contract-change-changed { background: color-mix(in srgb, var(--warning, #c98a1b) 22%, transparent); }
1120
+ .contract-change-changed { background: color-mix(in srgb, var(--warning) 22%, transparent); }
1121
1121
  .contract-before, .contract-after { margin-top: .15rem; font-size: .85rem; }
1122
1122
  .contract-before { color: var(--muted-foreground); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--muted-foreground) 55%, transparent); }
1123
1123
  .contract-note { margin-top: .5rem; }
@@ -1251,6 +1251,35 @@
1251
1251
  .approval-roles dd { margin: 0; }
1252
1252
  .approval-roles dd ul { margin: 2px 0 0; color: var(--muted-foreground); }
1253
1253
  .chat-plan .approval-sheet { padding: 0; border: 0; background: none; }
1254
+ /* The plan sheet (task page, docs/plans/task-page-steps.md): the same terms read top to bottom — one lead sentence,
1255
+ Steps, Done when, Will change / Won't touch, Worth knowing, the agent — every part open, small muted headings. */
1256
+ .plan-sheet { display: grid; gap: 22px; margin: 0; min-width: 0; color: var(--so-ink); }
1257
+ .plan-sheet > * { margin: 0; }
1258
+ .plan-sheet-lead { max-width: 66ch; font-size: 17px; line-height: 1.5; letter-spacing: -.01em; overflow-wrap: anywhere; }
1259
+ .plan-sheet-lead-long { font-size: 15px; line-height: 1.6; letter-spacing: 0; }
1260
+ .plan-sheet-text { max-width: 66ch; font-size: 14px; line-height: 1.65; overflow-wrap: anywhere; white-space: pre-wrap; }
1261
+ .plan-sheet-part { display: grid; align-content: start; gap: var(--so-space-sm); min-width: 0; max-width: 75ch; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; }
1262
+ .plan-sheet-part > *, .plan-sheet-part p, .plan-sheet-part ul, .plan-sheet-part ol, .plan-sheet-part dl { margin: 0; }
1263
+ .plan-sheet-h { margin: 0; font-size: 12px; font-weight: 500; line-height: 1.5; letter-spacing: 0; text-transform: none; color: var(--so-muted); }
1264
+ .plan-sheet-sub { color: var(--so-muted); font-size: 12.5px; }
1265
+ .plan-sheet-steps { display: grid; gap: var(--so-space-sm); padding: 0; list-style: none; counter-reset: plan-step; }
1266
+ .plan-sheet-steps > li { display: flex; gap: var(--so-space-md); counter-increment: plan-step; }
1267
+ .plan-sheet-steps > li::before { content: counter(plan-step); flex: 0 0 22px; box-sizing: border-box; width: 22px; height: 22px; border: 1px solid var(--so-line); border-radius: 50%; display: grid; place-items: center; font-size: 12px; color: var(--so-muted); font-variant-numeric: tabular-nums; }
1268
+ .plan-sheet-steps > li > span { min-width: 0; padding-top: 1px; }
1269
+ .plan-sheet-done { display: grid; padding: 0; list-style: none; }
1270
+ .plan-sheet-done > li { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--so-space-xs) var(--so-space-md); padding: var(--so-space-sm) 0; border-bottom: 1px solid var(--so-line); }
1271
+ .plan-sheet-done > li:last-child { border-bottom: 0; }
1272
+ .plan-sheet-done > li > span:first-child { flex: 1 1 260px; min-width: 0; }
1273
+ .plan-sheet-how { flex: none; font-size: 12.5px; color: var(--so-muted); }
1274
+ .plan-sheet-pair { display: flex; flex-wrap: wrap; gap: var(--so-space-lg) 40px; }
1275
+ .plan-sheet-pair > .plan-sheet-part { flex: 1 1 220px; }
1276
+ .plan-sheet .approval-paths li { color: var(--so-ink); font-size: 13px; line-height: 1.7; }
1277
+ .plan-sheet-list { display: grid; gap: var(--so-space-xs); padding-left: 1.25rem; }
1278
+ .plan-sheet-warn { color: var(--so-ink); }
1279
+ .plan-sheet .approval-amendment { margin: 0; }
1280
+ .plan-sheet .approval-who, .plan-sheet .approval-evidence, .plan-sheet .approval-allowing, .plan-sheet .approval-you-check { margin: 0; }
1281
+ .plan-sheet .approval-roles { margin: 0; }
1282
+ .plan-sheet .revision-card { margin: 0; padding: 0; border: 0; background: none; }
1254
1283
  @media (max-width: 760px) {
1255
1284
  .approval-sheet { gap: var(--so-space-md); padding: 14px; }
1256
1285
  .approval-row { grid-template-columns: 1fr; gap: 2px; }
@@ -1564,7 +1593,7 @@
1564
1593
  .status-line { display: inline-flex; align-items: center; gap: .45rem; min-width: 0; font-size: .875rem; font-weight: 600; color: var(--foreground); }
1565
1594
  .status-line .status-label { min-width: 0; overflow-wrap: anywhere; }
1566
1595
  .status-line .status-dot { flex: none; width: .5rem; height: .5rem; border-radius: 50%; background: var(--muted-foreground); }
1567
- .status-line[data-tone="attention"] .status-dot { background: var(--warning); }
1596
+ .status-line[data-tone="attention"] .status-dot { background: var(--so-needs); }
1568
1597
  .status-line[data-tone="problem"] .status-dot { background: var(--destructive); }
1569
1598
  .status-line[data-tone="live"] .status-dot { background: var(--running); box-shadow: 0 0 0 3px var(--running-soft); }
1570
1599
  .status-line[data-tone="ready"] .status-dot, .status-line[data-tone="done"] .status-dot { background: var(--success); }
@@ -1577,7 +1606,7 @@
1577
1606
  .pull-request-head strong { font-size: 1.0625rem; font-weight: 600; color: var(--foreground); display: inline-flex; align-items: center; gap: .5rem; }
1578
1607
  .pull-request-head strong::before { content: ""; width: .5rem; height: .5rem; border-radius: 999px; background: var(--muted-foreground); }
1579
1608
  .pull-request--ok .pull-request-head strong::before { background: var(--success, #15803d); }
1580
- .pull-request--problem .pull-request-head strong::before { background: var(--so-warning, #ab6400); }
1609
+ .pull-request--problem .pull-request-head strong::before { background: var(--foreground); }
1581
1610
  .pull-request-act { display: flex; flex-wrap: wrap; align-items: flex-end; gap: .5rem .75rem; margin: .25rem 0 0; }
1582
1611
  .pull-request-act .meta { flex: 1 1 100%; }
1583
1612
  .pull-request-act label { display: grid; gap: .25rem; flex: 1 1 12rem; margin: 0; }
@@ -1813,7 +1842,7 @@
1813
1842
  }
1814
1843
  /* A lane is a frame (components/ui/frame.tsx, after ReUI's Frame): a raised well with a hairline, its head a
1815
1844
  status chip and a mono count on the well's fill, its cards paper panels at concentric corners. Only the lane
1816
- that waits on a person is amber: its chip, and each card's thin full outline. No fill, no stripe, no shadow. */
1845
+ that waits on a person is ink: its chip, and each card's thin full outline. No fill, no stripe, no shadow. */
1817
1846
  .lane {
1818
1847
  background: var(--so-raised); border: 1px solid var(--so-line);
1819
1848
  border-radius: var(--so-radius-card); padding: .25rem; min-height: 12rem;
@@ -1830,7 +1859,7 @@
1830
1859
  background: var(--so-paper); border: 1px solid var(--so-line);
1831
1860
  border-radius: calc(var(--so-radius-card) - .25rem - 1px); padding: .625rem .75rem; margin-top: .25rem;
1832
1861
  }
1833
- .lane-attention .lane-card { border-color: var(--so-warning); }
1862
+ .lane-attention .lane-card { border-color: var(--so-needs); }
1834
1863
  /* The order view's columns are frames too, with roomier insides (a form, a note) and a quiet dot by the name. */
1835
1864
  .lane.queue-column { padding: .5rem; }
1836
1865
  .queue-column > h2::before { content: ""; width: .375rem; height: .375rem; border-radius: var(--so-radius-full); flex: none; background: var(--muted-foreground); opacity: .55; }
@@ -2633,14 +2662,29 @@ button { min-height: 44px; }
2633
2662
  the same on both, swaps in place instead of fading (each shell names its
2634
2663
  own, so a page can never carry the name twice). */
2635
2664
  ::view-transition-old(root) { animation: so-page-leave 80ms ease-in both; }
2636
- ::view-transition-new(root) { animation: so-page-arrive 140ms ease-out 60ms both; }
2665
+ ::view-transition-new(root) { animation: so-page-arrive 160ms ease-out 60ms both; }
2637
2666
  @keyframes so-page-leave { to { opacity: 0; } }
2638
2667
  @keyframes so-page-arrive { from { opacity: 0; } }
2639
2668
  .side { view-transition-name: so-nav-page; }
2640
2669
  .so-sidebar { view-transition-name: so-nav-app; }
2641
- ::view-transition-group(so-nav-page), ::view-transition-group(so-nav-app),
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),
2642
2672
  ::view-transition-old(so-nav-page), ::view-transition-new(so-nav-page),
2643
- ::view-transition-old(so-nav-app), ::view-transition-new(so-nav-app) { animation: none; }
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); } }
2644
2688
  /* UI polish 2026-09-13 — the motion contract. Feedback transitions run
2645
2689
  140–200 ms on color, border, shadow, opacity, and transform only; an
2646
2690
  overlay's entrance is at most 220 ms of opacity + transform. Nothing
@@ -2670,7 +2714,7 @@ button { min-height: 44px; }
2670
2714
  @keyframes fade { from { opacity: 0; } }
2671
2715
  @media (prefers-reduced-motion: reduce) {
2672
2716
  *, *::before, *::after { animation: none !important; transition: none !important; }
2673
- ::view-transition-group(*), ::view-transition-old(root), ::view-transition-new(root) { animation: none; }
2717
+ ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
2674
2718
  button:active, :is(.button-link, .side .new-task, .content .new-task, .result-feedback-link, .next-pager .skip):active { transform: none; }
2675
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; } }
2676
2720
  }
@@ -2854,9 +2898,9 @@ html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms e
2854
2898
  .tc-c0 { color: var(--so-ink); stroke: var(--so-ink); }
2855
2899
  .tc-c1 { color: var(--so-info); stroke: var(--so-info); }
2856
2900
  .tc-c2 { color: var(--so-success); stroke: var(--so-success); }
2857
- .tc-c3 { color: var(--so-warning); stroke: var(--so-warning); }
2901
+ .tc-c3 { color: var(--so-muted); stroke: var(--so-muted); }
2858
2902
  .tc-c4 { color: var(--so-danger); stroke: var(--so-danger); }
2859
- .tc-c5 { color: var(--so-muted); stroke: var(--so-muted); }
2903
+ .tc-c5 { color: var(--so-input-line); stroke: var(--so-input-line); }
2860
2904
  .tc-line { margin: 0; display: grid; gap: var(--so-space-xs); }
2861
2905
  .tc-line svg { width: 100%; height: 90px; }
2862
2906
  .tc-line figcaption { font-size: 11.5px; color: var(--so-muted); }
@@ -1,14 +1,20 @@
1
1
  /**
2
- * Home ("/", UI revamp wave B): one sentence that answers "what needs me?", four counts read from the same work index
3
- * the Tasks list filters (so a count and the list it opens always agree), the work in three groups (Waiting on you,
4
- * Working, Done; home-groups.ts), and the lead's message box. Reads only: every act keeps its own route and rules (an agent's
5
- * question is answered through POST /d/<id>/answer, the same one-click act as its own page).
2
+ * Home ("/", docs/plans/live-pages-and-home.md B1): what needs this person, grouped by what is asked (Approve a plan,
3
+ * Answer a question, Something went wrong, Review results, and Needs you for the rest), each group with its exact
4
+ * 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
- /** How many waiting rows Home lists; the rest open in Tasks. */
12
+ /** How many waiting rows Home lists; each group says how many more open in Tasks. */
11
13
  export declare const HOME_NEEDS_SHOWN = 30;
14
+ /** How many rows finished today Home lists; its figure counts them all. */
15
+ export declare const HOME_DONE_SHOWN = 8;
16
+ /** Each group's heading, in the order Home shows them. */
17
+ export declare const HOME_ASK_LABEL: Readonly<Record<BrowserHomeAsk, string>>;
12
18
  export type HomeFacts = {
13
19
  store: Store;
14
20
  now: Date;
@@ -16,7 +22,7 @@ export type HomeFacts = {
16
22
  evidenceRoot?: string;
17
23
  /** The project in view (already admitted), or null for every project. */
18
24
  project: string | null;
19
- /** Every admitted project, for the scope links. */
25
+ /** Every admitted project, for the project filter. */
20
26
  projects: readonly {
21
27
  name: string;
22
28
  path: string;
@@ -31,12 +37,19 @@ export type HomeFacts = {
31
37
  why: string;
32
38
  road: "scope" | "agents";
33
39
  } | null;
40
+ /** The lead's last message in this person's main thread, already read through the chat's own access check
41
+ * (pages.ts leadLastMessage); null when there is none or this person may not read that thread. */
42
+ leadMessage?: {
43
+ text: string;
44
+ at: string;
45
+ } | null;
34
46
  };
35
- /** "7 need you across 2 projects." — the sentence and the figures read the same totals. */
36
- export declare function homeSentence(needs: number, perProject: readonly {
37
- name: string;
38
- needs: number;
39
- }[], scope: string | null, any: boolean): string;
40
- /** Home's view for one person and scope, from four reads of the work index (what waits on you, what runs, what waits on
41
- * Toolroll, what was done), grouped by the one rule every surface shares (home-groups.ts). */
47
+ /** "19 tasks need you": the heading, said once, from the same total as the rail's count. */
48
+ export declare function homeSentence(needs: number, any: boolean): string;
49
+ /** The lead's words as one plain line: no markdown marks, no line breaks, cut to about one line. */
50
+ export declare function leadLineOf(text: string): string;
51
+ /** Midnight today on this computer's clock: "finished today" starts here. */
52
+ export declare function startOfToday(now: Date): Date;
53
+ /** Home's view for one person and scope, from four reads of the work index: what waits on you (with exact per-ask
54
+ * totals), what runs, what waits its turn, and what was completed today (filtered before its limit). */
42
55
  export declare function homeView(facts: HomeFacts): BrowserHomeView;