rvlib-shipkit 1.3.0 → 1.5.0

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 (203) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/asks/asks-queue.ts +4 -2
  5. package/src/assets/kits/identity-kit.ts +15 -13
  6. package/src/assets/run-assets.ts +12 -2
  7. package/src/check.ts +73 -98
  8. package/src/checks/SK001-package-json-shape.check.ts +26 -4
  9. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  10. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  11. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  12. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  13. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  14. package/src/checks/SK007-github-visibility.check.ts +4 -2
  15. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  16. package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
  17. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  18. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  19. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  20. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  21. package/src/checks/SK014-prose-ste.check.ts +20 -6
  22. package/src/checks/SK015-nested-repo.check.ts +4 -2
  23. package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
  24. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  25. package/src/checks/SK018-test-files-run.check.ts +6 -2
  26. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  27. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  28. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  29. package/src/checks/SK024-identity-colors.check.ts +5 -3
  30. package/src/checks/SK025-release-drift.check.ts +4 -5
  31. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  32. package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
  33. package/src/checks/SK028-readme.check.ts +10 -2
  34. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  35. package/src/cli/cmd_Asks.ts +5 -5
  36. package/src/cli/cmd_Assets.ts +7 -7
  37. package/src/cli/cmd_Bump.ts +12 -11
  38. package/src/cli/cmd_Check.ts +8 -7
  39. package/src/cli/cmd_Checks.ts +7 -6
  40. package/src/cli/cmd_Ci.ts +12 -11
  41. package/src/cli/cmd_Commands.ts +16 -13
  42. package/src/cli/cmd_ConfigKeys.ts +7 -6
  43. package/src/cli/cmd_Contracts.ts +3 -3
  44. package/src/cli/cmd_CrossRepoActivity.ts +10 -6
  45. package/src/cli/cmd_Fleet.ts +9 -7
  46. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  47. package/src/cli/cmd_Home.ts +23 -17
  48. package/src/cli/cmd_Hooks.ts +6 -5
  49. package/src/cli/cmd_Init.ts +5 -4
  50. package/src/cli/cmd_Keys.ts +8 -7
  51. package/src/cli/cmd_Mv.ts +7 -6
  52. package/src/cli/cmd_Needs.ts +4 -3
  53. package/src/cli/cmd_Overview.ts +4 -3
  54. package/src/cli/cmd_Pb.ts +2 -2
  55. package/src/cli/cmd_Planned.ts +6 -5
  56. package/src/cli/cmd_Prose.ts +5 -5
  57. package/src/cli/cmd_Reflect.ts +7 -7
  58. package/src/cli/cmd_Release.ts +6 -5
  59. package/src/cli/cmd_Releases.ts +5 -4
  60. package/src/cli/cmd_Serve.ts +4 -3
  61. package/src/cli/cmd_Smoke.ts +5 -4
  62. package/src/cli/cmd_Start.ts +7 -6
  63. package/src/cli/cmd_Status.ts +4 -3
  64. package/src/cli/cmd_Tui.ts +3 -2
  65. package/src/cli/cmd_When.ts +71 -0
  66. package/src/cli/cmd_Wiki.ts +4 -4
  67. package/src/cli.ts +7 -2
  68. package/src/collections/checks.ts +2 -0
  69. package/src/collections/collection-types.ts +24 -4
  70. package/src/collections/providers.ts +6 -2
  71. package/src/contracts/apply-contracts.ts +9 -8
  72. package/src/contracts/contract-types.ts +8 -2
  73. package/src/contracts/contracts-engine.ts +34 -13
  74. package/src/contracts/manifests/identity.contract.ts +6 -5
  75. package/src/contracts/manifests/social.contract.ts +4 -2
  76. package/src/contracts/manifests/steam.contract.ts +7 -3
  77. package/src/contracts/web-surface.ts +59 -0
  78. package/src/core/budget.ts +19 -9
  79. package/src/core/canonical-scripts.ts +1 -1
  80. package/src/core/check-runner.ts +63 -25
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +31 -7
  83. package/src/core/layout.ts +2 -0
  84. package/src/core/legacy-shapes.ts +39 -10
  85. package/src/core/motherhome.ts +88 -66
  86. package/src/core/one-way-in-policy.ts +2 -2
  87. package/src/core/outcome-words.ts +18 -2
  88. package/src/core/port-check-cli.ts +52 -0
  89. package/src/core/port-check-file.ts +295 -0
  90. package/src/core/port-check-files.ts +31 -0
  91. package/src/core/retention.ts +0 -14
  92. package/src/core/run-check.ts +14 -14
  93. package/src/core/run-ci.ts +11 -7
  94. package/src/core/run-fix.ts +30 -23
  95. package/src/core/run-fixers.ts +42 -5
  96. package/src/core/runs-matrix.ts +179 -0
  97. package/src/core/runs.ts +242 -0
  98. package/src/core/update-policy.ts +20 -0
  99. package/src/core/when-block.ts +29 -30
  100. package/src/core/when.ts +41 -103
  101. package/src/core/wire.ts +34 -9
  102. package/src/fleet/scan-fleet.ts +20 -4
  103. package/src/fleet/update-fleet.ts +6 -6
  104. package/src/home/activity-shape.ts +4 -1
  105. package/src/home/activity.ts +22 -15
  106. package/src/home/gui-app/ShipkitGui.swift +144 -25
  107. package/src/home/gui-app.ts +93 -30
  108. package/src/home/home-launchd.ts +30 -6
  109. package/src/home/home-routes.ts +14 -2
  110. package/src/home/home-start.ts +33 -5
  111. package/src/home/hubs.ts +5 -5
  112. package/src/home/machine-stores.ts +4 -2
  113. package/src/home/open-app.ts +23 -10
  114. package/src/home/pm-config.ts +15 -11
  115. package/src/home/pm-inputs.ts +76 -0
  116. package/src/home/pm.ts +11 -3
  117. package/src/home/process-rows.ts +23 -6
  118. package/src/home/resources.ts +67 -15
  119. package/src/home/run-daemon.ts +6 -1
  120. package/src/home/run-pm.ts +30 -5
  121. package/src/home/serve-web.ts +2 -2
  122. package/src/home/supervise.ts +24 -1
  123. package/src/home/widgets.ts +32 -3
  124. package/src/hooks/commit-message.ts +2 -3
  125. package/src/hooks/run-hooks.ts +60 -6
  126. package/src/init-repo.ts +6 -6
  127. package/src/ledger/gate-checks.ts +115 -61
  128. package/src/ledger/git-key.ts +5 -1
  129. package/src/ledger/ledger.ts +5 -5
  130. package/src/ledger/store.ts +11 -0
  131. package/src/ledger/toolchain-identity.ts +45 -27
  132. package/src/ledger/world-cache.ts +17 -15
  133. package/src/plan/repo-source.ts +18 -39
  134. package/src/release/run-release.ts +5 -0
  135. package/src/repo-config.ts +36 -56
  136. package/src/rules/rule.ts +2 -2
  137. package/src/scaffold/files/agent-coding.md +1 -1
  138. package/src/scaffold/files/agent-index.md +1 -1
  139. package/src/scaffold/files/verify-skill.md +2 -2
  140. package/src/serve/hub-app/activity-st.ts +5 -4
  141. package/src/serve/hub-app/activity-view.tsx +4 -4
  142. package/src/serve/hub-app/admin-view.tsx +13 -11
  143. package/src/serve/hub-app/check-view.tsx +66 -46
  144. package/src/serve/hub-app/check-words.ts +1 -11
  145. package/src/serve/hub-app/checks-svc.ts +16 -36
  146. package/src/serve/hub-app/col-tables-view.tsx +131 -0
  147. package/src/serve/hub-app/col-tables.ts +115 -0
  148. package/src/serve/hub-app/cols-svc.ts +33 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +7 -1
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +15 -3
  160. package/src/serve/hub-app/home-strip.css +69 -19
  161. package/src/serve/hub-app/home-strip.tsx +192 -118
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +21 -5
  164. package/src/serve/hub-app/hub-types.ts +11 -5
  165. package/src/serve/hub-app/hub.css +88 -44
  166. package/src/serve/hub-app/icons.tsx +15 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +11 -8
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +34 -49
  171. package/src/serve/hub-app/palette-st.ts +5 -5
  172. package/src/serve/hub-app/palette-view.tsx +1 -1
  173. package/src/serve/hub-app/plan-view.tsx +51 -0
  174. package/src/serve/hub-app/planned-st.ts +9 -4
  175. package/src/serve/hub-app/processes-view.tsx +38 -0
  176. package/src/serve/hub-app/project-list.tsx +3 -3
  177. package/src/serve/hub-app/projects-svc.ts +15 -10
  178. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  179. package/src/serve/hub-app/reach.ts +36 -0
  180. package/src/serve/hub-app/readme-svc.ts +3 -2
  181. package/src/serve/hub-app/repo-sections.ts +117 -0
  182. package/src/serve/hub-app/repo-st.ts +47 -10
  183. package/src/serve/hub-app/runs-tabs.ts +1 -1
  184. package/src/serve/hub-app/sidebar.tsx +12 -37
  185. package/src/serve/hub-app/timeline-svc.ts +3 -2
  186. package/src/serve/hub-app/timeline-view.tsx +14 -11
  187. package/src/serve/hub-app/tree-build.ts +149 -155
  188. package/src/serve/hub-app/tree-row.tsx +39 -9
  189. package/src/serve/hub-app/tree-svc.ts +70 -49
  190. package/src/serve/hub-app/when-view.tsx +89 -0
  191. package/src/serve/hub-app/when.css +17 -0
  192. package/src/serve/hub-runtime.ts +3 -16
  193. package/src/serve/project-gutter.ts +1 -2
  194. package/src/serve/run-serve.ts +1 -1
  195. package/src/serve/serve-hub.ts +4 -14
  196. package/src/serve/warm-cell.ts +49 -16
  197. package/src/suite/judge-suites.ts +42 -17
  198. package/src/suite/suite-cli.ts +5 -8
  199. package/src/wiki/lint-md.ts +2 -0
  200. package/src/wiki/scan-md.ts +37 -10
  201. package/src/serve/hub-app/processes-svc.ts +0 -63
  202. package/src/serve/hub-app/repo-sections.tsx +0 -61
  203. package/src/serve/repo-processes.ts +0 -89
