rvlib-shipkit 1.2.0 → 1.4.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 (173) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  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 +15 -10
  7. package/src/check.ts +26 -10
  8. package/src/checks/SK001-package-json-shape.check.ts +34 -5
  9. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  11. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  12. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  13. package/src/checks/SK007-github-visibility.check.ts +9 -9
  14. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  15. package/src/checks/SK009-commit-ai-tells.check.ts +103 -61
  16. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  17. package/src/checks/SK014-prose-ste.check.ts +16 -6
  18. package/src/checks/SK015-nested-repo.check.ts +5 -1
  19. package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
  20. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  21. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
  22. package/src/checks/SK024-identity-colors.check.ts +5 -0
  23. package/src/checks/SK025-release-drift.check.ts +58 -0
  24. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  25. package/src/checks/SK027-agent-wiki-links.check.ts +50 -0
  26. package/src/checks/SK028-readme.check.ts +83 -0
  27. package/src/cli/cmd_Assets.ts +1 -1
  28. package/src/cli/cmd_Ci.ts +5 -0
  29. package/src/cli/cmd_CrossRepoActivity.ts +119 -0
  30. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  31. package/src/cli/cmd_Keys.ts +17 -3
  32. package/src/cli/cmd_Overview.ts +98 -0
  33. package/src/cli/cmd_Planned.ts +117 -0
  34. package/src/cli.ts +9 -1
  35. package/src/collections/checks.ts +3 -2
  36. package/src/collections/collection-types.ts +15 -0
  37. package/src/collections/providers.ts +7 -3
  38. package/src/collections/test-time.ts +10 -1
  39. package/src/contracts/apply-contracts.ts +9 -8
  40. package/src/contracts/contract-types.ts +8 -2
  41. package/src/contracts/contracts-engine.ts +34 -13
  42. package/src/contracts/manifests/identity.contract.ts +6 -5
  43. package/src/contracts/manifests/social.contract.ts +4 -2
  44. package/src/contracts/manifests/steam.contract.ts +7 -3
  45. package/src/contracts/web-surface.ts +59 -0
  46. package/src/core/agent-spawn.ts +46 -0
  47. package/src/core/archived.ts +82 -0
  48. package/src/core/budget.ts +19 -9
  49. package/src/core/canonical-scripts.ts +12 -3
  50. package/src/core/check-runner.ts +66 -17
  51. package/src/core/discover-checks.ts +10 -5
  52. package/src/core/file-stamp.ts +20 -0
  53. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  54. package/src/core/identity-color.ts +1 -1
  55. package/src/core/layout.ts +2 -0
  56. package/src/core/motherhome.ts +88 -66
  57. package/src/core/repo-facts.ts +14 -0
  58. package/src/core/retention.ts +14 -5
  59. package/src/core/retired-checks.ts +7 -0
  60. package/src/core/run-check.ts +8 -7
  61. package/src/core/run-ci.ts +46 -15
  62. package/src/core/run-fix.ts +7 -2
  63. package/src/core/run-fixers.ts +10 -6
  64. package/src/core/wire.ts +49 -9
  65. package/src/fleet/scan-fleet.ts +2 -1
  66. package/src/home/activity-shape.ts +138 -0
  67. package/src/home/activity.ts +256 -0
  68. package/src/home/global-units.ts +3 -2
  69. package/src/home/gui-app/ShipkitGui.swift +144 -25
  70. package/src/home/gui-app.ts +93 -30
  71. package/src/home/home-launchd.ts +10 -2
  72. package/src/home/home-routes.ts +34 -6
  73. package/src/home/home-start.ts +33 -5
  74. package/src/home/machine-stores.ts +4 -2
  75. package/src/home/open-app.ts +23 -10
  76. package/src/home/pm-config.ts +15 -11
  77. package/src/home/pm-inputs.ts +76 -0
  78. package/src/home/pm.ts +40 -7
  79. package/src/home/process-rows.ts +222 -0
  80. package/src/home/resources.ts +99 -20
  81. package/src/home/run-daemon.ts +12 -7
  82. package/src/home/run-pm.ts +30 -5
  83. package/src/home/serve-web.ts +6 -3
  84. package/src/home/supervise.ts +16 -1
  85. package/src/home/widget-text.ts +119 -0
  86. package/src/home/widgets.ts +32 -3
  87. package/src/hooks/commit-message.ts +2 -3
  88. package/src/hooks/run-hooks.ts +32 -5
  89. package/src/ledger/db.ts +1 -1
  90. package/src/ledger/gate-checks.ts +20 -6
  91. package/src/ledger/house-runs.ts +43 -0
  92. package/src/ledger/live-timeline.ts +4 -19
  93. package/src/ledger/run-log.ts +39 -10
  94. package/src/ledger/store.ts +30 -2
  95. package/src/ledger/world-cache.ts +63 -14
  96. package/src/plan/cadence.ts +21 -5
  97. package/src/plan/catch-up.ts +37 -0
  98. package/src/plan/plan-timeline.ts +99 -0
  99. package/src/plan/planner.ts +138 -13
  100. package/src/plan/repo-source.ts +120 -8
  101. package/src/release/run-release.ts +9 -2
  102. package/src/release/unit-resolve.ts +8 -12
  103. package/src/repo-config.ts +17 -4
  104. package/src/serve/house-runs.ts +53 -0
  105. package/src/serve/hub-app/activity-st.ts +168 -0
  106. package/src/serve/hub-app/activity-view.tsx +218 -0
  107. package/src/serve/hub-app/activity.css +78 -0
  108. package/src/serve/hub-app/admin-svc.ts +41 -0
  109. package/src/serve/hub-app/admin-view.tsx +42 -0
  110. package/src/serve/hub-app/chart-view.ts +12 -5
  111. package/src/serve/hub-app/check-words.ts +2 -0
  112. package/src/serve/hub-app/checks-svc.ts +2 -1
  113. package/src/serve/hub-app/col-tables-view.tsx +124 -0
  114. package/src/serve/hub-app/col-tables.ts +115 -0
  115. package/src/serve/hub-app/cols-svc.ts +28 -0
  116. package/src/serve/hub-app/controls.css +32 -0
  117. package/src/serve/hub-app/cycle-button.tsx +36 -0
  118. package/src/serve/hub-app/dock-anim.ts +25 -0
  119. package/src/serve/hub-app/dock-st.ts +329 -0
  120. package/src/serve/hub-app/dock-view.tsx +68 -0
  121. package/src/serve/hub-app/dock.css +35 -0
  122. package/src/serve/hub-app/hash-route.ts +7 -1
  123. package/src/serve/hub-app/header-bar.tsx +79 -104
  124. package/src/serve/hub-app/home-st.ts +50 -13
  125. package/src/serve/hub-app/home-strip.css +81 -23
  126. package/src/serve/hub-app/home-strip.tsx +243 -102
  127. package/src/serve/hub-app/home.html +4 -0
  128. package/src/serve/hub-app/hub-app.tsx +103 -161
  129. package/src/serve/hub-app/hub-types.ts +6 -3
  130. package/src/serve/hub-app/hub.css +101 -39
  131. package/src/serve/hub-app/icons.tsx +27 -3
  132. package/src/serve/hub-app/index.html +4 -0
  133. package/src/serve/hub-app/keys.ts +44 -9
  134. package/src/serve/hub-app/live-svc.ts +24 -64
  135. package/src/serve/hub-app/machine-st.ts +65 -13
  136. package/src/serve/hub-app/md.ts +38 -5
  137. package/src/serve/hub-app/next-run.ts +1 -1
  138. package/src/serve/hub-app/palette-st.ts +13 -9
  139. package/src/serve/hub-app/palette-view.tsx +47 -0
  140. package/src/serve/hub-app/pill-row.tsx +65 -0
  141. package/src/serve/hub-app/planned-st.ts +21 -85
  142. package/src/serve/hub-app/planned-view.tsx +12 -7
  143. package/src/serve/hub-app/planned.css +5 -2
  144. package/src/serve/hub-app/project-list.css +43 -21
  145. package/src/serve/hub-app/project-list.tsx +122 -23
  146. package/src/serve/hub-app/projects-svc.ts +120 -12
  147. package/src/serve/hub-app/readme-svc.ts +38 -0
  148. package/src/serve/hub-app/readme-view.tsx +19 -0
  149. package/src/serve/hub-app/readme.css +11 -0
  150. package/src/serve/hub-app/repo-sections.tsx +61 -0
  151. package/src/serve/hub-app/repo-st.ts +26 -7
  152. package/src/serve/hub-app/runs-tabs.ts +1 -1
  153. package/src/serve/hub-app/sidebar.tsx +84 -0
  154. package/src/serve/hub-app/tests-view.tsx +31 -4
  155. package/src/serve/hub-app/timeline-chart.tsx +31 -27
  156. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  157. package/src/serve/hub-app/timeline-svc.ts +2 -2
  158. package/src/serve/hub-app/timeline-view.tsx +29 -9
  159. package/src/serve/hub-runtime.ts +17 -9
  160. package/src/serve/other-repos.ts +8 -45
  161. package/src/serve/project-gutter-route.ts +100 -20
  162. package/src/serve/project-gutter.ts +34 -13
  163. package/src/serve/readme-route.ts +57 -0
  164. package/src/serve/serve-hub.ts +13 -8
  165. package/src/serve/tracked-watch.ts +33 -1
  166. package/src/serve/warm-cell.ts +49 -16
  167. package/src/shipkit-files.ts +20 -4
  168. package/src/suite/judge-suites.ts +42 -17
  169. package/src/suite/suite-cli.ts +5 -8
  170. package/src/wiki/lint-md.ts +2 -0
  171. package/src/wiki/scan-md.ts +37 -10
  172. package/src/checks/SK023-stores-under-cap.check.ts +0 -49
  173. package/src/checks/contracts.check.ts +0 -20
