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
@@ -1,32 +1,35 @@
1
1
  // the home's strip over every page it serves: one line that never wraps. On a
2
2
  // repo page the shown repo (its icon, its name in its own colour, the two
3
- // swatches that set those colours), what it runs now and its two runs, then
4
- // on the right its processes, its hub's trouble and its editor icons
5
- // (header-bar.tsx); on the overview and the plan, the page's name. At the
6
- // far right, only machine wide trouble, and only while it lasts (pm or the
7
- // daemon down, a child or a global process crashed). The places themselves
8
- // are the place buttons at the top of the project list (⌘J, ⌘L, ⌘', the
9
- // picker's ⌘K between the first two) and the first three entries of the
3
+ // swatches that set those colours), its check… button and a spinner while
4
+ // it runs, then on the right its processes and its editor icons
5
+ // (header-bar.tsx); on the overview and the plan, the page's name. The strip
6
+ // carries no trouble text: the shown hub's trouble and the machine's (the
7
+ // daemon, pm or the web not answering, a child or a global process crashed, a
8
+ // failed poll) are cards floating under it at the top right, only while they
9
+ // last (float-cards.tsx). The machine not answering is said there and nowhere
10
+ // else (reach.ts). The places themselves
11
+ // are the place buttons at the top of the project list (the picker's ⌘J,
12
+ // then ⌘K, ⌘L, ⌘') and the first three entries of the
10
13
  // picker. What administers one repo (reload its hub, keep it active, archive
11
14
  // it) is its admin page (admin-view.tsx). NotUp stands in for the hub page
12
15
  // while its hub cannot answer, with the fix. Every link of the home is a
13
- // page of the same app: a plain click moves it in place. The daemon or pm
14
- // down is said in MachineBar, a reserved line the strip brings with it,
15
- // drawn last in the page column (its flex order).
16
+ // page of the same app: a plain click moves it in place.
16
17
  import { observer } from 'mobx-react-lite'
17
18
  import type { ReactNode } from 'react'
18
- import type { ProcessIcon, ResourceView } from '../../home/resources.ts'
19
+ import type { OpenHow, ProcessIcon, ResourceView } from '../../home/resources.ts'
19
20
  import type { WorkerView } from '../../home/hubs.ts'
20
21
  import { NOT_INSTALLED } from '../../home/home-words.ts'
21
22
  import { splitWidgetHead } from '../../home/widget-text.ts'
22
- import type { MachineSt, OpenHow } from './machine-st.ts'
23
+ import type { MachineSt } from './machine-st.ts'
23
24
  import type { Role } from '../../home/pm.ts'
24
25
  import { processIcon, stateWords, type ProcessRow } from '../../home/process-rows.ts'
25
26
  import { plainClick, repoPage, type HomeSt } from './home-st.ts'
26
27
  import { stampWords } from './time-ago.tsx'
27
28
  import { Icon, ICON } from './icons.tsx'
28
- import { Tip } from './floating.tsx'
29
- import { notUp, RepoOpeners, RepoRun, RepoTools } from './header-bar.tsx'
29
+ import { Pop, Tip } from './floating.tsx'
30
+ import { renderMd } from './md.ts'
31
+ import { FloatCards, type Card } from './float-cards.tsx'
32
+ import { hubCards, notUp, RepoOpeners, RepoRun, RepoTools } from './header-bar.tsx'
30
33
 
31
34
  const STATUS_WORD: Record<WorkerView['status'], string> = { up: 'up', starting: 'starting', crashed: 'crashed', down: 'down', stopping: 'stopping' }
32
35
 
@@ -42,9 +45,9 @@ export const HomeLink = ({ app, to, className, title, children }: { app: HomeSt;
42
45
  }}>{children}</a>
43
46
  )
44
47
 
45
- const Log = ({ lines }: { lines: string[] }) => (lines.length === 0 ? null : <pre className="hlog">{lines.join('\n')}</pre>)
48
+ export const Log = ({ lines }: { lines: string[] }) => (lines.length === 0 ? null : <pre className="hlog">{lines.join('\n')}</pre>)
46
49
 
