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
@@ -8,7 +8,8 @@ import type { AssignmentSnapshot } from './assignment.js';
8
8
  export { browserCrewFromIndex, browserWorkActionHref } from './browser-crew.js';
9
9
  import type { Store } from './store.js';
10
10
  import type { WorkSummaryAccess } from './work-summary.js';
11
- import type { StatusTone } from './workspace-ui.js';
11
+ import type { StatusTone, StepRow } from './workspace-ui.js';
12
+ import type { PageWork } from './page-work-contract.js';
12
13
  import type { Ask, AskChip } from './needs-you.js';
13
14
  import type { WorkIndexGroup } from './work-index.js';
14
15
  import type { AssignmentCard } from './assignment-ui.js';
@@ -414,6 +415,24 @@ export type BrowserTaskView = {
414
415
  line: string;
415
416
  action: BrowserLink | null;
416
417
  } | null;
418
+ /** The plan's steps and where each stands, from the latest checkpoint (all pending before the first). */
419
+ steps?: {
420
+ title: string;
421
+ state: "pending" | "current" | "completed" | "blocked";
422
+ }[];
423
+ } | null;
424
+ /** The Plan row: the plan's lead sentence, who wrote it and when, and its body (the plan sheet and the plan's editor;
425
+ * only the editor while the approval shows the sheet). `planning`: a planner is writing it now. Null: no plan yet.
426
+ * `key` names what the body holds (the editor alone, or the sheet of one exact plan): the page installs the body once
427
+ * and again when the key changes. */
428
+ plan?: {
429
+ lead: string | null;
430
+ by: string | null;
431
+ at: string | null;
432
+ html: string;
433
+ planning: boolean;
434
+ updating?: boolean;
435
+ key: string;
417
436
  } | null;
418
437
  /** What the agent did last on the live run, and when; the page words it against its own clock (activity-line.ts). */
419
438
  activity?: RunActivity | null;
@@ -422,6 +441,14 @@ export type BrowserTaskView = {
422
441
  room: string;
423
442
  at: string | null;
424
443
  } | null;
444
+ /** The work panel's run (docs/plans/task-page-steps.md "Watch the work"): the live build, else the latest one, who ran it,
445
+ * and its records window on the run page (null on a page that asks for a password). Null: no build yet. */
446
+ work?: {
447
+ run: number;
448
+ live: boolean;
449
+ agent: string;
450
+ transcript: string | null;
451
+ } | null;
425
452
  /** Stop, on the Building card: the exact live build's stop form (posts its run id). */
426
453
  stop?: {
427
454
  action: string;
@@ -460,9 +487,12 @@ export type BrowserTaskView = {
460
487
  }[];
461
488
  } | null;
462
489
  /** Server cards that may need a person now (stop/resume, scope prompt, plan, live attempt). */
490
+ /** `instance`: what the block is about now (the attempt card: its build). The page installs a block's HTML once;
491
+ * another instance replaces it. */
463
492
  lead: {
464
493
  key: string;
465
494
  html: string;
495
+ instance?: string;
466
496
  }[];
467
497
  questions: string;
468
498
  facts: BrowserTaskFact[];
@@ -478,8 +508,11 @@ export type BrowserTaskView = {
478
508
  href: string;
479
509
  starter: string;
480
510
  } | null;
481
- /** Until the first Ready result: Plan → You approve → Build → Checks → Ready, filled in as it moves. */
511
+ /** The task's road, Plan → Approval → Build → Checks → Ready (four for a report), from recorded facts. */
482
512
  journey?: JourneyStep[] | null;
513
+ /** The page's step rows (workspace-ui.ts stepRowsOf over this view, after every change the server made to it): the
514
+ * steps are the page. Null: no road (cancelled, or no status), so the page keeps its status block. */
515
+ steps?: StepRow[] | null;
483
516
  /** The thread's server-side entries; the conversation's messages join them in time order. */
484
517
  thread?: BrowserTaskThreadItem[];
485
518
  details?: BrowserTaskDetailGroup[];
@@ -520,6 +553,8 @@ export type BrowserTaskView = {
520
553
  /** When a remembered password stops counting (ISO); the page words it in the viewer's zone. */
521
554
  rememberedUntil: string | null;
522
555
  planHtml: string;
556
+ /** The plan sheet alone (no editor, no form): what Home shows beside its Approve (live-pages-and-home.md B2.1). */
557
+ sheetHtml?: string;
523
558
  /** The terms line in words, when the rule isn't one of the kit's (easy-start C5: a laptop's own owner). */
524
559
  termsWords?: string;
525
560
  } | null;
@@ -1584,6 +1619,15 @@ export type BrowserWorkspace = {
1584
1619
  user: string;
1585
1620
  csrf: string;
1586
1621
  sensitive: boolean;
1622
+ /** True when this page may replace another without a document load (docs/plans/live-pages-and-home.md A1.1): the server
1623
+ * sets it on every workspace it sends; absent (an older server, a fixture) is false. */
1624
+ inPlace?: boolean;
1625
+ /** What the app runs for this page in place of a page script (docs/plans/task-page-in-app.md): data, a closed list.
1626
+ * Present only when the app covers the page's whole script; the document then ships no page script string. */
1627
+ work?: readonly PageWork[];
1628
+ /** This document ships a page script string (set on a document load only). Its listeners live as long as the
1629
+ * document, so the app never changes pages in place from it, and never runs `work` in it. */
1630
+ scripted?: boolean;
1587
1631
  /** Passwordless is on for this signed-in browser: ceremony password fields are left out (docs/plans/passwordless.md). */
1588
1632
  passwordless?: boolean;
1589
1633
  /** The session's open project (its path), or null: the sidebar's project picker names it. */
@@ -1659,13 +1703,14 @@ export type BrowserWorkspace = {
1659
1703
  command: string;
1660
1704
  };
1661
1705
  };