@@ -1,37 +1,44 @@
1
1
  // THE page (Bun.serve HTML import — no vite, no build step), one single page
2
- // app for every page the home serves ('/', '/planned', '/r/<repo>/') and for a
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 ONE tree of every repo
6
- // resource, by path or by type, coloured by state in the label itself, over
7
- // one severity filter persisted at ?f=needs · ?f=red. A row opens its item
8
- // view, an asset row opens the atelier.
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
8
+ // ?f=needs · ?f=red. A row opens its item view, an asset row opens the atelier.
9
9
  import { runInAction } from 'mobx'
10
10
  import { observer } from 'mobx-react-lite'
11
- import { StrictMode, useEffect, useRef } from 'react'
11
+ import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
12
12
  import { createRoot } from 'react-dom/client'
13
13
  import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
14
+ import { AskTable, CheckTable } from './col-tables-view.tsx'
14
15
  import { WIKIS, WikisView } from './wikis-view.tsx'
15
16
  import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
16
17
  import type { Filter, RepoSt } from './repo-st.ts'
17
- import { HomeSt } from './home-st.ts'
18
+ import { HomeSt, PLACE_PATH } from './home-st.ts'
18
19
  import { TimelineView } from './timeline-view.tsx'
19
- import { HeaderBar, LIVE_KEY, LiveStrip } from './header-bar.tsx'
20
+ import { HeaderBar, notUp } from './header-bar.tsx'
20
21
  import { FOLLOW_KEY } from './timeline-chart.tsx'
21
22
  import { CheckCard } from './check-view.tsx'
22
23
  import { TestCard } from './test-view.tsx'
23
24
  import { TestsPage } from './tests-view.tsx'
24
25
  import { AssetStage, CatGrid, InfoChips, MediaTopbar, ToolsBar, VariantsBar } from './media-view.tsx'
25
- import { TYPE_MARK } from './tree-build.ts'
26
- import { TreeRowView } from './tree-row.tsx'
27
- import { Help } from './help.tsx'
26
+ import { Sidebar } from './sidebar.tsx'
27
+ import { AdminPage } from './admin-view.tsx'
28
+ import { ReadmeView } from './readme-view.tsx'
28
29
  import { ProjectList } from './project-list.tsx'