@@ -38,7 +38,7 @@ a,a:visited{color:var(--accent)}
38
38
  .pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
39
39
  .apptop,.pdock{height:100%;min-height:0}
40
40
  .pjpanel{display:flex}
41
- .appcol{display:flex;flex-direction:column}
41
+ .appcol{position:relative;display:flex;flex-direction:column}
42
42
  /* one region per repo visited, the shown one alone displayed */
43
43
  .repo{flex:1;min-height:0;display:flex;flex-direction:column}
44
44
  .repo[hidden]{display:none}
@@ -49,10 +49,9 @@ a,a:visited{color:var(--accent)}
49
49
  .pmain > main{flex:1}
50
50
 
51
51
  /* ── a repo's bar: on the home the strip (home-strip.css .hstrip), on a hub
52
- of its own the header (.hdr). Its name, what runs now and the two runs,
53
- then its processes, its trouble and its editor icons. One line at any
54
- width: the container queries shrink the labels to their icons, then the
55
- run state to its spinner ── */
52
+ of its own the header (.hdr). Its name, its check… button and a spinner
53
+ while a run goes, then its processes and its editor icons, on one line.
54
+ Its trouble floats under it (home-strip.css .htoasts) ── */
56
55
  /* the repo's processes (serve.processes): a dot, its id, a link once up */
57
56
  .hprocs{display:inline-flex;gap:var(--s1);align-items:center}
58
57
  .hproc{display:inline-flex;align-items:center;gap:var(--s0)}
@@ -63,7 +62,7 @@ a,a:visited{color:var(--accent)}
63
62
  .hdr{display:flex;gap:var(--s2);align-items:center;height:36px;flex:none;padding:0 var(--s3);border-bottom:1px solid var(--line);background:var(--bg-top)}
64
63
  .hdr .sp{flex:1}
65
64
  .hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
66
- .hdr,.hstrip{container-type:inline-size;white-space:nowrap}
65
+ .hdr,.hstrip{white-space:nowrap}
67
66
  .hrun{display:inline-flex;align-items:center;gap:var(--s1);flex:none;margin-left:var(--s2)}
68
67
  .hbtn{display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s2);font-size:11.5px;white-space:nowrap;flex:none}
69
68
  .hbtn .hico{width:13px;height:13px}
@@ -71,40 +70,34 @@ a,a:visited{color:var(--accent)}
71
70
  .hicon{display:inline-flex;align-items:center;justify-content:center;flex:none;width:24px;height:22px;padding:0;border:0;background:none;color:var(--dim)}
72
71
  .hicon:hover{color:var(--accent)}
73
72
  .hicon .hico{width:15px;height:15px}