47
- /** one switch per global process; its log floats under it on hover */
50
+ /** a crashed global process's switch (start it again); its log floats under it on hover */
48
51
  export const ResourceChip = observer(({ m, r }: { m: MachineSt; r: ResourceView }) => {
49
52
  const on = r.status === 'up' || r.status === 'starting'
50
53
  const title = [r.description, r.where != null ? `on ${r.where}` : null, r.note].filter((x) => x != null).join(' · ')
@@ -64,81 +67,81 @@ export const ResourceChip = observer(({ m, r }: { m: MachineSt; r: ResourceView
64
67
  const Mark = ({ i }: { i: ProcessIcon | null }) =>
65
68
  i == null ? <i className="hpico" /> : 'text' in i ? <span className="hpico">{i.text}</span> : <img className="hpico" src={i.src} alt="" width={16} height={16} />
66
69
 
67
- /** a global process's state in words, on the one clock: `up 2h`, `crashed 3m ago` */
68
- const StateWords = observer(({ m, r }: { m: MachineSt; r: ProcessRow }) => (
69
- <span title={r.since == null ? undefined : `since ${stampWords(r.since)}`}>{stateWords(r, m.clock.now)}</span>
70
- ))
70
+ /** a process's state in words, on the one clock (`up 2h`, `crashed 3m ago`,
71
+ * `no answer`). What it has to say (its note, a refused take over) is a
72
+ * mark after the words and the hover, never a line of its own, so the
73
+ * table keeps its height whatever a process says */
74
+ const StateCell = observer(({ m, r }: { m: MachineSt; r: ProcessRow }) => {
75
+ const say = [r.note, r.takeOver?.ok === false ? `cannot take over: ${r.takeOver.why}` : null].filter((x) => x != null)
76
+ const title = [r.since == null ? null : `since ${stampWords(r.since)}`, ...say].filter((x) => x != null).join('\n')
77
+ return (
78
+ <td className={`gpstate ${r.dot}`}>
79
+ <span title={title === '' ? undefined : title}>{stateWords(r, m.clock.now)}</span>
80
+ {say.length > 0 ? <i className="gpwarn" /> : null}
81
+ </td>
82
+ )
83
+ })
71
84
 
72
85
  /** the button columns a table holds: a kind no line needs takes none */
73
86
  type BtnCols = { act: string | null; take: boolean }
74
87
 
75
88
  /** one process as one line, like a commit on the activity page: its dot,
76
- * icon and label (cut to its column, whole with its log on hover), its url
77
- * as a link, its version, its pid (a click copies it), its state, then one
78
- * cell per kind of button, so every start sits under every start: its
79
- * declared actions, take over, restart, start (stop while it runs), open.
80
- * One of shipkit's own wears a core tag and has its stop disabled: it is
81
- * always on, restarted through pm (pm itself by launchd). A running line is
82
- * tinted green, a crashed one red */
83
- const ProcessLine = observer(({ m, r, repoIcon, head, cols, span }: { m: MachineSt; r: ProcessRow; repoIcon: string | null; head: ReactNode; cols: BtnCols; span: number }) => {
89
+ * icon and label (cut to its column, whole with its log on hover), then
90
+ * its two switches as small icons, start or stop (play while stopped, stop
91
+ * while it runs) and restart, each in a column of its own so every play
92
+ * sits under every play, then its url: open in Chrome, open as its own
93
+ * app (a spinner while that app builds), then the url itself, a link to
94
+ * the default browser. Each starts the process first when it is down. Then
95
+ * its version, its pid (a click copies it), its state, and its
96
+ * declared actions and take over. One of shipkit's own wears a core tag and
97
+ * has its stop disabled: it is always on, restarted through pm (pm itself
98
+ * by launchd). A running line is tinted green, a crashed one red */
99
+ const ProcessLine = observer(({ m, r, repoIcon, head, cols }: { m: MachineSt; r: ProcessRow; repoIcon: string | null; head: ReactNode; cols: BtnCols }) => {
84
100
  const on = r.status === 'up' || r.status === 'starting'
85
101
  const about = [r.label, ...(r.core ? [r.description] : [`${r.repo}/${r.file}`, r.description, r.where != null ? `on ${r.where}` : null])].filter((x) => x != null).join(' · ')
86
102
  const log = r.tail.length > 0 ? `${about}\n\n${r.tail.join('\n')}` : null
87
- const via = m.openHow === 'app' ? 'as a shipkit app window' : 'in the default browser'
88
- const note = r.takeOver?.ok === false ? `cannot take over: ${r.takeOver.why}` : r.note
103
+ const first = r.status === 'up' ? '' : `start ${r.label}, then `
89
104
  const stopTitle = r.core ? `always on: ${r.id === 'pm' ? 'pm is the LaunchAgent com.shipkit.pm, launchd starts it again when it dies' : `restart ${r.label} instead`}`
90
105
  : on && !r.canStop ? `${r.label} was started outside pm, so pm cannot stop it` : on ? `stop ${r.label}` : `start ${r.label}`
106
+ const stops = on || r.core
91
107
  const copied = r.pid != null && m.copiedPid === r.pid
92
108
  return (
93
- <>
94
- <tr className={`gpline ${r.dot}`}>
95
- {head}
96
- <td className="gpname">
97
- <Tip className="gplabel" tipClass="hpop" placement="bottom-start" interactive content={log} data-log={log == null ? undefined : 'yes'}>
98
- <i className={`hdot ${r.dot}`} /><Mark i={processIcon(r, repoIcon)} /><span title={log != null ? undefined : about}>{r.label}</span>
99
- {r.core ? <b className="gptag" title="built into shipkit: always on">core</b> : null}
100
- </Tip>
101
- </td>
102
- <td className="gphost">
103
- {r.url == null ? null : r.core ? <span>{r.host}</span> : (
104
- <a href={r.url} title={`open ${r.url} ${via}`} onClick={(e) => {
109
+ <tr className={`gpline ${r.dot}`}>
110
+ {head}
111
+ <td className="gpname">
112
+ <Tip className="gplabel" tipClass="hpop" placement="bottom-start" interactive content={log} data-log={log == null ? undefined : 'yes'}>
113
+ <i className={`hdot ${r.dot}`} /><Mark i={processIcon(r, repoIcon)} /><span title={log != null ? undefined : about}>{r.label}</span>
114
+ {r.core ? <b className="gptag" title="built into shipkit: always on">core</b> : null}
115
+ </Tip>
116
+ </td>
117
+ <td className="gpb stop"><button className={`gpi${stops ? '' : ' go'}`} aria-label={stops ? 'stop' : 'start'} disabled={r.core || r.status === 'stopping' || r.status === 'unknown' || (on && !r.canStop)}
118
+ title={stopTitle} onClick={() => void m.processAct(r.id, on ? 'stop' : 'start')}><Icon d={stops ? ICON.stop : ICON.play} /></button></td>
119
+ <td className="gpb restart">{r.canRestart ? <button className="gpi" aria-label="restart" onClick={() => void (r.core ? m.restartChild(r.id as Role) : m.processAct(r.id, 'restart'))} title={r.core ? `stop the ${r.label} and start it again, through pm` : `stop ${r.label} and start it again`}><Icon d={ICON.restart} /></button> : null}</td>
120
+ <td className="gphost">
121
+ {r.url == null ? null : r.core ? <span>{r.host}</span> : (
122
+ <>
123
+ <button className="gpi" aria-label="open in Chrome" onClick={() => void m.openProcess(r.id, 'chrome')} title={`${first}open ${r.url} in Chrome`}><Icon d={ICON.browser} /></button>
124
+ <button className="gpi" aria-label="open as an app" disabled={r.appBusy} aria-busy={r.appBusy || undefined} onClick={() => void m.openProcess(r.id, 'app')}
125
+ title={r.appBusy ? `building the ${r.label} app, then opening it` : `${first}open ${r.url} as its own app, ${r.label} in the dock`}>{r.appBusy ? <span className="spin" /> : <Icon d={ICON.window} />}</button>
126
+ <a href={r.url} title={`${first}open ${r.url} in the default browser`} onClick={(e) => {
105
127
  if (!plainClick(e)) return
106
128
  e.preventDefault()
107
- void m.openProcess(r.id)
129
+ void m.openProcess(r.id, 'browser')
108
130
  }}>{r.host}</a>
109
- )}
110
- </td>
111
- <td className="gpver">{r.version ?? ''}</td>
112
- <td className="gppid">{r.pid == null ? null : <button className={copied ? 'gpcopy on' : 'gpcopy'} title={`copy pid ${r.pid}`} onClick={() => void m.copyPid(r.pid!)}>{copied ? 'copied' : r.pid}</button>}</td>
113
- <td className={`gpstate ${r.dot}`}><StateWords m={m} r={r} /></td>
114
- {cols.act == null ? null : <td className="gpb act">{r.actions.map((a) => (
115
- <button key={a.id} className="ghost hsm" disabled={r.running != null} onClick={() => void m.processAct(r.id, 'action', a.id)}>{r.running === a.id ? <span className="spin" /> : null}{a.label}</button>
116
- ))}</td>}
117
- {!cols.take ? null : <td className="gpb take">{r.takeOver?.ok === true ? <button className="ghost hsm" onClick={() => void m.processAct(r.id, 'takeover')} title={`stop the copy of ${r.label} started elsewhere (the process group listening on its port) and start one pm supervises, in one go`}>take over</button> : null}</td>}
118
- <td className="gpb restart">{r.canRestart ? <button className="ghost hsm" onClick={() => void (r.core ? m.restartChild(r.id as Role) : m.processAct(r.id, 'restart'))} title={r.core ? `stop the ${r.label} and start it again, through pm` : `stop ${r.label} and start it again`}>restart</button> : null}</td>
119
- <td className="gpb stop"><button className={on || r.core ? 'ghost' : 'primary'} disabled={r.core || r.status === 'stopping' || r.status === 'unknown' || (on && !r.canStop)}
120
- title={stopTitle} onClick={() => void m.processAct(r.id, on ? 'stop' : 'start')}>{on || r.core ? 'stop' : 'start'}</button></td>
121
- <td className="gpb open">{r.url != null && !r.core ? <button className="ghost" onClick={() => void m.openProcess(r.id)} title={r.status === 'up' ? `open ${r.url} ${via}` : `start ${r.label}, then open ${r.url} ${via}`}>open</button> : null}</td>
122
- </tr>
123
- {note == null ? null : <tr className={`gpline ${r.dot}`}><td colSpan={span} className="gpnote hnote" title={note}>{note}</td></tr>}
124
- </>
131
+ </>
132
+ )}
133
+ </td>
134
+ <td className="gpver">{r.version ?? ''}</td>
135
+ <td className="gppid">{r.pid == null ? null : <button className={copied ? 'gpcopy on' : 'gpcopy'} title={`copy pid ${r.pid}`} onClick={() => void m.copyPid(r.pid!)}>{copied ? 'copied' : r.pid}</button>}</td>
136
+ <StateCell m={m} r={r} />
137
+ {cols.act == null ? null : <td className="gpb act">{r.actions.map((a) => (
138
+ <button key={a.id} className="ghost hsm" disabled={r.running != null} onClick={() => void m.processAct(r.id, 'action', a.id)}>{r.running === a.id ? <span className="spin" /> : null}{a.label}</button>
139
+ ))}</td>}
140
+ {!cols.take ? null : <td className="gpb take">{r.takeOver?.ok === true ? <button className="ghost hsm" onClick={() => void m.processAct(r.id, 'takeover')} title={`stop the copy of ${r.label} started elsewhere (the process group listening on its port) and start one pm supervises, in one go`}>take over</button> : null}</td>}
141
+ </tr>
125
142
  )
126
143
  })
127
144
 
128
- const OPENERS: { how: OpenHow; d: string; title: string }[] = [
129
- { how: 'browser', d: ICON.browser, title: 'open opens the default browser' },
130
- { how: 'app', d: ICON.window, title: 'open opens a shipkit app window' },
131
- ]
132
-
133
- /** how every open button and url of the table opens, kept per browser */
134
- const OpenHowPick = observer(({ m }: { m: MachineSt }) => (
135
- <span className="seg hopen" role="group" aria-label="how open opens">
136
- {OPENERS.map((o) => (
137
- <button key={o.how} className={m.openHow === o.how ? 'on' : undefined} aria-pressed={m.openHow === o.how} title={o.title} onClick={() => m.setOpenHow(o.how)}><Icon d={o.d} /></button>
138
- ))}
139
- </span>
140
- ))
141
-
142
145
  /** the repo's icon and plain name, as on the activity page; a plain click shows it in place */
143
146
  const ProcessRepo = observer(({ app, name, icon }: { app: HomeSt; name: string; icon: string | null }) => (
144
147
  <a className="acrepo" href={repoPage(name)} onClick={(e) => {
@@ -154,35 +157,33 @@ const ProcessRepo = observer(({ app, name, icon }: { app: HomeSt; name: string;
154
157
  /** the processes as one table in the activity page's style: shipkit's own
155
158
  * first, then a row group per declaring repo in the project list's order,
156
159
  * its name in the first column, at the top of its group, then one line per
157
- * process. The header row ends with the new window button and the open
158
- * choice */
160
+ * process. The url's header carries the new window button */
159
161
  const ProcessTable = observer(({ app, m }: { app: HomeSt; m: MachineSt }) => {
160
162
  const groups = m.processGroups
161
163
  const rows = groups.flatMap((g) => g.rows)
162
164
  // wide enough for the longest set of declared actions, in the button font
163
165
  const act = Math.max(0, ...rows.map((r) => r.actions.reduce((n, a) => n + a.label.length + 3, 0)))
164
166
  const cols: BtnCols = { act: act === 0 ? null : `${act}ch`, take: rows.some((r) => r.takeOver?.ok === true) }
165
- const btns = 3 + (cols.act == null ? 0 : 1) + (cols.take ? 1 : 0)
166
167
  return (
167
168
  <div className="gpwrap">
168
169
  <table className="actable gptable">
169
170
  <colgroup>
170
- <col className="c-name" /><col className="c-gpname" /><col className="c-gphost" /><col className="c-gpver" /><col className="c-gppid" /><col className="c-gpstate" />
171
+ <col className="c-name" /><col className="c-gpname" /><col className="c-gpb stop" /><col className="c-gpb restart" /><col className="c-gphost" /><col className="c-gpver" /><col className="c-gppid" /><col className="c-gpstate" />
171
172
  {cols.act == null ? null : <col className="c-gpb act" style={{ width: cols.act }} />}
172
173
  {cols.take ? <col className="c-gpb take" /> : null}
173
- <col className="c-gpb restart" /><col className="c-gpb stop" /><col className="c-gpb open" />
174
174
  </colgroup>
175
175
  <thead><tr>
176
- <th>repo</th><th>process</th><th className="gphost">url</th><th className="gpver">version</th><th className="gppid">pid</th><th className="gpstate">state</th>
177
- <th colSpan={btns} className="gphead"><div><button className="ghost hsm" title="open one more window on this page" onClick={() => m.newWindow()}>new window</button><OpenHowPick m={m} /></div></th>
176
+ <th>repo</th><th>process</th><th colSpan={2} />
177
+ <th className="gphost gphead"><div><span>url</span><span className="sp" /><button className="ghost hsm" title="open one more window on this page" onClick={() => m.newWindow()}>new window</button></div></th>
178
+ <th className="gpver">version</th><th className="gppid">pid</th><th className="gpstate">state</th>
179
+ {cols.act == null ? null : <th />}{cols.take ? <th /> : null}
178
180
  </tr></thead>
179
181
  {groups.map((g) => {
180
182
  const icon = g.core ? null : app.projects.list.find((p) => p.name === g.repo)?.icon ?? null
181
- const lines = g.rows.length + g.rows.filter((r) => r.note != null || r.takeOver?.ok === false).length
182
- const head = <td rowSpan={lines} className="acname">{g.core ? <span className="acrepo gpcore"><Icon d={ICON.shipkit} /><span>{g.repo}</span></span> : <ProcessRepo app={app} name={g.repo} icon={icon} />}</td>
183
+ const head = <td rowSpan={g.rows.length} className="acname">{g.core ? <span className="acrepo gpcore"><Icon d={ICON.shipkit} /><span>{g.repo}</span></span> : <ProcessRepo app={app} name={g.repo} icon={icon} />}</td>
183
184
  return (
184
185
  <tbody key={g.core ? ':core' : g.repo} className="acgrp">
185
- {g.rows.map((r, i) => <ProcessLine key={r.id} m={m} r={r} repoIcon={icon} head={i === 0 ? head : null} cols={cols} span={5 + btns} />)}
186
+ {g.rows.map((r, i) => <ProcessLine key={r.id} m={m} r={r} repoIcon={icon} head={i === 0 ? head : null} cols={cols} />)}
186
187
  </tbody>
187
188
  )
188
189
  })}
@@ -191,7 +192,10 @@ const ProcessTable = observer(({ app, m }: { app: HomeSt; m: MachineSt }) => {
191
192
  )
192
193
  })
193
194
 
194
- /** every house repo's widgets: the html the daemon last rendered, each under its title */
195
+ /** every house repo's widgets: the html the daemon last rendered, each under
196
+ * its title. An error is a mark on the title, its words on hover, so a card
197
+ * keeps its lines; a card with no html yet says it where the html goes. A
198
+ * widget file that failed to load is one mark after the cards */
195
199
  const Monitor = observer(({ m }: { m: MachineSt }) => {
196
200
  const w = m.widgets
197
201
  if (w == null || (w.widgets.length === 0 && w.errors.length === 0)) return null
@@ -205,14 +209,18 @@ const Monitor = observer(({ m }: { m: MachineSt }) => {
205
209
  <h3>
206
210
  {x.title}
207
211
  {head == null ? null : <span className="hdim"> · <span dangerouslySetInnerHTML={{ __html: head }} /></span>}
212
+ {x.error != null && x.html != null ? <i className="gpwarn" title={x.error} /> : null}
208
213
  </h3>
209
- {x.error != null ? <p className="hnote">{x.error}</p> : null}
210
- {x.html == null ? <p className="twait"><span className="spin" /></p> : <div dangerouslySetInnerHTML={{ __html: body }} />}
214
+ {x.html != null ? <div dangerouslySetInnerHTML={{ __html: body }} />
215
+ : x.error != null ? <div className="hwerr">{x.error}</div>
216
+ : <p className="twait"><span className="spin" /></p>}
211
217
  </section>
212
218
  )
213
219
  })}
220
+ {w.errors.length === 0 ? null : (
221
+ <span className="hwerr" title={w.errors.join('\n')}><i className="gpwarn" />{w.errors.length} widget file{w.errors.length === 1 ? '' : 's'} failed</span>
222
+ )}
214
223
  </div>
215
- {w.errors.map((e) => <p key={e} className="hnote">{e}</p>)}
216
224
  </section>
217
225
  )
218
226
  })
@@ -244,17 +252,92 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
244
252
  )
245
253
  })
246
254
 
247
- /** what is wrong with the machine itself, one chip each. Nothing when all is
248
- * well: the strip states trouble, never health */
249
- const MachineAlerts = observer(({ m }: { m: MachineSt }) => (
250
- <>
251
- {(m.pm?.children ?? []).filter((c) => c.status === 'crashed' || c.status === 'down').map((c) => (
252
- <span key={c.id} className="halert" title={c.note ?? c.tail.at(-1) ?? ''}><Dot s={c.status} />{c.id} {STATUS_WORD[c.status]}</span>
253
- ))}
254
- {(m.pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r) => <ResourceChip key={r.id} m={m} r={r} />)}
255
- {m.error !== '' ? <span className="herr" title={m.error}>{m.error}</span> : null}
256
- </>
257
- ))
255
+ /** the machine not answering, as ONE card: the web (pm behind it is then
256
+ * unknown, not down), the daemon, pm, or the daemon and pm together */
257
+ const downCard = (m: MachineSt): Card[] => {
258
+ const web = m.down !== '' && m.downWho === 'web'
259
+ const pm = m.pmDown !== '' && !web
260
+ if (m.down === '' && !pm) return []
261
+ const head = m.down === '' ? 'pm down' : pm ? 'daemon and pm down' : `${m.downWho} down`
262
+ return [{ key: 'down', tone: 'red', head, body: m.down === '' ? m.pmDown : pm ? <>{m.down}<br />{m.pmDown}</> : m.down }]
263
+ }
264
+
265
+ /** what is wrong with the machine itself, one card each: the machine not
266
+ * answering, a crashed child of pm, a crashed global process (its switch
267
+ * starts it again), a failed poll. While pm is silent its last answer says
268
+ * nothing here */
269
+ const machineCards = (m: MachineSt): Card[] => {
270
+ const pm = m.pmDown !== '' ? null : m.pm
271
+ return [
272
+ ...downCard(m),
273
+ ...(pm?.children ?? []).filter((c) => c.status === 'crashed' || c.status === 'down').map((c): Card => ({ key: `child:${c.id}`, tone: 'red', head: `${c.id} ${STATUS_WORD[c.status]}`, body: c.note ?? c.tail.at(-1) ?? '' })),
274
+ ...(pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r): Card => ({ key: `res:${r.id}`, tone: 'red', head: 'crashed', body: <ResourceChip m={m} r={r} /> })),
275
+ ...(m.error !== '' ? [{ key: 'poll', tone: 'red' as const, body: m.error }] : []),
276
+ ]
277
+ }
278
+
279
+ /** a global process's menu: start or stop, restart, its url opened three
280
+ * ways (each starts it first when it is down), then its declared actions and
281
+ * take over. Every entry is a click on purpose, never the chip itself */
282
+ /** `lastLog: false` where the whole log is drawn under it, so its last line is not said twice */
283
+ export const ProcessMenu = observer(({ m, r, close, lastLog = true }: { m: MachineSt; r: ProcessRow; close: () => void; lastLog?: boolean }) => {
284
+ const on = r.status === 'up' || r.status === 'starting'
285
+ const first = r.status === 'up' ? '' : ', starts it first'
286
+ const act = (a: Parameters<MachineSt['processAct']>[1], action?: string) => () => { close(); void m.processAct(r.id, a, action) }
287
+ const open = (how: OpenHow) => () => { close(); void m.openProcess(r.id, how) }
288
+ const log = lastLog ? r.lastLog : null
289
+ const about = r.description != null && r.description !== '' || r.note != null || log != null
290
+ return (
291
+ <div className="hmenu2">
292
+ <div className="hmenu hmacts">
293
+ <div className="hmhead"><i className={`pdot ${r.status}`} /><b>{r.label}</b><span>{r.status}</span></div>
294
+ {on ? (
295
+ <button className="hmitem" disabled={r.status === 'stopping' || !r.canStop} onClick={act('stop')}>
296
+ <Icon d={ICON.stop} /><span><b>stop</b>{r.canStop ? null : <small>started outside pm, so pm cannot stop it</small>}</span>
297
+ </button>
298
+ ) : (
299
+ <button className="hmitem" disabled={r.status === 'unknown'} onClick={act('start')}><Icon d={ICON.play} /><span><b>start</b></span></button>
300
+ )}
301
+ {r.canRestart ? <button className="hmitem" onClick={act('restart')}><Icon d={ICON.restart} /><span><b>restart</b></span></button> : null}
302
+ {r.url == null ? null : (
303
+ <>
304
+ <button className="hmitem" onClick={open('browser')}><Icon d={ICON.browser} /><span><b>open in a browser tab</b><small>{r.host}{first}</small></span></button>
305
+ <button className="hmitem" onClick={open('chrome')}><Icon d={ICON.browser} /><span><b>open in Chrome</b><small>{r.host}{first}</small></span></button>
306
+ <button className="hmitem" disabled={r.appBusy} onClick={open('app')}><Icon d={ICON.window} /><span><b>open as an app</b><small>{r.appBusy ? `building the ${r.label} app` : `${r.label} in the dock${first}`}</small></span></button>
307
+ </>
308
+ )}
309
+ {r.actions.map((a) => <button key={a.id} className="hmitem" disabled={r.running != null} onClick={act('action', a.id)}><Icon d={ICON.process} /><span><b>{a.label}</b></span></button>)}
310
+ {r.takeOver?.ok === true ? <button className="hmitem" onClick={act('takeover')}><Icon d={ICON.restart} /><span><b>take over</b><small>stop the copy started elsewhere and start one pm supervises</small></span></button> : null}
311
+ </div>
312
+ {about ? (
313
+ <div className="hmabout">
314
+ {r.description != null && r.description !== '' ? <div className="md" dangerouslySetInnerHTML={{ __html: renderMd(r.description) }} /> : null}
315
+ {r.note != null ? <small className="hmnote">{r.note}</small> : null}
316
+ {log != null ? <code className="hmlog" title="its last log line">{log}</code> : null}
317
+ </div>
318
+ ) : null}
319
+ </div>
320
+ )
321
+ })
322
+
323
+ /** the shown repo's global processes, one chip each, right after its name: its
324
+ * dot and label, and a click opens its menu (ProcessMenu). pm runs them, so
325
+ * they show whether the repo's hub answers or not */
326
+ const RepoProcesses = observer(({ m, name }: { m: MachineSt; name: string }) => {
327
+ const rows = m.processGroups.find((g) => g.core !== true && g.repo === name)?.rows ?? []
328
+ if (rows.length === 0) return null
329
+ return (
330
+ <span className="hprocs hgprocs">
331
+ {rows.map((r) => (
332
+ <span key={r.id} className="hproc">
333
+ <Pop className={`ghost ${r.status}`} popClass="fpop fpopw" aria-label={`${r.label}, ${r.status}: open its menu`} label={<><i className={`pdot ${r.status}`} />{r.label}</>}>
334
+ {(close) => <ProcessMenu m={m} r={r} close={close} />}
335
+ </Pop>
336
+ </span>
337
+ ))}
338
+ </span>
339
+ )
340
+ })
258
341
 
259
342
  export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
260
343
  const m = app.machine
@@ -267,32 +350,17 @@ export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
267
350
  <>
268
351
  <div className="hstrip">
269
352
  {name == null ? <span className="hplace">{app.page === 'overview' ? 'overview' : app.page}</span> : <ShownRepo app={app} name={name} />}
270
- {live == null ? null : <RepoRun app={live} />}
353
+ {name == null ? null : <RepoProcesses m={m} name={name} />}
271
354
  <span className="sp" />
272
355
  {live == null ? null : <RepoTools app={live} />}
273
- <MachineAlerts m={m} />
356
+ {live == null ? null : <RepoRun app={live} />}
274
357
  {live == null ? null : <RepoOpeners app={live} />}
275
358
  </div>
276
- <MachineBar app={app} />
359
+ <FloatCards cards={[...(live == null ? [] : hubCards(live)), ...machineCards(m)]} />
277
360
  </>
278
361
  )
279
362
  })
280
363
 
281
- /** the daemon or pm not answering, on one line at the bottom of the window.
282
- * The line is always there, empty while all is well, so an alert never
283
- * moves the page. Only a failed probe sets down or pmDown: before the first
284
- * answer the line stays empty */
285
- export const MachineBar = observer(({ app }: { app: HomeSt }) => {
286
- const m = app.machine
287
- if (m == null) return null
288
- return (
289
- <div className="hbar">
290
- {m.down !== '' ? <span className="hdown" role="alert" title={m.down}><b>daemon down</b> {m.down}</span> : null}
291
- {m.pmDown !== '' ? <span className="hdown" role="alert" title={m.pmDown}><b>pm down</b> {m.pmDown}</span> : null}
292
- </div>
293
- )
294
- })
295
-
296
364
  /** archive the repo or bring it back: `archived` in its repo.config.ts, and
297
365
  * only that key. Archived, it stays listed and nothing runs there */
298
366
  export const ArchiveButton = observer(({ app, name }: { app: HomeSt; name: string }) => {
@@ -319,7 +387,7 @@ export const NotUp = observer(({ app }: { app: HomeSt }) => {
319
387
  <ArchiveButton app={app} name={name} />
320
388
  </div>
321
389
  )
322
- if (w == null) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? `waiting for the daemon to load ${name}…` : `looking for ${name}…`}</p></div>
390
+ if (w == null) return <div className="hwait"><p className="twait"><span className="spin" />{`looking for ${name}…`}</p></div>
323
391
  return (
324
392
  <div className="hwait">
325
393
  {w.blocked != null ? <p>{w.blocked}</p> : <p><Dot s={w.status} />{name}'s hub is {STATUS_WORD[w.status]}{w.note != null ? `: ${w.note}` : ''}</p>}
@@ -358,7 +426,13 @@ export const ArchivedView = observer(({ app }: { app: HomeSt }) => {
358
426
  export const Overview = observer(({ app }: { app: HomeSt }) => {
359
427
  const m = app.machine
360
428
  if (m == null) return null
361
- if (m.data == null) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? 'the daemon does not answer yet' : 'loading…'}</p></div>
429
+ // the table waits for pm's first word and for the list's order, so its
430
+ // rows never arrive or reorder after it paints. The daemon silent, it
431
+ // draws whatever it last had: the down card says the rest
432
+ const p = app.projects
433
+ const nothing = m.data == null && m.pm == null && m.widgets == null
434
+ const first = m.data == null || (m.pm == null && m.pmDown === '') || (p.list.length === 0 && !p.fresh && p.error === '')
435
+ if (nothing || (m.down === '' && first)) return <div className="hwait"><p className="twait"><span className="spin" />loading…</p></div>
362
436
  return (
363
437
  <div className="hall">
364
438
  <div className="hstack">
@@ -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" />
@@ -2,15 +2,17 @@
2
2
  // app for every page the home serves ('/', '/planned', '/activity', '/r/<repo>/') and for a
3
3
  // hub serving on its own at '/'. mobx+react per build/app-state-tree: HomeSt
4
4
  // is the single root (window.app), each repo a RepoSt under it, services own
5
- // state, components read and call. A repo's page is its sections over ONE
6
- // tree of every repo resource (sidebar.tsx), by path or by type, coloured by
7
- // state in the label itself, over one severity filter persisted at
5
+ // state, components read and call. A repo's page is ONE tree whose top rows
6
+ // are its sections and whose rows under them are every repo resource
7
+ // (sidebar.tsx), coloured by state in the label itself, under one filter
8
+ // input and one severity filter persisted at
8
9
  // ?f=needs · ?f=red. A row opens its item view, an asset row opens the atelier.
9
10
  import { runInAction } from 'mobx'
10
11
  import { observer } from 'mobx-react-lite'
11
12
  import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
12
13
  import { createRoot } from 'react-dom/client'
13
14
  import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
15
+ import { AskTable, CheckTable } from './col-tables-view.tsx'
14
16
  import { WIKIS, WikisView } from './wikis-view.tsx'
15
17
  import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
16
18
  import type { Filter, RepoSt } from './repo-st.ts'
@@ -23,6 +25,10 @@ import { TestCard } from './test-view.tsx'
23
25
  import { TestsPage } from './tests-view.tsx'
24
26
  import { AssetStage, CatGrid, InfoChips, MediaTopbar, ToolsBar, VariantsBar } from './media-view.tsx'
25
27
  import { Sidebar } from './sidebar.tsx'
28
+ import { ProcessesPage } from './processes-view.tsx'
29
+ import { PlanPage } from './plan-view.tsx'
30
+ import { WhenPage } from './when-view.tsx'
31
+ import { GitPage } from './git-view.tsx'
26
32
  import { AdminPage } from './admin-view.tsx'
27
33
  import { ReadmeView } from './readme-view.tsx'
28
34
  import { ProjectList } from './project-list.tsx'
@@ -34,7 +40,7 @@ import { ActivityView } from './activity-view.tsx'
34
40
  import { Palette } from './palette-view.tsx'
35
41
  import { SCREEN_WAIT_MS } from './screen-cache.ts'
36
42
  import { isSearchKey, pageKey, sectionKey } from './keys.ts'
37
- import { repoSections } from './repo-sections.tsx'
43
+ import { repoSections } from './repo-sections.ts'
38
44
  import { Dock } from './dock-view.tsx'
39
45
  import { DOCK } from './dock-st.ts'
40
46
  import { animateDock } from './dock-anim.ts'
@@ -84,9 +90,19 @@ const Main = observer(({ app }: { app: RepoSt }) => {
84
90
  : <div className="stagewrap"><ColCard app={app} col={app.sel.col} id={app.sel.id} /></div>}
85
91
  </main>
86
92
  )
87
- if (app.sel?.t === 'col') return <main>{errs}<ColGrid app={app} id={app.sel.id} /></main>
93
+ if (app.sel?.t === 'col') return (
94
+ <main>
95
+ {errs}
96
+ {app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : <ColGrid app={app} id={app.sel.id} />}
97
+ </main>
98
+ )
88
99
  if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
89
100
  if (app.sel?.t === 'admin') return <main>{errs}<div className="stagewrap"><AdminPage app={app} /></div></main>
101
+ if (app.sel?.t === 'plan') return <main>{errs}<div className="stagewrap"><PlanPage app={app} /></div></main>
102
+ if (app.sel?.t === 'git') return <main>{errs}<div className="stagewrap"><GitPage app={app} /></div></main>
103
+ if (app.sel?.t === 'checks') return <main>{errs}<CheckTable app={app} id="checks" from={app.sel.from} /></main>
104
+ if (app.sel?.t === 'when') return <main>{errs}<div className="stagewrap"><WhenPage app={app} /></div></main>
105
+ if (app.sel?.t === 'processes') return <main>{errs}<div className="stagewrap"><ProcessesPage app={app} id={app.sel.id} /></div></main>
90
106
  if (app.sel?.t === 'status') return <main>{errs}<div className="stagewrap"><StatusCard app={app} /></div></main>
91
107
  if (app.sel?.t === 'cat') return <main>{errs}<CatGrid app={app} /></main>
92
108
  if (app.sel?.t === 'asset') return (
@@ -1,7 +1,7 @@
1
1
  // Wire types the hub app reads — mirrors of the server shapes
2
2
  // (studio-server.ts, collection-types.ts). Kept structural on purpose: the
3
3
  // page renders what the API sends, the file is the truth.
4
- import type { ResourceType, SuiteFacts, TestFacts, TestRun } from '../../collections/collection-types.ts'
4
+ import type { AskFacts, ResourceType, SuiteFacts, TestFacts, TestRun } from '../../collections/collection-types.ts'
5
5
 
6
6
  export type SideAsset = {
7
7
  id: string
@@ -62,7 +62,7 @@ export type Artifact = { copy?: string; url?: string; steps?: string[] }
62
62
  * The list lives where the providers that emit it live, so a kind can never
63
63
  * be known on one side of the wire only. Type only, so nothing server side
64
64
  * reaches the bundle */
65
- export type { ResourceType, SuiteFacts, TestFacts, TestRun }
65
+ export type { AskFacts, ResourceType, SuiteFacts, TestFacts, TestRun }
66
66
 
67
67
  export type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip'
68
68
 
@@ -73,8 +73,10 @@ export type RecentRun = { at: number; ms: number; status: CheckStatus; replayed:
73
73
  // engine reaches the bundle: two declarations of one vocabulary is how a
74
74
  // switch ends up writing a word the other half does not know
75
75
  import type { WhenAt, WhenSource } from '../../core/when.ts'
76
+ import type { RunsCells } from '../../core/runs-matrix.ts'
77
+ import type { Line } from '../../core/wire.ts'
76
78
  import type { CheckFrom } from '../../check.ts'
77
- export type { CheckFrom, WhenAt, WhenSource }
79
+ export type { CheckFrom, WhenAt, WhenSource, RunsCells }
78
80
 
79
81
  /** a gate check's declaration and its numbers (collections/collection-types.ts
80
82
  * CheckFacts is the source; this is the wire mirror the page reads) */
@@ -86,6 +88,8 @@ export type CheckFacts = {
86
88
  /** the wire lines of its last run, rendered as inline markdown */
87
89
  lastLines: string[]
88
90
  gate: boolean
91
+ /** every context it runs in and how it caches */
92
+ runs: RunsCells
89
93
  /** a build step: the gate runs it, the health pass does not */
90
94
  build: boolean
91
95
  blocking: boolean | null
@@ -137,7 +141,7 @@ export type GateBlock = {
137
141
  path: string
138
142
  hash: string
139
143
  state: 'missing-file' | 'missing-block' | 'stale' | 'current'
140
- when: Record<string, string>
144
+ off: Record<string, string>
141
145
  tests: { budgetMs?: number; slow?: SlowSuite[]; serial?: { file: string; why: string }[] }
142
146
  declaredOutside: boolean
143
147
  dirty: boolean
@@ -154,7 +158,7 @@ export type CheckEvent = {
154
158
  replayed: boolean
155
159
  blocking: boolean | null
156
160
  advisory: boolean
157
- lines: { kind: string; text: string }[]
161
+ lines: Line[]
158
162
  }
159
163
  /** the colour of the tree label: red broken · warn taste pending · ok
160
164
  * passing on a check that ran · idle the neutral default · quiet archived or out of play */
@@ -186,6 +190,8 @@ export type Resource = {
186
190
  check?: CheckFacts
187
191
  /** test files only: where it runs and what it last cost */
188
192
  test?: TestFacts
193
+ /** asks only: kind, dates, what it blocks, its probe */
194
+ ask?: AskFacts
189
195
  }
190
196
  export type ColMeta = { id: string; title: string; media: string }
191
197
  export type Col = ColMeta & { instances: Resource[] }