29
- import { Group, Panel, Separator, type Layout } from 'react-resizable-panels'
30
+ import { Group, Panel, Separator, usePanelRef, type Layout } from 'react-resizable-panels'
30
31
  import { LIST, SIDEBAR } from './layout-st.ts'
31
- import { HomeStrip, NotUp, Overview } from './home-strip.tsx'
32
+ import { ArchivedView, HomeStrip, NotUp, Overview } from './home-strip.tsx'
32
33
  import { PlannedView } from './planned-view.tsx'
34
+ import { ActivityView } from './activity-view.tsx'
35
+ import { Palette } from './palette-view.tsx'
33
36
  import { SCREEN_WAIT_MS } from './screen-cache.ts'
34
- import { pageKey } from './keys.ts'
37
+ import { isSearchKey, pageKey, sectionKey } from './keys.ts'
38
+ import { repoSections } from './repo-sections.tsx'
39
+ import { Dock } from './dock-view.tsx'
40
+ import { DOCK } from './dock-st.ts'
41
+ import { animateDock } from './dock-anim.ts'
35
42
 
36
43
  // home.html marks the home's page; index.html is a hub serving on its own
37
44
  const home = new HomeSt({ home: document.body.dataset.home != null })
@@ -39,112 +46,6 @@ const home = new HomeSt({ home: document.body.dataset.home != null })
39
46
  ;(window as unknown as { app: HomeSt }).app = home
40
47
  home.boot()
41
48
 
42
- // ── the tree: every resource of the repo, one row deep in its path ──────
43
- // the row itself is tree-row.tsx, so a test renders it headless
44
-
45
- const Tree = observer(({ app }: { app: RepoSt }) => {
46
- const t = app.tree
47
- return (
48
- // roving focus: the arrows belong to the tree only while it HAS the
49
- // focus, so the atelier keeps its own keys
50
- <div className="tree" tabIndex={0} onKeyDown={(e) => { if (t.key(e.key)) e.preventDefault() }}>
51
- {t.rows.map((r) => <TreeRowView key={r.key} app={app} row={r} />)}
52
- {!app.cols.loaded && !app.cols.cached ? <div className="tempty twait"><span className="spin" />reading the repo…</div> : null}
53
- {app.cols.loaded && t.rows.length === 0 ? <div className="tempty">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : app.filter === 'needs' ? 'nothing needs you ✨' : 'no resources yet'}</div> : null}
54
- </div>
55
- )
56
- })
57
-
58
- /** what every glyph in the type slot means, behind one `(?)` in the header */
59
- const TypeLegend = () => (
60
- <Help text={
61
- <span className="legend">
62
- {Object.entries(TYPE_MARK).map(([kind, m]) => (
63
- <span key={kind} className="lrow"><i className={`ty ${m.hue}`}>{m.glyph}</i>{m.word}</span>
64
- ))}
65
- </span>
66
- } />
67
- )
68
-
69
- const TreeTabs = observer(({ app }: { app: RepoSt }) => (
70
- <span className="seg">
71
- {(['path', 'type'] as const).map((t) => (
72
- <button key={t} className={app.tree.tab === t ? 'on' : ''} onClick={() => app.tree.setTab(t)}>by {t}</button>
73
- ))}
74
- </span>
75
- ))
76
-
77
- // segmented severity filter — all · needs (yellow+red) · red, the address
78
- // rides at /?f=… so a filtered view is shareable
79
- // the shortcut letter rides INSIDE its own button, so the binding is
80
- // discoverable without a legend
81
- const FILTER_KEY = { all: 'a', needs: 'n', red: 'r' } as const
82
- const SegFilter = observer(({ app }: { app: RepoSt }) => (
83
- <span className="seg">
84
- {(['all', 'needs', 'red'] as const).map((f) => (
85
- <button key={f} className={app.filter === f ? 'on' : ''} onClick={() => app.setFilter(f)}>
86
- {f}<span className="sk">{FILTER_KEY[f]}</span>
87
- </button>
88
- ))}
89
- </span>
90
- ))
91
-
92
- const Sidebar = observer(({ app }: { app: RepoSt }) => (
93
- <aside>
94
- <div className="shead">
95
- resources
96
- <SegFilter app={app} />
97
- </div>
98
- <div className="ssearch">
99
- <input ref={(el) => { app.searchEl = el }} placeholder="filter… (/ to focus, ⌘K to jump)" value={app.query}
100
- onChange={(e) => app.setQuery(e.target.value)}
101
- onKeyDown={(e) => { if (e.key === 'Escape') app.setQuery('') }} />
102
- </div>
103
- <div className="stabs">
104
- <TreeTabs app={app} />
105
- <span className="sp" />
106
- <TypeLegend />
107
- <span className="cnt">{app.tree.visible.length}</span>
108
- </div>
109
- {/* its line is held while the repo is read, so the tree never drops under it */}
110
- {app.tree.whenLine === '' && app.cols.loaded ? null : <div className="swhen">{app.tree.whenLine === '' ? '\u00a0' : app.tree.whenLine}</div>}
111
- {app.media.dead != null ? <div className="tempty"><span className="warnline">media API down: {app.media.dead}</span></div> : null}
112
- <Tree app={app} />
113
- </aside>
114
- ))
115
-
116
- // ── the one picker, cmd+k: home and planned, the shown repo's targets, then
117
- // every other repo ──────────────────────────────────────────────────────
118
- const Palette = observer(({ app }: { app: HomeSt }) => {
119
- const p = app.palette
120
- if (!p.open) return null
121
- return (
122
- <div className="palov" onClick={() => p.close()}>
123
- <div className="pal" onClick={(e) => e.stopPropagation()}>
124
- <input autoFocus placeholder="jump to…" value={p.query}
125
- onChange={(e) => p.setQuery(e.target.value)}
126
- onKeyDown={(e) => {
127
- if (e.key === 'ArrowDown') { e.preventDefault(); p.move(1) }
128
- else if (e.key === 'ArrowUp') { e.preventDefault(); p.move(-1) }
129
- else if (e.key === 'Enter') p.run()
130
- else if (e.key === 'Escape') p.close()
131
- }} />
132
- <div className="prows">
133
- {p.results.map((jp, n) => (
134
- <div key={jp.id} className={`prow${n === p.idx ? ' on' : ''}`}
135
- onMouseEnter={() => p.setIdx(n)}
136
- onClick={() => p.run(jp)}>
137
- <span className="plabel">{jp.label}</span>
138
- <span className="pchip">{jp.section}</span>
139
- </div>
140
- ))}
141
- {p.results.length === 0 ? <div className="prow none">no match</div> : null}
142
- </div>
143
- </div>
144
- </div>
145
- )
146
- })
147
-
148
49
  // remark box on the ASSET view too: remarks land in .shipkit/assets/notes.md