74
- /* the run state holds its width whatever it says, so a run never moves the buttons beside it */
75
- .hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
76
- .hstatus.red{color:var(--red)}
77
- .hstatus.warn{color:var(--warn)}
78
- /* its words sit against the run buttons, cut with an ellipsis */
79
- .hstate{display:inline-flex;gap:var(--s1);align-items:center;width:200px;min-width:200px;flex:none;justify-content:flex-end;font-size:11.5px;color:var(--dim);overflow:hidden}
80
- .hstate.on{color:var(--ink)}
81
- .hstate .htext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
82
- .hstate .spin.idle{visibility:hidden;animation:none}
83
- /* after .hstate, so a narrow bar wins over its width */
84
- @container (max-width:600px){.hbtn .lbl{display:none}.hbtn{padding:0 var(--s1)}}
85
- @container (max-width:520px){.hstate{width:120px;min-width:120px}}
86
- @container (max-width:420px){.hstate .htext{display:none}.hstate{width:16px;min-width:16px}}
87
73
 
88
74
  /* a row of a (?) legend: one glyph or word, then its meaning */
89
75
  .lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
90
76
 
91
- /* ── the left pane: the sections, the filter, then the tree, the one part
92
- that scrolls ── */
77
+ /* ── the left pane: the filter, then the tree, the one part that
78
+ scrolls ── */
93
79
  aside{border-right:1px solid var(--line);background:var(--panel);min-height:0;display:flex;flex-direction:column;overflow:hidden}
94
80
  aside > *{flex:none}
95
- aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
96
- /* a repo's sections: the project list's lines (project-list.css .pjpl), a
97
- little shorter, the current one lit and marked in the accent */
98
- .rsecs{display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
99
- .rsec{display:flex;align-items:center;gap:var(--s2);width:100%;height:26px;padding:0 var(--s3);background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
100
- .rsec:hover{background:var(--panel2);color:var(--ink);border-color:transparent}
101
- .rsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
102
- .rsec .hico{flex:none;width:16px;height:16px}
103
- .rsecl{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
104
- .rsecn{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg)}
81
+ /* the tree is long, so it keeps a scroll indicator, a thin one with no track:
82
+ WebKit (the macOS app) ignores scrollbar-width and draws a wide dark band */
83
+ aside > .tscroll{flex:1;min-height:0;overflow-y:auto;scrollbar-width:thin;scrollbar-color:var(--line) transparent}
84
+ aside > .tscroll::-webkit-scrollbar{width:6px}
85
+ aside > .tscroll::-webkit-scrollbar-track{background:transparent}
86
+ aside > .tscroll::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
87
+ /* a section, the tree's top row: the project list's lines (project-list.css
88
+ .pjpl), a little shorter, the current one lit and marked in the accent.
89
+ The chevron column is the tree's own, so a section's rows line up under it */
90
+ .tsec{display:grid;grid-template-columns:14px 16px minmax(0,1fr) auto auto;align-items:center;gap:var(--s2);height:26px;padding:0 var(--s1) 0 var(--s2);color:var(--dim);font-size:13px;cursor:pointer;white-space:nowrap}
91
+ .tsec:hover{background:var(--panel2);color:var(--ink)}
92
+ .tsec.on{box-shadow:inset 2px 0 0 var(--accent)}
93
+ .tsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
94
+ .tsec .hico{width:16px;height:16px}
95
+ .tsec:hover .tw{color:var(--ink)}
96
+ .rsecl{min-width:0;overflow:hidden;text-overflow:ellipsis}
97
+ .rsecn{min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg)}
105
98
  .rsecn.nred{background:var(--red)}
106
99
  .rsecn.nwarn{background:var(--warn)}
107
- .rsec:hover .pjkey,.rsec.cur .pjkey{opacity:1}
100
+ .tsec:hover .pjkey,.tsec.cur .pjkey{opacity:1}
108
101
  /* segmented severity filter — all · needs · red. A thin border and an
109
102
  underline on the live one: no fill, no pill */
110
103
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
@@ -114,9 +107,8 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
114
107
  .seg button:hover{color:var(--ink)}
115
108
  .seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
116
109
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
117
- .swhen{padding:0 var(--s3) var(--s2);font-size:11px;color:var(--dim)}
118
110
  /* a narrow pane puts the severity filter under the input, never over it */
119
- .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3)}
111
+ .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line)}
120
112
  .sfield{position:relative;flex:1 1 96px;min-width:0;display:flex}
121
113
  .ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
122
114
  padding:var(--s0) var(--s4) var(--s0) var(--s2);font-size:12px;outline:none}
@@ -132,8 +124,8 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
132
124
  no prose, no dash separators ── */
133
125
  .tree{padding:var(--s1) 0 var(--s4);outline:none}
134
126
  .tree:focus-visible{box-shadow:inset 2px 0 0 var(--accent)}
135
- .trow{display:grid;grid-template-columns:calc(var(--d,0)*12px + 18px) 14px minmax(0,1fr) 58px 46px 14px;
136
- align-items:center;gap:var(--s1);padding:0 var(--s2);height:20px;line-height:20px;cursor:pointer;white-space:nowrap}
127
+ .trow{display:grid;grid-template-columns:calc(var(--d,0)*10px + 14px) 14px minmax(0,1fr) 12px auto 14px;
128
+ align-items:center;gap:var(--s1);padding:0 var(--s1) 0 var(--s2);height:20px;line-height:20px;cursor:pointer;white-space:nowrap}
137
129
  .trow:hover{background:var(--panel2)}
138
130
  .trow.on{background:var(--sel);box-shadow:inset 2px 0 0 var(--accent)}
139
131
  /* the fold marker: an 11px chevron with the whole first column as its hit
@@ -167,21 +159,20 @@ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
167
159
  .tg.ok{color:var(--ok)}
168
160
  .tg.adv{color:var(--warn);opacity:.75}
169
161
  .tg.run{color:var(--accent);animation:pulse 1.1s ease-in-out infinite}
170
- /* right aligned, tabular figures, ONE unit (ms) and one tone table, so a
171
- column of durations compares by eye (hub-app/dur.ts owns both) */
172
- .tdur{font:10.5px ui-monospace,Menlo,monospace;font-variant-numeric:tabular-nums;text-align:right;color:var(--dim);overflow:hidden}
162
+ /* the duration: a clock tinted by the tone table (hub-app/dur.ts), the
163
+ figure in its tooltip, so the label keeps the width */
164
+ .tdur{display:flex;align-items:center;justify-content:center;color:var(--dim)}
165
+ .tdur .hico{width:11px;height:11px;color:inherit}
173
166
  .tdur.fast,.dnum.fast{color:var(--ok)}
174
167
  .tdur.plain,.dnum.plain{color:var(--ink)}
175
168
  .tdur.slow,.dnum.slow{color:var(--warn)}
176
169
  .tdur.bad,.dnum.bad{color:var(--red)}
177
170
  .tnote{font-size:10px;text-align:right;color:var(--dim);overflow:hidden;text-overflow:ellipsis}