1662
- /** Home ("/"): one sentence that answers "what needs me?", four counts that agree with the Tasks filters they open,
1663
- * the work grouped as Waiting on you, Working and Done, and the selected item answered in place. */
1706
+ /** Home ("/", docs/plans/live-pages-and-home.md B1): the heading said once, one quiet line for what runs and waits, what
1707
+ * needs this person grouped by what is asked (each group with its exact count), the selected one open beside the list,
1708
+ * and the lead's last message above the message box. With nothing waiting: what runs and what finished today. */
1664
1709
  export type BrowserHomeView = {
1665
1710
  kind: 'home';
1666
- /** "7 need you across 2 projects.", derived from the same counts as the figures. */
1711
+ /** "19 tasks need you", "Nothing needs you", read from the same total as the rail's count. */
1667
1712
  say: string;
1668
- /** The project in view (a path) or every project (null), and the scope links (null with one project). */
1713
+ /** The project in view (a path) or every project (null), and the project filter's choices (null with one project). */
1669
1714
  project: string | null;
1670
1715
  scopes: {
1671
1716
  key: string;
@@ -1673,29 +1718,42 @@ export type BrowserHomeView = {
1673
1718
  href: string;
1674
1719
  active: boolean;
1675
1720
  }[] | null;
1721
+ /** The work index totals, each with the Tasks list it opens. */
1676
1722
  counts: {
1677
1723
  key: 'needs' | 'running' | 'queued' | 'finished';
1678
1724
  label: string;
1679
1725
  value: number;
1680
1726
  href: string;
1681
1727
  }[];
1682
- /** The work in three groups, in this order (home-groups.ts): Waiting on you, Working, Done (finished in the last 7 days). */
1728
+ /** What needs you, by what is asked, in this order: approve, answer, wrong, review, other (anything none of the four
1729
+ * fits). `count` is the exact total for that ask, past the rows listed. */
1683
1730
  groups: {
1684
- key: 'you' | 'working' | 'done';
1731
+ key: BrowserHomeAsk;
1685
1732
  label: string;
1733
+ count: number;
1686
1734
  rows: BrowserHomeRow[];
1687
1735
  }[];
1688
- /** More waits on you than the list shows: open them all in Tasks. */
1689
- more: {
1736
+ /** What runs, then what waits its turn. */
1737
+ working: BrowserHomeRow[];
1738
+ /** What was completed today: the exact figure, the newest rows, and the list that has them all. */
1739
+ finishedToday: {
1690
1740
  count: number;
1741
+ rows: BrowserHomeRow[];
1691
1742
  href: string;
1692
- } | null;
1743
+ };
1693
1744
  newTask: string;
1694
1745
  /** The lead's message box, where chat is allowed for this person: a POST to /chat in this scope. */
1695
1746
  ask: {
1696
1747
  project: string | null;
1697
1748
  placeholder: string;
1698
1749
  } | null;
1750
+ /** The lead's last message in this person's main thread, as one plain line that opens the chat; null when there is
1751
+ * none or this person may not read that thread. */
1752
+ lastLead: {
1753
+ text: string;
1754
+ age: string;
1755
+ href: string;
1756
+ } | null;
1699
1757
  empty: {
1700
1758
  title: string;
1701
1759
  text: string;
@@ -1703,16 +1761,20 @@ export type BrowserHomeView = {
1703
1761
  /** While the first run is open (no first result yet): the welcome's address, linked as Finish setup. */
1704
1762
  setup?: string | null;
1705
1763
  };
1764
+ /** What a waiting row asks (work-index.ts WorkAskGroup). */
1765
+ export type BrowserHomeAsk = 'approve' | 'answer' | 'wrong' | 'review' | 'other';
1706
1766
  export type BrowserHomeRow = {
1707
1767
  id: string;
1708
1768
  title: string;
1709
1769
  href: string;
1710
1770
  project: string | null;
1711
1771
  age: string;
1712
- chip: {
1713
- word: string;
1714
- tone: '' | 'needs' | 'ask' | 'live' | 'done' | 'failed';
1715
- };
1772
+ /** Which list it is in: it needs you, it runs, it waits its turn, or it finished. */
1773
+ state: 'needs' | 'running' | 'queued' | 'done';
1774
+ /** What it asks of you (its group), or null when it doesn't wait on you. */
1775
+ ask: BrowserHomeAsk | null;
1776
+ /** Why, in one short line: only a row where something went wrong. */
1777
+ reason: string | null;
1716
1778
  /** One line: what it asks, or what the agent is doing now. */
1717
1779
  line: string | null;
1718
1780
  /** The one act that resolves it (a link), or null. */
@@ -1904,3 +1966,4 @@ export type MergeReadyView = {
1904
1966
  act: MergeReadyAct | null;
1905
1967
  }[];
1906
1968
  };
1969
+ export type { PageWork } from './page-work-contract.js';
@@ -97,11 +97,13 @@ export function browserProjectsOf(projects) {
97
97
  export function browserNavigationOf(path, project = null, needsYou = 0, tasksProject = project, needsYouLabel) {
98
98
  const pathname = path.split('?')[0].split('#')[0];
99
99
  const under = (...roots) => roots.some(root => pathname === root || pathname.startsWith(`${root}/`));
100
- // The rail (UI revamp, wave B): Home carries the one Needs-you count; the lead chat and an agent's question live
101
- // under Home, a task's pages under Tasks, every computer and terminal under Agents, every setting under Settings.
100
+ // The rail (UI revamp, wave B; Chat under Home, live-pages-and-home.md B1): Home carries the one Needs-you count and
101
+ // an agent's question lives under it; the lead chat is its own item; a task's pages under Tasks, every computer and
102
+ // terminal under Agents, every setting under Settings.
102
103
  return [
103
- { label: 'Home', href: '/', active: pathname === '/' || pathname === '/chat' || under('/d', '/activity', '/morning'),
104
+ { label: 'Home', href: '/', active: pathname === '/' || under('/d', '/activity', '/morning'),
104
105
  ...(needsYou > 0 ? { count: needsYou, ...(needsYouLabel === undefined ? {} : { countLabel: needsYouLabel }) } : {}) },
106
+ { label: 'Chat', href: '/chat', active: under('/chat') },
105
107
  { label: 'Tasks', href: `/tasks${tasksProject === null ? '' : `?project=${encodeURIComponent(tasksProject)}`}`,
106
108
  active: under('/tasks', '/t', '/r', '/review') },
107
109
  { label: 'Flows', href: '/flows', active: under('/flows', '/recipes', '/kits', '/routines') },
@@ -19,7 +19,7 @@
19
19
  * stays here: status words on their soft wash, mixed toward ink so they read at 4.5:1 or better, and the motion
20
20
  * vocabulary (three durations, one curve; nothing longer, no bounce). */
21
21
  :root {
22
- --so-needs-ink: color-mix(in srgb, var(--so-needs) 78%, var(--so-ink));
22
+ --so-needs-ink: var(--so-ink);
23
23
  --so-live-ink: color-mix(in srgb, var(--so-info) 82%, var(--so-ink));
24
24
  --so-done-ink: color-mix(in srgb, var(--so-success) 80%, var(--so-ink));
25
25
  --so-fail-ink: color-mix(in srgb, var(--so-danger) 85%, var(--so-ink));
@@ -64,7 +64,8 @@
64
64
  /* ---------- Status: five words, one tone each ---------- */
65
65
  .k-chip { display: inline-flex; align-items: center; gap: 6px; height: 20px; padding: 0 7px; border-radius: 6px; font: var(--so-typography-badge); background: var(--so-neutral-soft); color: var(--so-neutral-ink); white-space: nowrap; flex: none; }
66
66
  .k-chip::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--dot, var(--so-muted)); flex: none; }
67
- .k-chip.needs { background: var(--so-needs-soft); color: var(--so-needs-ink); --dot: var(--so-needs); }
67
+ .k-chip.needs { background: var(--so-needs); color: var(--so-on-signal); }
68
+ .k-chip.needs::before { display: none; }
68
69
  .k-chip.ask { background: var(--so-neutral-soft); color: var(--so-ink); --dot: var(--so-needs); }
69
70
  .k-chip.live { background: var(--so-info-soft); color: var(--so-live-ink); --dot: var(--so-info); }
70
71
  .k-chip.done { background: var(--so-success-soft); color: var(--so-done-ink); --dot: var(--so-success); }
@@ -255,6 +256,82 @@
255
256
  .k-step-l { line-height: 1.2; white-space: nowrap; }
256
257
  .k-step-s { margin-top: -4px; min-height: 1.3em; font-size: 11.5px; font-weight: 400; line-height: 1.3; color: var(--so-muted); white-space: nowrap; }
257
258
 
259
+ /* ---------- Step rows: the steps are the page ---------- */
260
+ .k-steprows { display: flex; flex-direction: column; margin: 0; padding: 0; list-style: none; }
261
+ .k-steprow { position: relative; display: grid; grid-template-columns: 20px minmax(0, 1fr); column-gap: 14px; margin: 0; padding: 0; }
262
+ .k-steprow::before { content: ""; position: absolute; left: 9.5px; top: 24px; bottom: 4px; width: 1px; background: var(--so-line); }
263
+ .k-steprow:last-child::before { content: none; }
264
+ .k-steprow-mark { position: relative; z-index: 1; box-sizing: border-box; display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; border: 1.5px solid var(--so-field-line); background: var(--so-paper); }
265
+ .k-steprow.done .k-steprow-mark { border-color: var(--so-ink); background: var(--so-ink); }
266
+ .k-steprow.done .k-steprow-mark::after { content: ""; width: 4px; height: 8px; margin-top: -2px; border: solid var(--so-paper); border-width: 0 1.6px 1.6px 0; transform: rotate(45deg); }
267
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-mark { border-width: 2px; }
268
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-mark::after { content: ""; width: 8px; height: 8px; border-radius: 50%; background: currentColor; }
269
+ .k-steprow.needs .k-steprow-mark { border-color: var(--so-needs); color: var(--so-needs); }
270
+ .k-steprow.live .k-steprow-mark { border-color: var(--so-info); color: var(--so-info); }
271
+ .k-steprow.wait .k-steprow-mark { border-color: var(--so-muted); color: var(--so-muted); }
272
+ .k-steprow.stuck .k-steprow-mark { border-color: var(--so-danger); color: var(--so-danger); }
273
+ .k-steprow.skipped .k-steprow-mark { border-style: dashed; }
274
+ .k-steprow-main { min-width: 0; display: grid; align-content: start; gap: 12px; padding-bottom: 22px; }
275
+ .k-steprow:last-child .k-steprow-main { padding-bottom: 0; }
276
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-main { padding-bottom: 26px; }
277
+ .k-steprow-head { display: flex; align-items: baseline; gap: 12px; width: 100%; min-width: 0; min-height: 20px; margin: 0; padding: 0; border: 0; border-radius: 6px; background: none; color: inherit; font: 400 13px/20px var(--so-font-sans); text-align: left; }
278
+ button.k-steprow-head { cursor: pointer; transition: color var(--m-quick) var(--m-ease); }
279
+ button.k-steprow-head::after { content: ""; flex: none; align-self: center; width: 6px; height: 6px; margin: 0 4px 0 2px; border: solid var(--so-muted); border-width: 0 1.5px 1.5px 0; transform: rotate(-45deg); transition: transform var(--m-quick) var(--m-ease); }
280
+ button.k-steprow-head[aria-expanded="true"]::after { transform: rotate(45deg); }
281
+ .k-steprow-label { flex: 0 0 84px; min-width: 0; font: 600 14px/20px var(--so-font-sans); color: var(--so-ink); }
282
+ .k-steprow-say { flex: 1 1 auto; min-width: 0; display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; color: var(--so-muted); overflow-wrap: anywhere; }
283
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-say { color: var(--so-ink); }
284
+ .k-steprow-time { flex: none; color: var(--so-muted); font-size: 12.5px; }
285
+ .k-steprow.todo .k-steprow-label { font-weight: 500; color: var(--so-muted); }
286
+ .k-steprow-body { display: grid; gap: 16px; min-width: 0; }
287
+ .k-steprow-body[hidden] { display: none; }
288
+ /* The one ink outline of a step that waits on the viewer is the row's; a waiting box inside it draws none of its own. */
289
+ .k-steprow.needs > .k-steprow-main > .k-steprow-body { border: 1px solid var(--so-needs); border-radius: var(--so-radius-card); padding: 16px 18px 18px; }
290
+ .k-steprow.needs > .k-steprow-main > .k-steprow-body .k-slots.wait { border: 0; border-radius: 0; padding: 0; }
291
+ .k-steprow-body .k-slot-status h2 { color: var(--so-ink); }
292
+ .k-steprow-body .k-slot-acts:empty { display: none; }
293
+ @media (hover: hover) and (pointer: fine) {
294
+ button.k-steprow-head:hover .k-steprow-say { color: var(--so-ink); }
295
+ }
296
+
297
+ /* ---------- The work: one agent's work as a chat ---------- */
298
+ .k-work { box-sizing: border-box; display: flex; flex-direction: column; min-width: 0; min-height: 0; background: var(--so-paper); color: var(--so-ink); }
299
+ .k-work[hidden] { display: none; }
300
+ .k-work-head { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 12px 10px 12px 18px; border-bottom: 1px solid var(--so-line); }
301
+ .k-work-title { display: grid; gap: 1px; min-width: 0; }
302
+ .k-work-title h2 { margin: 0; font: 600 14px/1.4 var(--so-font-sans); }
303
+ .k-work-who { display: flex; align-items: center; gap: 6px; min-width: 0; margin: 0; font-size: 12px; line-height: 1.4; color: var(--so-muted); }
304
+ .k-work-who > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
305
+ .k-work-dot { flex: none; width: 6px; height: 6px; border-radius: 50%; background: var(--so-live); }
306
+ .k-work-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: grid; align-content: start; gap: 16px; padding: 18px; }
307
+ .k-work-foot { flex: none; display: grid; gap: 8px; padding: 8px 14px 14px; max-height: min(50%, 420px); overflow-y: auto; overscroll-behavior: contain; }
308
+ .k-work-foot:empty { display: none; }
309
+ .k-work-conv { display: grid; gap: var(--so-space-sm); min-width: 0; }
310
+ .k-work-conv-h { display: flex; justify-content: flex-end; }
311
+ .k-work-conv .k-work-tx { max-height: 60vh; overflow-y: auto; overscroll-behavior: contain; }
312
+ .k-work-tx { min-width: 0; display: grid; gap: 12px; }
313
+ .k-work-empty { margin: 0; padding: var(--so-space-md) 0; font-size: 13px; color: var(--so-muted); }
314
+ .k-work-lines { display: grid; gap: 14px; margin: 0; padding: 0; list-style: none; font-size: 14px; line-height: 1.6; }
315
+ .k-work-say { white-space: pre-wrap; overflow-wrap: anywhere; }
316
+ .k-work-mark { font-size: 12px; color: var(--so-muted); }
317
+ .k-work-steps { display: grid; gap: 2px; margin: 0; padding: 0; list-style: none; font-size: 13px; line-height: 1.5; color: var(--so-muted); }
318
+ .k-work-step-l { display: flex; align-items: center; gap: 10px; min-height: 26px; min-width: 0; }
319
+ .k-work-step.running .k-work-step-l { color: var(--so-ink); }
320
+ .k-work-step-t { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
321
+ .k-work-step-t .k-mono { font-size: 12px; color: var(--so-ink); }
322
+ .k-work-step-s { flex: none; display: inline-flex; align-items: center; gap: var(--so-space-xs); font-size: 12px; color: var(--so-muted); }
323
+ .k-work-step-s.ok { color: var(--so-done-ink); }
324
+ .k-work-step-s.failed { color: var(--so-fail-ink); }
325
+ .k-work-step-s.running { color: var(--so-live-ink); }
326
+ .k-work-out { max-height: 288px; overflow: auto; margin: 4px 0 6px; padding: var(--so-space-sm) 10px; border-radius: var(--so-radius-md); background: var(--so-code-bg); color: var(--so-ink); font: 12px/1.5 var(--so-font-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
327
+ .k-work-note { display: grid; justify-items: end; gap: 2px; }
328
+ .k-work-bubble { max-width: 82%; margin: 0; padding: 8px 14px; border-radius: 18px; background: var(--so-user-bubble); white-space: pre-wrap; overflow-wrap: anywhere; }
329
+ .k-work-note-s { margin: 0; font-size: 12px; color: var(--so-muted); }
330
+ .k-work-term { display: grid; gap: 2px; margin: 0; padding: 10px var(--so-space-md); list-style: none; border-radius: var(--so-radius-card); background: var(--so-code-bg); color: var(--so-ink); font: 12px/1.45 var(--so-font-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
331
+ .k-work-term-note { color: var(--so-live-ink); }
332
+ .k-work-term-out { display: block; padding-left: var(--so-space-md); }
333
+ .k-work-term-failed { color: var(--so-fail-ink); }
334
+
258
335
  /* ---------- Approval: four fixed slots in every state ---------- */
259
336
  .k-slots { border: 1px solid var(--so-line); border-radius: var(--so-radius-card); padding: 16px 18px; display: grid; gap: 10px; background: var(--so-paper); transition: border-color var(--m-slow) var(--m-ease); }
260
337
  .k-slots.wait { border-color: var(--so-needs); }
@@ -521,6 +598,21 @@ html:not(.k-js) .k-composer:focus-within, html.k-kbd .k-composer:focus-within {
521
598
  .k-card { padding: 12px 16px; }
522
599
  .k-step { font-size: 11px; }
523
600
  .k-step-s { font-size: 10.5px; white-space: normal; }
601
+ .k-steprow-head { flex-wrap: wrap; row-gap: 2px; min-height: 44px; padding: 12px 0; }
602
+ .k-steprow-mark { margin-top: 12px; }
603
+ .k-steprow::before { top: 36px; }
604
+ .k-steprow-main { gap: 8px; padding-bottom: 8px; }
605
+ .k-steprow:is(.needs, .live, .wait, .stuck) .k-steprow-main { padding-bottom: 16px; }
606
+ /* The label and its time on one line, the sentence under them. */
607
+ .k-steprow-label { flex: 1 1 auto; order: 0; }
608
+ .k-steprow-time { order: 1; }
609
+ button.k-steprow-head::after { order: 2; }
610
+ .k-steprow-say { order: 3; flex: 1 1 100%; }
611
+ .k-steprow-say:empty { display: none; }
612
+ .k-steprow.needs > .k-steprow-main > .k-steprow-body { padding: 14px; }
613
+ /* An open body takes the whole width under its head; the rail stops at the head. No z-index: one would trap the body's phone dock under the app's. */
614
+ .k-steprow-body { margin-left: -34px; position: relative; background: var(--so-paper); }
615
+ .k-steprow-body .k-slot-acts > .k-btn, .k-steprow-body .k-slot-acts > form, .k-steprow-body .k-slot-acts > form > .k-btn { flex: 1 1 100%; width: 100%; }
524
616
  .k-doc { padding: 14px 16px 40px; gap: 18px; }
525
617
  .k-dh h1 { font-size: 20px; }
526
618
  .k-backlink { display: none; }
@@ -10,14 +10,14 @@
10
10
  import { type Headline } from "../task-status.js";
11
11
  /** The five status words. */
12
12
  export type StatusKey = "needs" | "running" | "queued" | "finished" | "failed";
13
- /** A chip's tone: "" is neutral; "ask" names a specific ask inside a Needs-you group (ink words, amber dot). */
13
+ /** A chip's tone: "" is neutral; "ask" names a specific ask inside a Needs-you group (ink words, ink dot). */
14
14
  export type ChipTone = "" | "needs" | "ask" | "live" | "done" | "failed";
15
15
  export declare const STATUS: Readonly<Record<StatusKey, {
16
16
  word: string;
17
17
  tone: ChipTone;
18
18
  }>>;
19
19
  /** A task's headline (task-status.ts) as the kit's chip: its own word, in the tone of the status it belongs to. A
20
- * row that waits on a person never shows a failure tone; something that waits on someone else is never amber. */
20
+ * row that waits on a person never shows a failure tone; something that waits on someone else never carries the needs-you mark. */
21
21
  export declare function chipOfHeadline(headline: Headline): {
22
22
  word: string;
23
23
  tone: ChipTone;
@@ -25,7 +25,7 @@ export declare function chipOfHeadline(headline: Headline): {
25
25
  };
26
26
  /** Which of the five a headline is. */
27
27
  export declare function statusOfHeadline(headline: Headline): StatusKey;
28
- /** Resource states (a computer that builds, not work): never amber. */
28
+ /** Resource states (a computer that builds, not work): never the needs-you mark. */
29
29
  export declare const RESOURCE: Readonly<Record<"idle" | "building" | "offline", {
30
30
  word: string;
31
31
  tone: ChipTone;
@@ -77,7 +77,15 @@ export declare function timingBars(rows: readonly TimingRow[], scale?: number):
77
77
  left: number;
78
78
  width: number;
79
79
  } | null>;
80
- /** One step on a progress line: done (a check), now (waits on you: amber), live (running: blue), failed, or to do. */
80
+ /** A step row's marker: done (an ink disc with a check), needs (waits on this person: an ink ring with a dot), live
81
+ * (runs now: blue), wait (current, waiting on nothing this person does), stuck (stopped there: vermilion), skipped (a
82
+ * dashed ring), todo (not reached: a grey ring). */
83
+ export type StepRowMark = "done" | "needs" | "live" | "wait" | "stuck" | "skipped" | "todo";
84
+ /** The words a screen reader hears for a step row's marker. */
85
+ export declare const STEP_ROW_WORDS: Readonly<Record<StepRowMark, string>>;
86
+ /** The id of a step row's body, for its disclosure's aria-controls. */
87
+ export declare const stepRowBodyId: (key: string) => string;
88
+ /** One step on a progress line: done (a check), now (waits on you: ink), live (running: blue), failed, or to do. */
81
89
  export type StepState = "done" | "now" | "live" | "failed" | "todo";
82
90
  export type StepItem = {
83
91
  label: string;
@@ -16,7 +16,7 @@ export const STATUS = {
16
16
  failed: { word: "Failed", tone: "failed" },
17
17
  };
18
18
  /** A task's headline (task-status.ts) as the kit's chip: its own word, in the tone of the status it belongs to. A
19
- * row that waits on a person never shows a failure tone; something that waits on someone else is never amber. */
19
+ * row that waits on a person never shows a failure tone; something that waits on someone else never carries the needs-you mark. */
20
20
  export function chipOfHeadline(headline) {
21
21
  const status = statusOfHeadline(headline);
22
22
  return { word: headline === "Needs you" ? STATUS.needs.word : headline, tone: STATUS[status].tone, status };
@@ -32,7 +32,7 @@ export function statusOfHeadline(headline) {
32
32
  case "neutral": return "queued";
33
33
  }
34
34
  }
35
- /** Resource states (a computer that builds, not work): never amber. */
35
+ /** Resource states (a computer that builds, not work): never the needs-you mark. */
36
36
  export const RESOURCE = {
37
37
  idle: { word: "Idle", tone: "" },
38
38
  building: { word: "Building", tone: "live" },
@@ -100,6 +100,12 @@ export function timingBars(rows, scale) {
100
100
  const sc = scale ?? Math.max(1, ...rows.map(row => row.start + (row.st === "q" ? 0 : row.dur))) * 1.02;
101
101
  return rows.map(row => row.st === "q" ? null : { left: (Math.max(0, row.start) / sc) * 100, width: (Math.max(row.dur, sc / 400) / sc) * 100 });
102
102
  }
103
+ /** The words a screen reader hears for a step row's marker. */
104
+ export const STEP_ROW_WORDS = {
105
+ done: "done", needs: "waiting on you", live: "in progress", wait: "waiting", stuck: "stopped here", skipped: "skipped", todo: "not started",
106
+ };
107
+ /** The id of a step row's body, for its disclosure's aria-controls. */
108
+ export const stepRowBodyId = (key) => `steprow-${key}`;
103
109
  /** The words a screen reader hears for a step's state. */
104
110
  export const STEP_WORDS = { done: "done", now: "waiting on you", live: "in progress", failed: "failed", todo: "not started" };
105
111
  /** How far the line is filled, 0–1: up to the current step (the first that isn't done), or all of it. */
@@ -41,10 +41,10 @@
41
41
  --so-nav-hover: #e4e4e4;
42
42
  /* Nav Ink: idle sidebar rows and their hover fill on the frame. */
43
43
  --so-nav-ink: #525252;
44
- /* Needs You Amber: what waits on this person, and nothing else (the Needs-you chip's dot, a waiting box's outline, the dock's dot). Never a warning, quota, link or decoration. */
45
- --so-needs: var(--so-warning);
46
- /* The wash behind a Needs-you chip. */
47
- --so-needs-soft: var(--so-warning-soft);
44
+ /* Ink accent: the default accent, used for what waits on a person. */
45
+ --so-needs: var(--so-signal);
46
+ /* Ink wash: the fill behind attention badge text and the 3px focus halo around a focused field or composer — one step stronger than the neutral badge fill, so a waiting badge still reads first. */
47
+ --so-needs-soft: var(--so-signal-soft);
48
48
  --so-neutral-ink: #525252;
49
49
  /* Neutral Badge: the badge for anything that is a fact and not a claim on the person, such as queued, running or built today in a project peek, or a count in a fold. */
50
50
  --so-neutral-soft: #f0f0f0;
@@ -156,7 +156,7 @@
156
156
  --so-typography-title-sm-letter-spacing: -0.14px;
157
157
  --so-typography-title-sm-line-height: 1.3;
158
158
  --so-user-bubble: #f2f2f2;
159
- /* Amber: setup trouble and caution. */
159
+ /* The terminal's yellow only. Pages never use it: cautions are neutral ink. */
160
160
  --so-warning: #ab6400;
161
161
  --so-warning-soft: #fff4d5;
162
162
  }
@@ -97,13 +97,13 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
97
97
  export declare function lockHeld(file: string): boolean;
98
98
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
99
99
  export declare function desktopUpdateStatus(stateDir: string): {
100
+ error?: never;
100
101
  active: boolean;
101
102
  phase: string;
102
103
  detail: string;
103
104
  running: boolean;
104
105
  canResume: boolean;
105
106
  canCancel: boolean;
106
- error?: never;
107
107
  automaticRecovery?: never;
108
108
  recoveryAttempts?: never;
109
109
  recoveryRepairs?: never;
@@ -76,20 +76,24 @@ export declare const TODO_GREP_ARGS: readonly string[];
76
76
  export declare function findFirstTasks(repo: string, runner?: Runner): Promise<FirstTaskSuggestion[]>;
77
77
  /** "First result in 7 min": how long after the installation began its first Ready result arrived. */
78
78
  export declare function firstResultWords(since: string, at: string): string | null;
79
- /** The first task's five steps, as a person reads them. */
80
- export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "You approve"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
79
+ /** A task's five steps, as a person reads them: the one label set the task page's rows, chat's first-task journey and
80
+ * the progress line all read. */
81
+ export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "Approval"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
81
82
  export type JourneyKey = (typeof JOURNEY_STEPS)[number][0];
82
83
  /** `stuck`: the step it stopped at (Failed, Stopped, or waiting on something other than the approval). */
83
84
  /** `skipped`: a stage the task went past without it happening (its checks didn't run or couldn't be read). `needsYou`:
84
- * the current or stuck step waits on a person, so it is the one step drawn amber. */
85
+ * the current or stuck step waits on a person, so it is the one step drawn in the needs-you ink. */
86
+ /** `say`: what the step's state means when its label alone doesn't say it ("Checks failed", "Checks not run"); the
87
+ * label is always the step's own name from JOURNEY_STEPS. */
85
88
  export type JourneyStep = {
86
89
  key: JourneyKey;
87
90
  label: string;
88
91
  state: "done" | "current" | "stuck" | "skipped" | "todo";
89
92
  needsYou?: true;
93
+ say?: string;
90
94
  };
91
95
  /**
92
- * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
96
+ * Where the first task stands on Plan → Approval → Build → Checks → Ready, from the one stage every surface reads
93
97
  * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
94
98
  * the build finished with a result.
95
99
  */
@@ -101,8 +105,8 @@ export declare function firstTaskJourney(read: {
101
105
  * nothing says (the step is then shown skipped, never done). The road reads this one answer, never a row's words. */
102
106
  export type RoadChecks = "passed" | "running" | "failed" | "not-run" | "off" | "unreadable" | null;
103
107
  /**
104
- * Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
105
- * from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
108
+ * Any task's road (the step rows on its page): the first task's five steps, told only
109
+ * from recorded facts. Approval is done only when the plan was approved (a rewritten plan waiting on approval
106
110
  * stands there again); a task that never needed one shows it skipped. Checks is done only when the approved check
107
111
  * passed; a running, missing or Off check says so, and with no checks fact it is skipped. A failed check, or one that
108
112
  * can't be read, stops the road at Checks: Ready isn't done (fail closed), even on a task a person marked complete.
package/dist/first-run.js CHANGED
@@ -133,10 +133,11 @@ export function firstResultWords(since, at) {
133
133
  }
134
134
  return `First result in ${Math.round(minutes / (24 * 60))} days`;
135
135
  }
136
- /** The first task's five steps, as a person reads them. */
137
- export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "You approve"], ["build", "Build"], ["checks", "Checks"], ["ready", "Ready"]];
136
+ /** A task's five steps, as a person reads them: the one label set the task page's rows, chat's first-task journey and
137
+ * the progress line all read. */
138
+ export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "Approval"], ["build", "Build"], ["checks", "Checks"], ["ready", "Ready"]];
138
139
  /**
139
- * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
140
+ * Where the first task stands on Plan → Approval → Build → Checks → Ready, from the one stage every surface reads
140
141
  * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
141
142
  * the build finished with a result.
142
143
  */
@@ -155,8 +156,8 @@ export function firstTaskJourney(read, approved, built = false) {
155
156
  state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
156
157
  }
157
158
  /**
158
- * Any task's road (the read-only stepper on its page, after ReUI's Stepper): the first task's five steps, told only
159
- * from recorded facts. You approve is done only when the plan was approved (a rewritten plan waiting on approval
159
+ * Any task's road (the step rows on its page): the first task's five steps, told only
160
+ * from recorded facts. Approval is done only when the plan was approved (a rewritten plan waiting on approval
160
161
  * stands there again); a task that never needed one shows it skipped. Checks is done only when the approved check
161
162
  * passed; a running, missing or Off check says so, and with no checks fact it is skipped. A failed check, or one that
162
163
  * can't be read, stops the road at Checks: Ready isn't done (fail closed), even on a task a person marked complete.
@@ -181,13 +182,13 @@ export function taskRoad(read, approved, built, facts) {
181
182
  return { ...step, state: "done" };
182
183
  case "not-run":
183
184
  waitsAtReady = true;
184
- return { ...step, state: "skipped", label: "Checks not run" };
185
+ return { ...step, state: "skipped", say: "Checks not run" };
185
186
  case "off":
186
187
  waitsAtReady = true;
187
- return { ...step, state: "skipped", label: "Checks at release" };
188
- case "running": return { ...step, state: "current", label: "Checks running" };
189
- case "failed": return { ...step, label: "Checks failed" };
190
- case "unreadable": return { ...step, label: "Checks unreadable" };
188
+ return { ...step, state: "skipped", say: "Checks at release" };
189
+ case "running": return { ...step, state: "current", say: "Checks running" };
190
+ case "failed": return { ...step, say: "Checks failed" };
191
+ case "unreadable": return { ...step, say: "Checks unreadable" };
191
192
  default: return step;
192
193
  }
193
194
  }
@@ -195,16 +196,16 @@ export function taskRoad(read, approved, built, facts) {
195
196
  return step;
196
197
  // At Checks now: only a recorded failure, an unreadable check or a running check changes how it reads.
197
198
  if (step.state === "current")
198
- return facts.checks === "failed" ? { ...step, state: "stuck", label: "Checks failed" }
199
- : facts.checks === "unreadable" ? { ...step, state: "stuck", label: "Checks unreadable" }
200
- : facts.checks === "running" ? { ...step, label: "Checks running" } : step;
199
+ return facts.checks === "failed" ? { ...step, state: "stuck", say: "Checks failed" }
200
+ : facts.checks === "unreadable" ? { ...step, state: "stuck", say: "Checks unreadable" }
201
+ : facts.checks === "running" ? { ...step, say: "Checks running" } : step;
201
202
  switch (facts.checks ?? null) {
202
203
  case "passed": return step;
203
- case "running": return { ...step, state: "current", label: "Checks running" };
204
- case "failed": return { ...step, state: "stuck", label: "Checks failed" };
205
- case "not-run": return { ...step, state: "skipped", label: "Checks not run" };
206
- case "off": return { ...step, state: "skipped", label: "Checks at release" };
207
- case "unreadable": return { ...step, state: "stuck", label: "Checks unreadable", ...(complete ? {} : { needsYou: true }) };
204
+ case "running": return { ...step, state: "current", say: "Checks running" };
205
+ case "failed": return { ...step, state: "stuck", say: "Checks failed" };
206
+ case "not-run": return { ...step, state: "skipped", say: "Checks not run" };
207
+ case "off": return { ...step, state: "skipped", say: "Checks at release" };
208
+ case "unreadable": return { ...step, state: "stuck", say: "Checks unreadable", ...(complete ? {} : { needsYou: true }) };
208
209
  default: return { ...step, state: "skipped" };
209
210
  }
210
211
  });
package/dist/guides.js CHANGED
@@ -335,7 +335,7 @@ ${AUTHORITY_LINE}
335
335
  - The rail is four rows — inbox · board · builds · projects — and a
336
336
  **more** group: portfolio, task list, fleet, system,
337
337
  requirements, people, operating mode, settings. On a phone the same
338
- five sit in the tab bar and more is \`/menu\`. Amber appears only on
338
+ five sit in the tab bar and more is \`/menu\`. The needs-you mark appears only on
339
339
  what needs a person: the inbox count and the one act that resolves a
340
340
  screen (approve, answer, retry).
341
341
 
package/dist/operate.js CHANGED
@@ -10531,6 +10531,8 @@ function describeApproveFailure(reason, id) {
10531
10531
  return "only a person can approve this — approve it on the task page or from your paired chat";
10532
10532
  if (reason === "archived")
10533
10533
  return `${id} is put away — restore it with \`toolroll task restore ${id}\` first`;
10534
+ if (reason === "cancelled")
10535
+ return `${id} was cancelled, so there is nothing to approve`;
10534
10536
  if (reason === "second-approver")
10535
10537
  return `your approval of ${id} is recorded; it is protected work, so a second person needs to approve it (toolroll task approve ${id} as them)`;
10536
10538
  if (reason === "no-approvers") {
@@ -0,0 +1,21 @@
1
+ /**
2
+ * One behaviour the app runs for the page on screen instead of a page script string (docs/plans/task-page-in-app.md).
3
+ * `answer`: a question's inline answer (render-tasks.ts decisionAnswerScript). `region`: a server-drawn region kept
4
+ * current from a build's own fragment (chrome.ts regionScript), `/r/<run>?fragment=check|peek`. `result`: reviewing the
5
+ * result the page shows (result-review.ts RESULT_REVIEW_SCRIPT with render-results.ts reviewEvidenceScript). No code,
6
+ * selector or address travels here: the app runs only the kinds it knows.
7
+ */
8
+ export type PageWork = {
9
+ kind: "answer";
10
+ } | {
11
+ kind: "result";
12
+ } | {
13
+ kind: "region";
14
+ region: "check-progress" | "run-peek";
15
+ run: number;
16
+ };
17
+ /** The work the app and the server agree on. A workspace read says it with `x-toolroll-work`; a reader that does not
18
+ * say exactly this version (an older tab) gets the page as a document with its script string, as before. A hint, never
19
+ * authority. It moves whenever a kind is added: 2 added `result`. */
20
+ export declare const PAGE_WORK_VERSION = 2;
21
+ export declare const PAGE_WORK_HEADER = "x-toolroll-work";
@@ -0,0 +1,5 @@
1
+ /** The work the app and the server agree on. A workspace read says it with `x-toolroll-work`; a reader that does not
2
+ * say exactly this version (an older tab) gets the page as a document with its script string, as before. A hint, never
3
+ * authority. It moves whenever a kind is added: 2 added `result`. */
4
+ export const PAGE_WORK_VERSION = 2;
5
+ export const PAGE_WORK_HEADER = "x-toolroll-work";