149
50
  // and come back as `remarks` (exchange both ways, media included)
150
51
  const AssetRemarks = observer(({ app }: { app: RepoSt }) => {
@@ -184,8 +85,14 @@ const Main = observer(({ app }: { app: RepoSt }) => {
184
85
  : <div className="stagewrap"><ColCard app={app} col={app.sel.col} id={app.sel.id} /></div>}
185
86
  </main>
186
87
  )
187
- if (app.sel?.t === 'col') return <main>{errs}<ColGrid app={app} id={app.sel.id} /></main>
88
+ if (app.sel?.t === 'col') return (
89
+ <main>
90
+ {errs}
91
+ {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} />}
92
+ </main>
93
+ )
188
94
  if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
95
+ if (app.sel?.t === 'admin') return <main>{errs}<div className="stagewrap"><AdminPage app={app} /></div></main>
189
96
  if (app.sel?.t === 'status') return <main>{errs}<div className="stagewrap"><StatusCard app={app} /></div></main>
190
97
  if (app.sel?.t === 'cat') return <main>{errs}<CatGrid app={app} /></main>
191
98
  if (app.sel?.t === 'asset') return (
@@ -207,10 +114,10 @@ const Main = observer(({ app }: { app: RepoSt }) => {
207
114
  return (
208
115
  <main>
209
116
  {errs}
210
- <div className="stagewrap"><div className="ph">
211
- {allClear ? (app.filter === 'red' ? 'no reds — nothing is blocked ✨' : 'all clear — nothing needs you ✨')
212
- : 'Pick a row on the left.'}
213
- </div></div>
117
+ <div className="stagewrap">
118
+ {allClear ? <div className="ph">{app.filter === 'red' ? 'no reds — nothing is blocked ✨' : 'all clear — nothing needs you ✨'}</div>
119
+ : <ReadmeView app={app} />}
120
+ </div>
214
121
  </main>
215
122
  )
216
123
  })
@@ -254,16 +161,6 @@ const Body = observer(({ app }: { app: RepoSt }) => {
254
161
  )
255
162
  })
256
163
 
257
- /** the repo's hub has not answered on the home: its reason and its fix show
258
- * instead of the page. A screen this browser kept shows meanwhile, until
259
- * the machine says the hub is not up */
260
- const notUp = (app: RepoSt): boolean => {
261
- const m = app.home.machine
262
- if (m == null || app.started) return false
263
- if (!app.cols.cached) return true
264
- return m.fresh && m.worker(app.key)?.status !== 'up'
265
- }
266
-
267
164
  /** one repo's page. Every repo visited keeps its region mounted, hidden while
268
165
  * another one shows, so its scroll, its frames and its component state are
269
166
  * there when it comes back; a hidden repo polls nothing, so it repaints
@@ -274,8 +171,8 @@ const RepoView = observer(({ app }: { app: RepoSt }) => {
274
171
  <div className="repo" hidden={!on}>
275
172
  {notUp(app) ? <NotUp app={app.home} /> : (
276
173
  <>
277
- <HeaderBar app={app} />
278
- <LiveStrip app={app} />
174
+ {/* on the home the strip carries the repo's bar */}
175
+ {app.home.machine == null ? <HeaderBar app={app} /> : null}
279
176
  <Body app={app} />
280
177
  {on ? <ComfyOverlay app={app} /> : null}
281
178
  </>