178
171
  .tnote b{font-weight:600;color:var(--red)}
179
- .stabs{display:flex;gap:var(--s2);align-items:center;padding:var(--s1) var(--s3);border-bottom:1px solid var(--line)}
180
- .stabs .sp{flex:1}
181
- .stabs .cnt{min-width:4ch;text-align:right;font-size:11px;color:var(--dim);font-variant-numeric:tabular-nums}
182
172
  .catgen{display:inline-flex;gap:var(--s1)}
183
173
  .catgen button{padding:var(--s0) var(--s2);font-size:11px;font-weight:700}
184
174
  .tempty{padding:var(--s2) var(--s3);font-size:12px;color:var(--dim);white-space:pre-wrap}
175
+ .thidden{cursor:pointer}.thidden:hover{color:var(--ink)}
185
176
  .twait{display:flex;align-items:center;gap:var(--s2)}
186
177
  .warnline{color:var(--red)}
187
178
  .fold{cursor:pointer;color:var(--dim);font-size:15px;display:inline-block;width:22px;text-align:center;
@@ -368,7 +359,15 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
368
359
  .assetrem .rem{color:var(--ok);border-left:2px solid var(--ok);padding-left:var(--s2);font-size:12.5px;margin:0 0 var(--s2);white-space:pre-wrap}
369
360
 
370
361
  /* ── collection item card + status card ── */
371
- .colcard{max-width:760px;width:100%;background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s4);text-align:left}
362
+ /* a repo page sits on the page background, never in a box of its own: its
363
+ header and its sentence are unboxed, and each group it holds (the facts,
364
+ a titled section, the command, the admin rows, the runs list) is one
365
+ lighter box, so a box always means "these belong together" */
366
+ .colcard{max-width:760px;width:100%;padding:var(--s2) 0;text-align:left}
367
+ .colcard>.cfacts,.colcard>.csec,.colcard>.ccmd,.colcard>.md:not(.usays),.colcard>.adbox,.tlcard>.rlist{background:var(--panel);border:1px solid var(--line);border-radius:4px;padding:var(--s2) var(--s3)}
368
+ .colcard>.cfacts,.colcard>.ccmd,.colcard>.md:not(.usays),.colcard>.adbox{margin-top:var(--s3)}
369
+ .ccmd>.cf:first-child,.cfacts>.cf:nth-child(-n+2){border-top:0}
370
+ @media (max-width:900px){.cfacts>.cf:nth-child(2){border-top:1px solid var(--line)}}
372
371
  .colcard h3{margin:0 0 var(--s2);font-size:15px;display:flex;gap:var(--s2);align-items:baseline}
373
372
  .colcard h3 .sp{flex:1}
374
373
  .colcard .truth{margin-left:auto;font:11px ui-monospace,Menlo,monospace;color:var(--dim)}
@@ -559,6 +558,15 @@ a.tok{color:var(--accent)}
559
558
  .checkcard .dim{color:var(--dim)}
560
559
  /* what is not a fact: the output and the slow suites, titled, unboxed */
561
560
  .csec{margin-top:var(--s4)}
561
+ /* where it runs: one line per context, the value then its meaning. A context
562
+ that never runs the check stays dim, so the lit lines read down the list */
563
+ .cruns{display:grid;grid-template-columns:auto auto minmax(0,1fr);column-gap:var(--s3);row-gap:var(--s0);align-items:baseline}
564
+ .crun{display:contents;color:var(--dim)}
565
+ .crv{font:11px ui-monospace,Menlo,monospace;white-space:nowrap}
566
+ .crun.on .crv{color:var(--ok)}
567
+ .crun.cache .crv{color:var(--ink)}
568
+ .crm{font-size:11.5px;color:var(--dim);opacity:.8}
569
+ .crun:not(.on) .crm{opacity:.5}
562
570
  .cstitle{display:flex;gap:var(--s1);align-items:center;font-size:11px;text-transform:uppercase;letter-spacing:.08em;
563
571
  color:var(--dim);margin-bottom:var(--s2)}
564
572
  .wline{margin-bottom:var(--s1);word-break:break-word}
@@ -627,7 +635,29 @@ a.tok{color:var(--accent)}
627
635
  .colgrid{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4)}
628
636
  .cghead{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
629
637
  display:flex;gap:var(--s2);align-items:baseline;margin:0 0 var(--s3)}
638
+ .ctlead{font-size:11px;color:var(--dim);margin:calc(-1 * var(--s1)) 0 var(--s2)}
630
639
  .cghead .cnt{font-size:11px;color:var(--dim);letter-spacing:0;text-transform:none}
640
+ /* the checks and asks pages: one dense table each (col-tables-view.tsx), on
641
+ the activity table (.actable), a group header per state that folds */
642
+ .ctpage{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4);font-size:12.5px;container-type:inline-size}
643
+ .cttable{font-variant-numeric:tabular-nums}
644
+ .cttable td{padding:var(--s0) var(--s2);vertical-align:baseline}
645
+ .ctgrp td{padding:var(--s2) var(--s2) var(--s1);color:var(--dim);font-size:11.5px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none}
646
+ .ctgrp:hover td{color:var(--ink)}
647
+ .ctgrp.g-fail td{color:var(--red)}
648
+ .ctgrp.g-warn td{color:var(--warn)}
649
+ .ctcaret{display:inline-block;width:14px}
650
+ .ctrow{cursor:pointer}
651
+ .ctrow:hover{background:var(--panel2)}
652
+ .ctdot{font-size:10px}
653
+ .ctid,.ctkind{font:11.5px ui-monospace,Menlo,monospace}
654
+ .ctfind{font:11.5px ui-monospace,Menlo,monospace;color:var(--dim)}
655
+ .ctdur{text-align:right;font:11.5px ui-monospace,Menlo,monospace}
656
+ .ctbudget,.ctplan,.ctblocks{color:var(--dim)}
657
+ .ctprobe{font-size:11.5px}
658
+ .cttable .c-dot{width:22px}.cttable .c-id{width:140px}.cttable .c-title{width:16%}.cttable .c-age{width:72px}.cttable .c-dur{width:152px}.cttable .c-plan{width:88px}
659
+ .cttable .c-kind{width:92px}.cttable .c-blocks{width:32%}.cttable .c-probe{width:84px}
660
+ @container (max-width:760px){.cttable .c-title{width:0}.cttable .cttitle{display:none}.cttable .c-plan{width:0}.cttable .ctplan{display:none}}
631
661
  .cgcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--s3)}
632
662
  .cgcard{background:var(--panel);border:1px solid var(--line);border-radius:5px;padding:var(--s2) var(--s3);cursor:pointer;min-width:0}