@@ -291,9 +188,10 @@ function onKey(e: KeyboardEvent): void {
291
188
  if (pk != null) {
292
189
  e.preventDefault()
293
190
  if (pk.t === 'picker') home.palette.toggle()
294
- else if (pk.t === 'home' || pk.t === 'planned') {
191
+ else if (pk.t === 'dock') home.dock.cycle()
192
+ else if (pk.t === 'home' || pk.t === 'planned' || pk.t === 'activity') {
295
193
  home.palette.close()
296
- if (home.machine != null) home.navigate(pk.t === 'home' ? '/' : '/planned')
194
+ if (home.machine != null) home.navigate(PLACE_PATH[pk.t === 'home' ? 'overview' : pk.t])
297
195
  } else {
298
196
  const p = home.projects.list[pk.n - 1]
299
197
  home.palette.close()
@@ -312,20 +210,25 @@ function onKey(e: KeyboardEvent): void {
312
210
  e.preventDefault()
313
211
  return
314
212
  }
315
- if (e.key === '/') {
316
- e.preventDefault()
317
- app.searchEl?.focus()
213
+ const sk = sectionKey(e)
214
+ if (sk != null) {
215
+ const s = repoSections(app).find((x) => x.id === sk)
216
+ if (s != null) {
217
+ e.preventDefault()
218
+ s.go()
219
+ }
318
220
  return
319
221
  }
320
- if (e.metaKey || e.ctrlKey || e.altKey) return
321
- if (e.key === LIVE_KEY) {
222
+ if (isSearchKey(e)) {
322
223
  e.preventDefault()
323
- app.live.toggle()
224
+ app.searchEl?.focus()
324
225
  return
325
226
  }
326
- if (e.key === FOLLOW_KEY && !app.live.chart.follow) {
227
+ if (e.metaKey || e.ctrlKey || e.altKey) return
228
+ if (e.key === FOLLOW_KEY && (!app.live.chart.follow || !home.dock.chart.follow)) {
327
229
  e.preventDefault()
328
230
  app.live.chart.attach()
231
+ home.dock.chart.attach()
329
232
  return
330
233
  }
331
234
  const f = FILTER_BY_KEY[e.key.toLowerCase()]
@@ -335,33 +238,72 @@ function onKey(e: KeyboardEvent): void {
335
238
  }
336
239
  }
337
240
 
241
+ /** a drag of a separator ended: the height of the panel below it, in px */
242
+ const belowPx = (el: HTMLElement | null, layout: Layout, id: string): number | null => {
243
+ const pct = layout[id]
244
+ const h = el?.getBoundingClientRect().height
245
+ return pct == null || h == null || h === 0 ? null : (pct / 100) * h
246
+ }
247
+
338
248
  const App = observer(() => {
339
249
  useEffect(() => {
340
250
  document.addEventListener('keydown', onKey)
341
251
  return () => document.removeEventListener('keydown', onKey)
342
252
  }, [])
343
253
  const l = home.layout
254
+ const d = home.dock
344
255
  const el = useRef<HTMLDivElement | null>(null)
256
+ const vel = useRef<HTMLDivElement | null>(null)
257
+ const dockRef = usePanelRef()
258
+ // the dock's size moves the boundary, and only the boundary: its first
259
+ // height is the panel's default, read before the first render. After the
260
+ // render, so the panel already holds the bounds of its new size, and
261
+ // eased (dock-anim.ts), so the boundary slides rather than jumps
262
+ const px = d.px
263
+ useLayoutEffect(() => {
264
+ const p = dockRef.current
265
+ if (p != null && Math.abs(p.getSize().inPixels - px) >= 1) animateDock(vel.current, () => p.resize(px))
266
+ }, [px, dockRef])
345
267
  return (
346
- // two panels: the project list, its width dragged by the separator
347
- // and kept per browser, then the page. The list and the strip stay
348
- // mounted across pages; only the region under the strip changes
268
+ // the page over the dock: the project list, its width dragged by the
269
+ // separator and kept per browser, then the page; under both, the
270
+ // dock, on every page. The list, the strip and the dock stay mounted
271
+ // across pages; only the region under the strip changes
349
272
  <>
350
- <Group className="app" orientation="horizontal" elementRef={el}
273
+ <Group className="appv" orientation="vertical" elementRef={vel}
351
274
  onLayoutChanged={(layout, meta) => {
352
- if (!meta.isUserInteraction) return
353
- const px = leftPx(el.current, layout, 'projects')
354
- if (px != null) l.setListWidth(px)
275
+ if (!meta.isUserInteraction || d.size !== 'open') return
276
+ const px = belowPx(vel.current, layout, 'dock')
277
+ if (px == null) return
278
+ // a drag past a bound lands on it: the panel's own bounds
279
+ // stay 0 and the max, so the key can take it to any size
280
+ d.setHeight(px)
281
+ if (Math.abs(px - d.height) >= 1) dockRef.current?.resize(d.height)
355
282
  }}>
356
- <Panel id="projects" defaultSize={l.listWidth} minSize={LIST.min} maxSize={LIST.max} groupResizeBehavior="preserve-pixel-size" className="pjpanel" style={PANEL}>
357
- <ProjectList app={home} />
283
+ <Panel id="top" minSize={200} className="apptop" style={PANEL}>
284
+ <Group className="app" orientation="horizontal" elementRef={el}
285
+ onLayoutChanged={(layout, meta) => {
286
+ if (!meta.isUserInteraction) return
287
+ const px = leftPx(el.current, layout, 'projects')
288
+ if (px != null) l.setListWidth(px)
289
+ }}>
290
+ <Panel id="projects" defaultSize={l.listWidth} minSize={LIST.min} maxSize={LIST.max} groupResizeBehavior="preserve-pixel-size" className="pjpanel" style={PANEL}>
291
+ <ProjectList app={home} />
292
+ </Panel>
293
+ <Separator className="rsep" />
294
+ <Panel id="page" minSize={320} className="appcol" style={PANEL}>
295
+ {home.machine == null ? null : <HomeStrip app={home} />}
296
+ {home.page === 'overview' ? <Overview app={home} /> : null}
297
+ {home.page === 'planned' ? <PlannedView app={home} /> : null}
298
+ {home.page === 'activity' ? <ActivityView app={home} /> : null}
299
+ {home.page === 'archived' ? <ArchivedView app={home} /> : null}
300
+ {home.visited.map((r) => <RepoView key={r.key} app={r} />)}
301
+ </Panel>
302
+ </Group>
358
303
  </Panel>
359
- <Separator className="rsep" />
360
- <Panel id="page" minSize={320} className="appcol" style={PANEL}>
361
- {home.machine == null ? null : <HomeStrip app={home} />}
362
- {home.page === 'overview' ? <Overview app={home} /> : null}
363
- {home.page === 'planned' ? <PlannedView app={home} /> : null}
364
- {home.visited.map((r) => <RepoView key={r.key} app={r} />)}
304
+ <Separator className={`rsep hsep${d.size === 'open' ? '' : ' fixed'}`} disabled={d.size !== 'open'} />
305
+ <Panel id="dock" panelRef={dockRef} defaultSize={d.px} minSize={0} maxSize={DOCK.max} groupResizeBehavior="preserve-pixel-size" className="pdock" style={PANEL}>
306
+ <Dock app={home} />
365
307
  </Panel>
366
308
  </Group>
367
309
  <Palette app={home} />
@@ -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,6 +73,7 @@ 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 { Line } from '../../core/wire.ts'
76
77
  import type { CheckFrom } from '../../check.ts'
77
78
  export type { CheckFrom, WhenAt, WhenSource }
78
79
 
@@ -154,7 +155,7 @@ export type CheckEvent = {
154
155
  replayed: boolean
155
156
  blocking: boolean | null
156
157
  advisory: boolean
157
- lines: { kind: string; text: string }[]
158
+ lines: Line[]
158
159
  }
159
160
  /** the colour of the tree label: red broken · warn taste pending · ok
160
161
  * passing on a check that ran · idle the neutral default · quiet archived or out of play */
@@ -186,6 +187,8 @@ export type Resource = {
186
187
  check?: CheckFacts
187
188
  /** test files only: where it runs and what it last cost */
188
189
  test?: TestFacts
190
+ /** asks only: kind, dates, what it blocks, its probe */
191
+ ask?: AskFacts
189
192
  }
190
193
  export type ColMeta = { id: string; title: string; media: string }
191
194
  export type Col = ColMeta & { instances: Resource[] }
@@ -4,7 +4,11 @@
4
4
  action, and red/orange/green reserved for STATE. No rule outside this
5
5
  block writes a colour, and tests/hub-css-invariants.test.ts fails one
6
6
  that does. */
7
- :root{--bg:#0d0e10;--panel:#141619;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
7
+ :root{--bg:#0d0e10;--panel:#141619;
8
+ /* the surfaces of the regions, each a small step from the page so the rail,
9
+ the header and the dock read apart at a glance (no light theme: the page
10
+ is dark only, and colorDark is what it draws) */
11
+ --bg-side:#131519;--bg-top:#171a1f;--bg-dock:#10151b;--panel2:#1b1e22;--line:#282c31;--ink:#e2e4e7;--dim:#8d9299;
8
12
  --accent:#6b8fb3;--sel:#1a2430;--chk1:#16181b;--chk2:#121417;
9
13
  /* state, and nothing else: red broken · orange taste pending · green measured passing */
10
14
  --red:#d2605c;--warn:#cf9a4e;--ok:#6faa86;--blue:#6f9bd1;
@@ -16,18 +20,26 @@
16
20
  --k1:#8ca8c7;--k2:#a892c9;--k3:#6fb0b8;--k4:#c792b4;--k5:#9198a1;--k6:#8f95cf;
17
21
  --wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
18
22
  --errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
23
+ /* a running thing's wash, the green twin of errbg and errline */
24
+ --okbg:#15251c;--okline:#2f5a40;
19
25
  /* the ONE spacing scale: every padding, margin and gap in this sheet is a
20
26
  value from it or 0, and tests/hub-css-invariants.test.ts fails the rest */
21
27
  --s0:2px;--s1:4px;--s2:8px;--s3:12px;--s4:16px}
22
28
  *{box-sizing:border-box}html,body,#root{height:100%}
23
29
  body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans-serif;overflow:hidden}
24
- /* the page is two panels (hub-app.tsx): the project list, then .appcol,
25
- which stacks the strip, the header, the live strip and the body */
26
- .app{height:100vh}
30
+ /* a link with no class of its own: the browser default is dark blue on this dark page */
31
+ a,a:visited{color:var(--accent)}
32
+ /* the page is the window's height, the dock under it (hub-app.tsx): over
33
+ the dock two panels, the project list, then .appcol, which stacks the
34
+ strip, the header and the body */
35
+ .appv{height:100vh}
36
+ .app{height:100%}
27
37
  /* a panel's class lands on the div inside the panel (react-resizable-panels) */
28
38
  .pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
39
+ .apptop,.pdock{height:100%;min-height:0}
29
40
  .pjpanel{display:flex}
30
- .appcol{display:flex;flex-direction:column}
41
+ /* the box the machine toasts float in (home-strip.css .htoasts) */
42
+ .appcol{position:relative;display:flex;flex-direction:column}
31
43
  /* one region per repo visited, the shown one alone displayed */
32
44
  .repo{flex:1;min-height:0;display:flex;flex-direction:column}
33
45
  .repo[hidden]{display:none}
@@ -37,7 +49,11 @@ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans
37
49
  .pmain{display:flex;flex-direction:column}
38
50
  .pmain > main{flex:1}
39
51
 
40
- /* ── the header: repo name, the core pages, the two runs and the run state ── */
52
+ /* ── a repo's bar: on the home the strip (home-strip.css .hstrip), on a hub
53
+ of its own the header (.hdr). Its name, what runs now and the two runs,
54
+ then its processes, its trouble and its editor icons. One line at any
55
+ width: the container queries shrink the labels to their icons, then the
56
+ run state to its spinner ── */
41
57
  /* the repo's processes (serve.processes): a dot, its id, a link once up */
42
58
  .hprocs{display:inline-flex;gap:var(--s1);align-items:center}
43
59
  .hproc{display:inline-flex;align-items:center;gap:var(--s0)}
@@ -45,45 +61,51 @@ body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 system-ui,sans
45
61
  .hproc a{color:var(--accent);text-decoration:none;font-size:12px}
46
62
  .pdot{display:inline-block;width:7px;height:7px;border-radius:4px;margin-right:var(--s1);background:var(--line)}
47
63
  .pdot.up{background:var(--ok)}.pdot.starting,.pdot.stopping{background:var(--warn)}.pdot.crashed{background:var(--red)}
48
- .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(--panel)}
64
+ .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)}
49
65
  .hdr .sp{flex:1}
50
66
  .hname{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);font-weight:600;margin-right:var(--s2);white-space:nowrap}
51
- .hnav{display:flex;gap:var(--s1);overflow:hidden;align-items:center}
52
- .hnav button{border-color:transparent;color:var(--dim);border-radius:0;border-bottom:1px solid transparent;white-space:nowrap}
53
- .hnav button:hover{color:var(--ink);border-color:transparent;border-bottom-color:var(--line)}
54
- .hnav button.on{color:var(--accent);border-bottom-color:var(--accent)}
67
+ .hdr,.hstrip{container-type:inline-size;white-space:nowrap}
68
+ .hrun{display:inline-flex;align-items:center;gap:var(--s1);flex:none;margin-left:var(--s2)}
69
+ .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}
70
+ .hbtn .hico{width:13px;height:13px}
71
+ /* the editor icons: bare, dim, lit on hover, as the activity page's Fork icon */
72
+ .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)}
73
+ .hicon:hover{color:var(--accent)}
74
+ .hicon .hico{width:15px;height:15px}
55
75
  /* the run state holds its width whatever it says, so a run never moves the buttons beside it */
56
76
  .hstatus{max-width:420px;flex:0 1 auto;font-size:11.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
57
77
  .hstatus.red{color:var(--red)}
58
78
  .hstatus.warn{color:var(--warn)}
59
- .hstate{display:inline-flex;gap:var(--s1);align-items:center;width:260px;min-width:260px;flex:none;justify-content:flex-start;font-size:11.5px;color:var(--dim);overflow:hidden}
79
+ /* its words sit against the run buttons, cut with an ellipsis */
80
+ .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}
60
81
  .hstate.on{color:var(--ink)}
61
- .hstate .htext{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
62
- .hstate .spin.idle,.hnav .spin.idle{visibility:hidden;animation:none}
82
+ .hstate .htext{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
83
+ .hstate .spin.idle{visibility:hidden;animation:none}
84
+ /* after .hstate, so a narrow bar wins over its width */
85
+ @container (max-width:600px){.hbtn .lbl{display:none}.hbtn{padding:0 var(--s1)}}
86
+ @container (max-width:520px){.hstate{width:120px;min-width:120px}}
87
+ @container (max-width:420px){.hstate .htext{display:none}.hstate{width:16px;min-width:16px}}
63
88
 
64
- /* ── the live strip: the one run chart. Folded, one thin row of ONE height
65
- whatever runs; unfolded, the chart at the height its lanes hold, set
66
- inline from a held value that grows at once and shrinks only after a
67
- while, so it never jumps every second. Its (?) and the chart tooltip float
68
- in a portal (floating.tsx), so no box of the strip clips them ── */
69
- .live{display:flex;flex-direction:column;height:30px;flex:none;border-bottom:1px solid var(--line);background:var(--bg);font-size:11px;color:var(--dim)}
89
+ /* a row of a (?) legend: one glyph or word, then its meaning */
70
90
  .lrow{display:flex;gap:var(--s2);align-items:center;height:29px;flex:none;padding:0 var(--s3)}
71
- .lrow .sp{flex:1}
72
- .lfold{display:inline-flex;gap:var(--s1);align-items:center;border-color:transparent;color:var(--dim);padding:var(--s0) var(--s1)}
73
- .lfold .sk{font-size:9px;opacity:.55}
74
- .lwho{width:180px;flex:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ink)}
75
- .live.idle .lwho{color:var(--dim)}
76
- .lwho i{font-style:normal;color:var(--dim);margin-left:var(--s1)}
77
- .lwho b{font-weight:500;color:var(--accent);margin-left:var(--s1)}
78
- .lchart{flex:1;min-width:0;height:21px;border:1px solid var(--line);border-radius:3px;overflow:hidden}
79
- .lopen{flex:1;min-height:0;overflow-y:auto;padding:0 var(--s3);scrollbar-width:thin;scrollbar-color:var(--line) transparent}
80
- .lnote,.lmore{flex:none;color:var(--warn);white-space:nowrap}
81
- .lbusy{flex:none;white-space:nowrap}
82
91
 
83
- /* ── sidebar ── */
84
- aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto;min-height:0}
85
- .shead{position:sticky;top:0;background:var(--panel);padding:var(--s3);border-bottom:1px solid var(--line);
86
- font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--dim);z-index:2;display:flex;justify-content:space-between;align-items:center}
92
+ /* ── the left pane: the sections, the filter, then the tree, the one part
93
+ that scrolls ── */
94
+ aside{border-right:1px solid var(--line);background:var(--panel);min-height:0;display:flex;flex-direction:column;overflow:hidden}
95
+ aside > *{flex:none}
96
+ aside > .tscroll{flex:1;min-height:0;overflow-y:auto}
97
+ /* a repo's sections: the project list's lines (project-list.css .pjpl), a
98
+ little shorter, the current one lit and marked in the accent */
99
+ .rsecs{display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
100
+ .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}
101
+ .rsec:hover{background:var(--panel2);color:var(--ink);border-color:transparent}
102
+ .rsec.cur{background:var(--sel);color:var(--ink);box-shadow:inset 3px 0 0 var(--accent);font-weight:600}
103
+ .rsec .hico{flex:none;width:16px;height:16px}
104
+ .rsecl{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
105
+ .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)}
106
+ .rsecn.nred{background:var(--red)}
107
+ .rsecn.nwarn{background:var(--warn)}
108
+ .rsec:hover .pjkey,.rsec.cur .pjkey{opacity:1}
87
109
  /* segmented severity filter — all · needs · red. A thin border and an
88
110
  underline on the live one: no fill, no pill */
89
111
  .seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden;flex:none}
@@ -94,10 +116,16 @@ aside{border-right:1px solid var(--line);background:var(--panel);overflow-y:auto
94
116
  .seg button.on{color:var(--accent);border-bottom-color:var(--accent)}
95
117
  .seg button .sk{margin-left:var(--s1);font-size:9px;opacity:.55}
96
118
  .swhen{padding:0 var(--s3) var(--s2);font-size:11px;color:var(--dim)}
97
- .ssearch{padding:var(--s2) var(--s3)}
119
+ /* a narrow pane puts the severity filter under the input, never over it */
120
+ .ssearch{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3)}
121
+ .sfield{position:relative;flex:1 1 96px;min-width:0;display:flex}
98
122
  .ssearch input{width:100%;background:var(--bg);border:1px solid var(--line);border-radius:4px;color:var(--ink);
99
- padding:var(--s1) var(--s2);font-size:12.5px;outline:none}
123
+ padding:var(--s0) var(--s4) var(--s0) var(--s2);font-size:12px;outline:none}
124
+ .ssearch input::placeholder{color:var(--dim);opacity:.6;font-size:11.5px}
100
125
  .ssearch input:focus{border-color:var(--accent)}
126
+ /* its key, the way the project list prints its keys (.pjkey) */
127
+ .skey{position:absolute;right:var(--s2);top:50%;transform:translateY(-50%);pointer-events:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
128
+ .sfield input:focus + .skey{opacity:0}
101
129
  /* ── the resource tree: ONE row shape, a fixed grid. The indent rides in
102
130
  the FIRST column and the label eats the slack, so every right side slot
103
131
  lines up down the whole column whatever the depth. Colour is on the
@@ -390,7 +418,6 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
390
418
  .tlb.bad,.tlb.red{fill:var(--red)}
391
419
  .tlb.rep{fill:var(--line)}
392
420
  .tlb.run{fill:var(--accent);animation:pulse 1.6s ease-in-out infinite}
393
- .tlb.repo{fill:var(--k6)}
394
421
  .tlband.run{stroke:var(--accent)}
395
422
  .tlconc{fill:var(--hair)}
396
423
  .tlconc.over{fill:var(--warn)}
@@ -439,6 +466,8 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
439
466
  per test check (the tests page), the failing files with their failure line */
440
467
  .rtime{grid-template-columns:minmax(0,1fr) 88px 72px 128px 72px 40px 88px}
441
468
  .rcheckt{grid-template-columns:152px 88px 48px 56px minmax(0,1fr) 72px}
469
+ /* every test file: file, check, duration, status, age */
470
+ .rall{grid-template-columns:minmax(0,1fr) 152px 88px 52px 72px}
442
471
  .rhead .tleft{text-align:left}
443
472
  .tshare{display:grid;grid-template-columns:minmax(0,1fr) 36px;gap:var(--s1);align-items:center}
444
473
  .ttrack{display:block;height:6px;border-radius:1px;overflow:hidden;background:var(--panel2)}
@@ -600,6 +629,27 @@ a.tok{color:var(--accent)}
600
629
  .cghead{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
601
630
  display:flex;gap:var(--s2);align-items:baseline;margin:0 0 var(--s3)}
602
631
  .cghead .cnt{font-size:11px;color:var(--dim);letter-spacing:0;text-transform:none}
632
+ /* the checks and asks pages: one dense table each (col-tables-view.tsx), on
633
+ the activity table (.actable), a group header per state that folds */
634
+ .ctpage{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4);font-size:12.5px;container-type:inline-size}
635
+ .cttable{font-variant-numeric:tabular-nums}
636
+ .cttable td{padding:var(--s0) var(--s2);vertical-align:baseline}
637
+ .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}
638
+ .ctgrp:hover td{color:var(--ink)}
639
+ .ctgrp.g-fail td{color:var(--red)}
640
+ .ctgrp.g-warn td{color:var(--warn)}
641
+ .ctcaret{display:inline-block;width:14px}
642
+ .ctrow{cursor:pointer}
643
+ .ctrow:hover{background:var(--panel2)}
644
+ .ctdot{font-size:10px}
645
+ .ctid,.ctkind{font:11.5px ui-monospace,Menlo,monospace}
646
+ .ctfind{font:11.5px ui-monospace,Menlo,monospace;color:var(--dim)}
647
+ .ctdur{text-align:right;font:11.5px ui-monospace,Menlo,monospace}
648
+ .ctbudget,.ctplan,.ctblocks{color:var(--dim)}
649
+ .ctprobe{font-size:11.5px}
650
+ .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}
651
+ .cttable .c-kind{width:92px}.cttable .c-blocks{width:32%}.cttable .c-probe{width:84px}
652
+ @container (max-width:760px){.cttable .c-title{width:0}.cttable .cttitle{display:none}.cttable .c-plan{width:0}.cttable .ctplan{display:none}}
603
653
  .cgcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--s3)}