633
663
  .cgcard:hover{border-color:var(--accent)}
@@ -694,7 +724,7 @@ a.mint{color:var(--accent);font-size:12px;text-decoration:none}
694
724
  a.mint:hover{text-decoration:underline}
695
725
  .colcard h3 .stchip,.colcard h3 a.mint{margin-left:var(--s2)}
696
726
 
697
- /* ── the cmd+k picker ── */
727
+ /* ── the cmd+j picker ── */
698
728
  .palov{position:fixed;inset:0;background:var(--scrim);z-index:100;display:flex;justify-content:center;align-items:flex-start;
699
729
  padding-top:14vh}
700
730
  .pal{width:min(560px,90vw);background:var(--panel);border:1px solid var(--line);border-radius:3px;
@@ -723,9 +753,23 @@ pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;paddi
723
753
 
724
754
  /* ── the admin page: one row per control, its sentence under its name ── */
725
755
  .adrow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0;border-top:1px solid var(--line)}
756
+ .adrow:first-child{border-top:0}
726
757
  .adtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--s0);font-size:13px}
727
758
  .adtext .dim{font-size:12px}
728
759
  .adctl{flex:none;display:flex;align-items:center}
729
760
  .adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
730
761
  .adflag input{width:auto;margin:0}
731
762
  .adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}