604
654
  .cgcard{background:var(--panel);border:1px solid var(--line);border-radius:5px;padding:var(--s2) var(--s3);cursor:pointer;min-width:0}
605
655
  .cgcard:hover{border-color:var(--accent)}
@@ -666,7 +716,7 @@ a.mint{color:var(--accent);font-size:12px;text-decoration:none}
666
716
  a.mint:hover{text-decoration:underline}
667
717
  .colcard h3 .stchip,.colcard h3 a.mint{margin-left:var(--s2)}
668
718
 
669
- /* ── the cmd+k picker ── */
719
+ /* ── the cmd+j picker ── */
670
720
  .palov{position:fixed;inset:0;background:var(--scrim);z-index:100;display:flex;justify-content:center;align-items:flex-start;
671
721
  padding-top:14vh}
672
722
  .pal{width:min(560px,90vw);background:var(--panel);border:1px solid var(--line);border-radius:3px;
@@ -678,6 +728,9 @@ a.mint:hover{text-decoration:underline}
678
728
  .prow.on{background:var(--panel2)}
679
729
  .prow.none{color:var(--dim);cursor:default}
680
730
  .prow .plabel{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
731
+ /* the entry's icon: a place's button icon, a repo's own (project-list.tsx Face) */
732
+ .prow .pface{flex:none;display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dim)}
733
+ .prow .pface img,.prow .pface .pgi{width:16px;height:16px;border-radius:4px;object-fit:contain}
681
734
  .prow .pchip{font-size:10px;color:var(--dim);background:var(--bg);border:1px solid var(--line);border-radius:5px;
682
735
  padding:var(--s0) var(--s1);text-transform:uppercase;letter-spacing:.08em;flex:none}
683
736
 
@@ -689,3 +742,12 @@ a.mint:hover{text-decoration:underline}
689
742
  pre{background:var(--panel);border:1px solid var(--line);border-radius:3px;padding:var(--s4);overflow-x:auto;
690
743
  font:12.5px/1.55 ui-monospace,Menlo,monospace;white-space:pre-wrap}
691
744
  .ok{color:var(--ok)}.warn{color:var(--warn)}.fail{color:var(--red)}.dim{color:var(--dim)}
745
+
746
+ /* ── the admin page: one row per control, its sentence under its name ── */
747
+ .adrow{display:flex;align-items:center;gap:var(--s4);padding:var(--s3) 0;border-top:1px solid var(--line)}
748
+ .adtext{flex:1;min-width:0;display:flex;flex-direction:column;gap:var(--s0);font-size:13px}
749
+ .adtext .dim{font-size:12px}
750
+ .adctl{flex:none;display:flex;align-items:center}
751
+ .adflag{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px;color:var(--dim)}
752
+ .adflag input{width:auto;margin:0}
753
+ .adconfirm{display:inline-flex;align-items:center;gap:var(--s2);font-size:12px}