763
+ /* a repo's plan page: one line per planned check */
764
+ .plntable{width:100%;border-collapse:collapse;font-size:12.5px}
765
+ .plntable td{padding:var(--s1) var(--s2);border-top:1px solid var(--line);vertical-align:baseline}
766
+ .plntable tr:first-child td{border-top:0}
767
+ .plnwhen{white-space:nowrap;font-variant-numeric:tabular-nums}
768
+ .plnrun{text-align:right;white-space:nowrap}
769
+ /* a repo's git page */
770
+ .gitfiles,.gitcommits{width:100%;border-collapse:collapse;font-size:12.5px}
771
+ .gitfiles td,.gitcommits td{padding:2px var(--s2);vertical-align:baseline}
772
+ .gitkind{width:1%;white-space:nowrap;color:var(--dim)}
773
+ .gitkind.staged{color:var(--ok)}
774
+ .gitkind.untracked{color:var(--warn)}
775
+ .gitpath{font:12px ui-monospace,Menlo,monospace;overflow-wrap:anywhere}
@@ -5,6 +5,11 @@
5
5
  export const ICON = {
6
6
  shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
7
7
  process: 'M2 10h3.5l2-5 5 10 2-5H18',
8
+ processes: 'M2 10h3.5l2-5 5 10 2-5H18',
9
+ when: 'M10 3a7 7 0 1 0 .01 0M10 6v4l2.5 2',
10
+ plan: 'M5.5 3h9M5.5 17h9M6.5 3c0 4 7 4 7 7s-7 3-7 7M13.5 3c0 4-7 4-7 7s7 3 7 7',
11
+ git: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
12
+ reflections: 'M4 3.5h12v9H9l-4 3.5v-3.5H4zM7.5 7h5M7.5 9.5h3',
8
13
  monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
9
14
  archived: 'M2.5 4h15v3.5h-15zM4 7.5V16h12V7.5M8 10.5h4',
10
15
  home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
@@ -15,7 +20,11 @@ export const ICON = {
15
20
  fork: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
16
21
  search: 'M8.5 3a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0-11M12.5 12.5 17 17',
17
22
  // a repo page's sections, then its two runs and its editor
23
+ readme: 'M5 2.5h7l3 3v12H5zM12 2.5v3h3M7.5 9h5M7.5 12h5M7.5 15h3',
18
24
  checks: 'M4 10.5 7.5 14 16 5.5',
25
+ contracts: 'M5 2.5h10v15H5zM7.5 6h5M7.5 9h5M7.5 12h2.5',
26
+ repo: 'M2.5 5h5l1.5 1.5h8.5v9.5h-15z',
27
+ secrets: 'M7 7a3 3 0 1 0 0 6a3 3 0 1 0 0-6M10 10h7.5M15 10v2.5M17.5 10v2',
19
28
  runs: 'M3 5h8M3 10h12M3 15h6',
20
29
  asks: 'M3 4h14v9H8l-4 3.5V13H3z',
21
30
  tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
@@ -27,6 +36,12 @@ export const ICON = {
27
36
  // the VS Code mark, its ribbon drawn as one outline
28
37
  vscode: 'M14 2.5l3.5 1.7v11.6L14 17.5zM14 2.5 4.5 12.3 2.5 11V9l2-1.3L14 17.5M14 6.8 10.2 10l3.8 3.2',
29
38
  sort: 'M6 3.5v13M3 13.5l3 3 3-3M14 16.5v-13M11 6.5l3-3 3 3',
39
+ // a process's own switches on the overview
40
+ play: 'M6.5 4.5v11l9-5.5z',
41
+ stop: 'M5.5 5.5h9v9h-9z',
42
+ link: 'M8.5 11.5a3.5 3.5 0 0 0 5 0l2.5-2.5a3.5 3.5 0 0 0-5-5l-1 1M11.5 8.5a3.5 3.5 0 0 0-5 0L4 11a3.5 3.5 0 0 0 5 5l1-1',
43
+ clock: 'M10 2.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 1 0 0-15M10 6v4l2.5 2',
44
+ restart: 'M15.5 10a5.5 5.5 0 1 1-1.6-3.9M14.2 3v3.3h-3.3',
30
45
  } as const
31
46
 
32
47
  export const Icon = ({ d }: { d: string }) => (
@@ -9,6 +9,7 @@
9
9
  <link rel="stylesheet" href="./prose-hits.css" />
10
10
  <link rel="stylesheet" href="./home-strip.css" />
11
11
  <link rel="stylesheet" href="./planned.css" />
12
+ <link rel="stylesheet" href="./when.css" />
12
13
  <link rel="stylesheet" href="./activity.css" />
13
14
  <link rel="stylesheet" href="./readme.css" />
14
15
  <link rel="stylesheet" href="./dock.css" />
@@ -1,6 +1,6 @@
1
1
  // the page's command keys, read the same on every layout: ⌘1 to ⌘9 select the
2
2
  // nth repo of the project list, then the home row under the right hand: ⌘J
3
- // the overview, ⌘K the picker, ⌘L the activity, ⌘' the plan. ⌘B cycles the
3
+ // the picker, ⌘K the overview, ⌘L the activity, ⌘' the plan. ⌘B cycles the
4
4
  // dock at the bottom of the page (dock-st.ts): the line, slim, open.
5
5
  // A digit and the quote are read from the physical key (`code`), so an
6
6
  // AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
@@ -17,17 +17,17 @@ export function pageKey(e: KeyLike): PageKey | null {
17
17
  if (e.shiftKey) return null
18
18
  if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
19
19
  const k = e.key.toLowerCase()
20
- if (k === 'j') return { t: 'home' }
21
- if (k === 'k') return { t: 'picker' }
20
+ if (k === 'j') return { t: 'picker' }
21
+ if (k === 'k') return { t: 'home' }
22
22
  if (k === 'l') return { t: 'activity' }
23
23
  if (k === 'b') return { t: 'dock' }
24
24
  return null
25
25
  }
26
26
 
27
27
  /** the shortcut each place of the home carries on its button */
28
- export const PLACE_KEY = { overview: '⌘J', planned: "⌘'", activity: '⌘L' } as const
29
- /** the picker's shortcut, on the second button, after home */
30
- export const PICKER_KEY = '⌘K'
28
+ export const PLACE_KEY = { overview: '⌘K', planned: "⌘'", activity: '⌘L' } as const
29
+ /** the picker's shortcut, on the first button, before home */
30
+ export const PICKER_KEY = '⌘J'
31
31
  /** the dock's shortcut, on the dock's own header */
32
32
  export const DOCK_KEY = '⌘B'
33
33
 
@@ -46,8 +46,11 @@ export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'al
46
46
  * plus a letter only types a character inside a text field, where the page
47
47
  * leaves the key alone. Read from the physical key (`code`): on a Mac ⌥C
48
48
  * types ç, and AZERTY moves the letters */
49
- export type SectionId = 'checks' | 'runs' | 'asks' | 'tests' | 'media' | 'wikis' | 'admin'
50
- export const SECTION_LETTER: Record<SectionId, string> = { checks: 'C', runs: 'R', asks: 'A', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
49
+ export type SectionId = 'readme' | 'contracts' | 'shipkit' | 'repo' | 'secrets' | 'when' | 'runs' | 'plan' | 'processes' | 'git' | 'asks' | 'reflections' | 'tests' | 'media' | 'wikis' | 'admin'
50
+ export const SECTION_LETTER: Record<SectionId, string> = { readme: 'E', contracts: 'K', shipkit: 'S', repo: 'C', secrets: 'X', when: 'O', runs: 'R', plan: 'N', processes: 'P', git: 'G', asks: 'A', reflections: 'F', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
51
+ /** a section's words on its row, where the id alone would not say it */
52
+ export const SECTION_LABEL: Partial<Record<SectionId, string>> = { shipkit: 'shipkit checks', repo: 'repo checks' }
53
+ export const sectionLabel = (id: SectionId): string => SECTION_LABEL[id] ?? id
51
54
  /** the badge at the end of a section's line */
52
55
  export const sectionKeyLabel = (id: SectionId): string => `⌥${SECTION_LETTER[id]}`
53
56
 
@@ -11,6 +11,7 @@ import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
11
11
  import { ChartView, type ChartSource } from './chart-view.ts'
12
12
  import type { RepoSt } from './repo-st.ts'
13
13
  import { chartHeight } from './timeline-geometry.ts'
14
+ import { ownTrouble, reach } from './reach.ts'
14
15
 
15
16
  /** how often the page asks. A strip a second behind the runner is still live */
16
17
  export const LIVE_POLL_MS = 1_000
@@ -107,7 +108,7 @@ export class LiveSvc {
107
108
  if (this.inFlight) return
108
109
  this.inFlight = true
109
110
  try {
110
- const r = await fetch(this.st.url('/api/live.json'))
111
+ const r = await reach(this.st.url('/api/live.json'))
111
112
  if (!r.ok) throw new Error(await r.text())
112
113
  const v = (await r.json()) as LiveView
113
114
  const ended = this.running && v.runs.length === 0
@@ -125,7 +126,7 @@ export class LiveSvc {
125
126
  if (this.st.timeline.data != null) void this.st.timeline.load(true)
126
127
  }
127
128
  } catch (e) {
128
- runInAction(() => { this.error = `the live runs could not be read: ${(e as Error).message}` })
129
+ runInAction(() => { this.error = ownTrouble(e, this.st.home.machine, (m) => `the live runs could not be read: ${m}`) })
129
130
  } finally {
130
131
  this.inFlight = false
131
132
  }
@@ -8,12 +8,13 @@
8
8
  import { makeAutoObservable, observableRef, runInAction } from 'mobx'
9
9
  import type { HomeAnswer, WidgetsAnswer } from '../../home/home-routes.ts'
10
10
  import type { WorkerView } from '../../home/hubs.ts'
11
- import type { ResourceView } from '../../home/resources.ts'
11
+ import type { OpenHow, ResourceView } from '../../home/resources.ts'
12
12
  import type { PmAnswer, Role } from '../../home/pm.ts'
13
13
  import { processGroups, type ProcessGroup } from '../../home/process-rows.ts'
14
14
  import { ClockSvc } from './clock-svc.ts'
15
15
  import type { HomeSt } from './home-st.ts'
16
16
  import type { HubDot } from './projects-svc.ts'
17
+ import { ownTrouble, reach, Unreachable } from './reach.ts'
17
18
 
18
19
  export const MACHINE_POLL_MS = 1_500
19
20
  /** past the web's own wait on the daemon (serve-web.ts MACHINE_ANSWER_MS), so
@@ -27,18 +28,6 @@ export const MACHINE_KEY = 'hub:machine'
27
28
  /** how long a copied pid says so */
28
29
  export const COPIED_MS = 1_200
29
30
 
30
- /** how open opens a global process's url, kept per browser */
31
- export const OPEN_HOW_KEY = 'hub:open-how'
32
- export type OpenHow = 'browser' | 'app'
33
-
34
- function loadOpenHow(): OpenHow {
35
- try {
36
- return globalThis.localStorage?.getItem(OPEN_HOW_KEY) === 'app' ? 'app' : 'browser'
37
- } catch {
38
- return 'browser'
39
- }
40
- }
41
-
42
31
  type Kept = { data: HomeAnswer | null; pm: PmAnswer | null; widgets: WidgetsAnswer | null }
43
32
 
44
33
  function loadKept(): Kept | null {
@@ -50,8 +39,9 @@ function loadKept(): Kept | null {
50
39
  }
51
40
  }
52
41
 
53
- /** the poll failed because nothing behind the page answers, not on an error of an answer */
54
- class DownError extends Error {}
42
+ /** the web not answering, in the words of the daemon's (serve-web.ts daemonDownText) */
43
+ export const webDownText = (why: string): string =>
44
+ `the shipkit web does not answer on ${globalThis.location?.origin ?? 'this page\'s origin'} (${why}). \`shipkit pm\` runs it; this page comes back by itself once it answers`
55
45
 
56
46
  export type MachineOpts = { reload?: () => void; timeoutMs?: number }
57
47
 
@@ -67,9 +57,10 @@ export class MachineSt {
67
57
  error = ''
68
58
  /** where open sends a global process's url: the default browser, or a
69
59
  * shipkit app window */
70
- openHow: OpenHow = loadOpenHow()
71
60
  /** why the daemon did not answer the last poll, or '' while it does */
72
61
  down = ''
62
+ /** which one did not answer: the web itself, or the daemon behind it */
63
+ downWho: 'web' | 'daemon' = 'daemon'
73
64
  /** the daemon answered once since the load: until then data, pm and
74
65
  * widgets are what this browser kept, shown but never acted on */
75
66
  fresh = false
@@ -115,7 +106,8 @@ export class MachineSt {
115
106
  * the project list's order, one row per process (process-rows.ts, what
116
107
  * `shipkit global-processes` prints) */
117
108
  get processGroups(): ProcessGroup[] {
118
- return processGroups(this.pm, this.st.projects.list.map((p) => p.name), { daemonVersion: this.data?.version ?? null })
109
+ const silent = { ...(this.down === '' ? {} : { [this.downWho]: this.down }), ...(this.pmDown === '' ? {} : { pm: this.pmDown }) }
110
+ return processGroups(this.pm, this.st.projects.list.map((p) => p.name), { daemonVersion: this.data?.version ?? null, silent })
119
111
  }
120
112
 
121
113
  resource(id: string): ResourceView | null {
@@ -154,7 +146,7 @@ export class MachineSt {
154
146
  /** pm is polled beside the daemon: either one down leaves the other shown */
155
147
  private async loadPm(): Promise<void> {
156
148
  try {
157
- const r = await fetch('/api/pm/status.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
149
+ const r = await reach('/api/pm/status.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
158
150
  if (!r.ok) throw new Error(await r.text())
159
151
  const j = (await r.json()) as PmAnswer
160
152
  runInAction(() => {
@@ -162,10 +154,8 @@ export class MachineSt {
162
154
  this.pmDown = ''
163
155
  })
164
156
  } catch (e) {
165
- runInAction(() => {
166
- this.pm = null
167
- this.pmDown = (e as Error).message
168
- })
157
+ // the last answer stays, its rows unknown (processGroups): the table keeps its lines
158
+ runInAction(() => { this.pmDown = (e as Error).message })
169
159
  }
170
160
  }
171
161
 
@@ -187,19 +177,15 @@ export class MachineSt {
187
177
  if (this.polling) return
188
178
  this.polling = true
189
179
  const pm = this.loadPm()
190
- // never held on: a widget that answers late only shows late
191
- if (!this.widgetsPolling) {
180
+ // never held on: a widget that answers late only shows late. Asked only
181
+ // while the overview shows: the daemon renders a widget for a reader,
182
+ // and a render can cost another machine an ssh session
183
+ if (!this.widgetsPolling && this.st.page === 'overview' && globalThis.document?.visibilityState !== 'hidden') {
192
184
  this.widgetsPolling = true
193
185
  void this.loadWidgets().finally(() => { this.widgetsPolling = false })
194
186
  }
195
187
  try {
196
- let r: Response
197
- try {
198
- r = await fetch('/api/home.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
199
- } catch (e) {
200
- throw new DownError(`the home does not answer: ${(e as Error).message}`)
201
- }
202
- if (r.headers.get('x-shipkit-daemon') === 'down') throw new DownError(await r.text())
188
+ const r = await reach('/api/home.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
203
189
  if (!r.ok) throw new Error(`/api/home.json answered ${r.status}: ${await r.text()}`)
204
190
  const j = (await r.json()) as HomeAnswer
205
191
  const back = this.wasDown
@@ -220,9 +206,13 @@ export class MachineSt {
220
206
  if (back) this.reload()
221
207
  } catch (e) {
222
208
  const msg = (e as Error).message
223
- if (e instanceof DownError) {
209
+ if (e instanceof Unreachable) {
224
210
  this.wasDown = true
225
- runInAction(() => { this.down = msg })
211
+ const who = e.who === 'web' ? 'web' : 'daemon'
212
+ runInAction(() => {
213
+ this.down = who === 'web' ? webDownText(msg) : msg
214
+ this.downWho = who
215
+ })
226
216
  } else {
227
217
  this.pollError = true
228
218
  runInAction(() => {
@@ -257,14 +247,16 @@ export class MachineSt {
257
247
 
258
248
  private async post(path: string, body: unknown): Promise<boolean> {
259
249
  try {
260
- const r = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
250
+ const r = await reach(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body) })
261
251
  if (!r.ok) throw new Error(await r.text())
262
252
  this.pollError = false
263
253
  runInAction(() => { this.error = '' })
264
254
  void this.load()
265
255
  return true
266
256
  } catch (e) {
267
- runInAction(() => { this.error = (e as Error).message })
257
+ // the machine not answering is the down card's, which the next poll raises
258
+ runInAction(() => { this.error = ownTrouble(e, this) })
259
+ void this.load()
268
260
  return false
269
261
  }
270
262
  }
@@ -278,16 +270,16 @@ export class MachineSt {
278
270
  }
279
271
 
280
272
  /** a global process: start, stop, restart, take over the copy started
281
- * elsewhere, open its url in the default browser or as an app window
282
- * (started first when it is down), or run one of its actions */
283
- processAct(id: string, act: 'start' | 'stop' | 'restart' | 'takeover' | 'open' | 'app' | 'action', action?: string): Promise<boolean> {
273
+ * elsewhere, open its url (started first when it is down), or run one
274
+ * of its actions */
275
+ processAct(id: string, act: 'start' | 'stop' | 'restart' | 'takeover' | 'open' | 'chrome' | 'app' | 'action', action?: string): Promise<boolean> {
284
276
  return this.post('/api/pm/resource', { id, act, action })
285
277
  }
286
278
 
287
- /** the url of a global process, the way this browser picked (started
288
- * first when it is down) */
289
- openProcess(id: string): Promise<boolean> {
290
- return this.processAct(id, this.openHow === 'app' ? 'app' : 'open')
279
+ /** the url of a global process in the default browser, in Chrome, or as
280
+ * its own app (started first when it is down) */
281
+ openProcess(id: string, how: OpenHow): Promise<boolean> {
282
+ return this.processAct(id, how === 'browser' ? 'open' : how)
291
283
  }
292
284
 
293
285
  /** a process's pid to the clipboard, and the "copied" flash on it; a
@@ -305,13 +297,6 @@ export class MachineSt {
305
297
  this.copiedTimer = setTimeout(() => runInAction(() => { this.copiedPid = null }), COPIED_MS)
306
298
  }
307
299
 
308
- setOpenHow(how: OpenHow): void {
309
- this.openHow = how
310
- try {
311
- globalThis.localStorage?.setItem(OPEN_HOW_KEY, how)
312
- } catch { /* private window or blocked site data: the pick lasts the tab */ }
313
- }
314
-
315
300
  /** restart the daemon or the web through pm */
316
301
  restartChild(id: Role): Promise<boolean> {
317
302
  return this.post('/api/pm/child', { id, act: 'restart' })
@@ -1,8 +1,8 @@
1
- // the one picker of the page (cmd+k, keys.ts), a node of the root: the
2
- // home's places (home, planned, activity) first, then the shown repo's own jump
1
+ // the one picker of the page (cmd+j, keys.ts), a node of the root: the
2
+ // home's places (home, activity, planned, in the list's order) first, then the shown repo's own jump
3
3
  // targets, then every other house repo as a place to go. Another repo's
4
4
  // resources are never listed: a jump to a repo selects it in place, and its
5
- // own targets are one more cmd+k away.
5
+ // own targets are one more cmd+j away.
6
6
  import { makeAutoObservable } from 'mobx'
7
7
  import type { HomeSt } from './home-st.ts'
8
8
  import type { Jump } from './repo-st.ts'
@@ -25,8 +25,8 @@ export class PaletteSt {
25
25
  if (st.machine == null) return []
26
26
  return [
27
27
  { id: 'place:home', label: 'home', section: 'place', face: { place: 'home' }, run: () => st.navigate('/') },
28
- { id: 'place:planned', label: 'planned', section: 'place', face: { place: 'planned' }, run: () => st.navigate('/planned') },
29
28
  { id: 'place:activity', label: 'activity', section: 'place', face: { place: 'activity' }, run: () => st.navigate('/activity') },
29
+ { id: 'place:planned', label: 'planned', section: 'place', face: { place: 'planned' }, run: () => st.navigate('/planned') },
30
30
  { id: 'place:archived', label: 'archived', section: 'place', face: { place: 'archived' }, run: () => st.navigate('/archived') },
31
31
  ]
32
32
  }
@@ -45,7 +45,7 @@ export class PaletteSt {
45
45
  }
46
46
 
47
47
  /** ranked: prefix matches first, then substring. With no query, home,
48
- * planned and activity, then the shown repo's first targets, then every other repo,
48
+ * activity and planned, then the shown repo's first targets, then every other repo,
49
49
  * so a jump elsewhere is always in reach */
50
50
  get results(): Jump[] {
51
51
  const q = this.query.trim().toLowerCase()
@@ -1,4 +1,4 @@
1
- // the one picker, cmd+k (palette-st.ts): home, planned, activity and
1
+ // the one picker, cmd+j (palette-st.ts): home, activity, planned and
2
2
  // archived, the shown repo's targets, then every other repo. A place wears
3
3
  // its button's icon, a repo its own icon, as the project list shows them.
4
4
  import { observer } from 'mobx-react-lite'
@@ -0,0 +1,51 @@
1
+ // A repo's plan page: what the machine runs here on its own time, soonest
2
+ // first, then the checks only a save runs. The rows are this repo's slice of
3
+ // the machine's plan, already carried by /api/resources.json, so the page
4
+ // reads nothing of its own. The home's planned page is the same plan for
5
+ // every repo at once.
6
+ import { observer } from 'mobx-react-lite'
7
+ import type { PlanRow } from '../../plan/planner.ts'
8
+ import { LAST_TONE, markOf, reasonWords, whenWords } from './planned-st.ts'
9
+ import type { RepoSt } from './repo-st.ts'
10
+ import { ageWords } from './time-ago.tsx'
11
+
12
+ const soonest = (a: PlanRow, b: PlanRow): number => (a.dueAt ?? Infinity) - (b.dueAt ?? Infinity) || a.check.localeCompare(b.check)
13
+
14
+ const PlanLine = observer(({ app, r }: { app: RepoSt; r: PlanRow }) => {
15
+ const now = app.clock.now
16
+ const m = markOf(r)
17
+ const running = r.state === 'running'
18
+ return (
19
+ <tr className={`pln m-${m.mark}`}>
20
+ <td className="plnwhen">{whenWords(r, now)}</td>
21
+ <td><a href="#" title={r.title ?? r.check} onClick={(e) => { e.preventDefault(); app.timeline.openCheck(r.check) }}>{r.check}</a>{r.title != null ? <span className="dim"> {r.title}</span> : null}</td>
22
+ <td className="dim" title={`rule: ${r.rule}\ninactive: ${r.inactive}`}>{r.error != null ? <span className="tl red">{r.error}</span> : reasonWords(r)}</td>
23
+ <td>{r.last == null ? <span className="dim">never ran</span> : <span className={`tl ${LAST_TONE[r.last.status]}`}>{r.last.status}, {ageWords(r.last.at, now)}</span>}</td>
24
+ <td className="plnrun"><button className="ghost" disabled={running} onClick={() => void app.home.planned.run(r)}>{running ? <><span className="spin" />running</> : 'run now'}</button></td>
25
+ </tr>
26
+ )
27
+ })
28
+
29
+ const PlanTable = observer(({ app, rows }: { app: RepoSt; rows: PlanRow[] }) => (
30
+ <table className="plntable">
31
+ <tbody>{rows.map((r) => <PlanLine key={r.check} app={app} r={r} />)}</tbody>
32
+ </table>
33
+ ))
34
+
35
+ export const PlanPage = observer(({ app }: { app: RepoSt }) => {
36
+ const all = [...(app.cols.planned ?? [])].sort(soonest)
37
+ const timed = all.filter((r) => r.dueAt != null)
38
+ const onSave = all.filter((r) => r.dueAt == null)
39
+ return (
40
+ <div className="colcard checkcard">
41
+ <div className="chead">
42
+ <span className="tid">plan</span>
43
+ <h3 className="ctitle">{app.name}</h3>
44
+ </div>
45
+ <div className="usays">what the machine runs here on its own time, soonest first. Every repo at once: the planned page (⌘')</div>
46
+ {app.cols.planned == null ? <div className="dim">no plan covers this repo</div> : null}
47
+ {timed.length > 0 ? <section className="csec"><div className="cstitle">next</div><PlanTable app={app} rows={timed} /></section> : null}
48
+ {onSave.length > 0 ? <section className="csec"><div className="cstitle">on a save only</div><PlanTable app={app} rows={onSave} /></section> : null}
49
+ </div>
50
+ )
51
+ })