rvlib-shipkit 1.1.1 → 1.3.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 (210) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +1 -1
  3. package/package.json +3 -1
  4. package/src/assets/run-assets.ts +3 -8
  5. package/src/check.ts +40 -2
  6. package/src/checks/SK001-package-json-shape.check.ts +14 -3
  7. package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
  8. package/src/checks/SK004-gitignore-policy.check.ts +11 -3
  9. package/src/checks/SK005-rvlib-versions.check.ts +13 -1
  10. package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
  11. package/src/checks/SK007-github-visibility.check.ts +9 -9
  12. package/src/checks/SK008-reviewer-agent.check.ts +14 -8
  13. package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
  14. package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
  15. package/src/checks/SK015-nested-repo.check.ts +5 -1
  16. package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
  17. package/src/checks/SK022-machine-runs-the-pin.check.ts +47 -0
  18. package/src/checks/SK024-identity-colors.check.ts +54 -0
  19. package/src/checks/SK025-release-drift.check.ts +58 -0
  20. package/src/checks/SK026-asks-autoclose.check.ts +44 -0
  21. package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
  22. package/src/checks/SK028-readme.check.ts +79 -0
  23. package/src/cli/cmd_Assets.ts +1 -1
  24. package/src/cli/cmd_Check.ts +26 -1
  25. package/src/cli/cmd_Ci.ts +23 -5
  26. package/src/cli/cmd_CrossRepoActivity.ts +116 -0
  27. package/src/cli/cmd_GlobalProcesses.ts +155 -0
  28. package/src/cli/cmd_Home.ts +170 -0
  29. package/src/cli/cmd_Keys.ts +17 -3
  30. package/src/cli/cmd_Overview.ts +98 -0
  31. package/src/cli/cmd_Planned.ts +117 -0
  32. package/src/cli.ts +30 -2
  33. package/src/collections/checks.ts +3 -2
  34. package/src/collections/providers.ts +1 -1
  35. package/src/collections/test-time.ts +10 -1
  36. package/src/comfy/comfy-panel.ts +1 -16
  37. package/src/core/agent-spawn.ts +46 -0
  38. package/src/core/archived.ts +82 -0
  39. package/src/core/attach.ts +14 -9
  40. package/src/core/canonical-scripts.ts +12 -3
  41. package/src/core/check-file-names.ts +8 -0
  42. package/src/core/check-runner.ts +80 -30
  43. package/src/core/discover-checks.ts +38 -18
  44. package/src/core/file-stamp.ts +20 -0
  45. package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
  46. package/src/core/identity-color.ts +203 -0
  47. package/src/core/identity.ts +91 -0
  48. package/src/core/job-follow.ts +102 -0
  49. package/src/core/job-run.ts +27 -0
  50. package/src/core/jobs.ts +87 -27
  51. package/src/core/legacy-shapes.ts +1 -14
  52. package/src/core/machine-db.ts +83 -0
  53. package/src/core/one-lib.ts +85 -0
  54. package/src/core/port-holders.ts +43 -0
  55. package/src/core/repo-facts.ts +14 -0
  56. package/src/core/retention.ts +222 -0
  57. package/src/core/retired-checks.ts +7 -0
  58. package/src/core/run-check.ts +12 -4
  59. package/src/core/run-ci.ts +46 -12
  60. package/src/core/run-fix.ts +7 -2
  61. package/src/core/run-fixers.ts +10 -6
  62. package/src/core/slots.ts +5 -0
  63. package/src/core/statement-end.ts +17 -0
  64. package/src/core/wire.ts +11 -0
  65. package/src/fleet/scan-fleet.ts +2 -1
  66. package/src/home/activity-shape.ts +135 -0
  67. package/src/home/activity.ts +249 -0
  68. package/src/home/attach.ts +95 -0
  69. package/src/home/autostart.ts +108 -0
  70. package/src/home/global-units.ts +87 -0
  71. package/src/home/gui-app/ShipkitGui.swift +159 -0
  72. package/src/home/gui-app.ts +99 -0
  73. package/src/home/handoff.ts +51 -0
  74. package/src/home/home-launchd.ts +104 -0
  75. package/src/home/home-paths.ts +88 -0
  76. package/src/home/home-routes.ts +235 -0
  77. package/src/home/home-start.ts +105 -0
  78. package/src/home/home-state.ts +43 -0
  79. package/src/home/home-words.ts +12 -0
  80. package/src/home/hubs.ts +286 -0
  81. package/src/home/lib-watch.ts +139 -0
  82. package/src/home/machine-stores.ts +115 -0
  83. package/src/home/open-app.ts +16 -0
  84. package/src/home/pm-config.ts +37 -0
  85. package/src/home/pm.ts +174 -0
  86. package/src/home/process-rows.ts +205 -0
  87. package/src/home/resources.ts +365 -0
  88. package/src/home/run-daemon.ts +137 -0
  89. package/src/home/run-pm.ts +150 -0
  90. package/src/home/run-unit-action.ts +25 -0
  91. package/src/home/run-web.ts +37 -0
  92. package/src/home/serve-daemon.ts +31 -0
  93. package/src/home/serve-web.ts +175 -0
  94. package/src/home/supervise.ts +361 -0
  95. package/src/home/via-hub.ts +142 -0
  96. package/src/home/widget-text.ts +119 -0
  97. package/src/home/widgets.ts +102 -0
  98. package/src/hooks/run-hooks.ts +20 -3
  99. package/src/ledger/check-stats.ts +19 -45
  100. package/src/ledger/db.ts +69 -17
  101. package/src/ledger/gate-checks.ts +22 -3
  102. package/src/ledger/git-scan.ts +31 -7
  103. package/src/ledger/house-runs.ts +43 -0
  104. package/src/ledger/ledger.ts +40 -14
  105. package/src/ledger/live-timeline.ts +4 -19
  106. package/src/ledger/run-log.ts +92 -28
  107. package/src/ledger/store.ts +245 -103
  108. package/src/ledger/world-cache.ts +64 -15
  109. package/src/plan/cadence.ts +253 -0
  110. package/src/plan/catch-up.ts +37 -0
  111. package/src/plan/plan-timeline.ts +99 -0
  112. package/src/plan/planner.ts +428 -0
  113. package/src/plan/repo-source.ts +212 -0
  114. package/src/problems/problem-line.ts +1 -1
  115. package/src/release/run-release.ts +9 -2
  116. package/src/release/unit-resolve.ts +8 -12
  117. package/src/repo-config.ts +61 -4
  118. package/src/serve/house-runs.ts +53 -0
  119. package/src/serve/hub-app/activity-st.ts +168 -0
  120. package/src/serve/hub-app/activity-view.tsx +218 -0
  121. package/src/serve/hub-app/activity.css +78 -0
  122. package/src/serve/hub-app/admin-svc.ts +41 -0
  123. package/src/serve/hub-app/admin-view.tsx +42 -0
  124. package/src/serve/hub-app/chart-view.ts +12 -5
  125. package/src/serve/hub-app/check-view.tsx +9 -9
  126. package/src/serve/hub-app/check-words.ts +3 -1
  127. package/src/serve/hub-app/checks-svc.ts +12 -8
  128. package/src/serve/hub-app/col-card.tsx +4 -4
  129. package/src/serve/hub-app/cols-svc.ts +92 -13
  130. package/src/serve/hub-app/comfy-svc.ts +12 -6
  131. package/src/serve/hub-app/comfy-view.tsx +3 -3
  132. package/src/serve/hub-app/controls.css +32 -0
  133. package/src/serve/hub-app/cycle-button.tsx +36 -0
  134. package/src/serve/hub-app/dock-anim.ts +25 -0
  135. package/src/serve/hub-app/dock-st.ts +329 -0
  136. package/src/serve/hub-app/dock-view.tsx +68 -0
  137. package/src/serve/hub-app/dock.css +35 -0
  138. package/src/serve/hub-app/floating.tsx +49 -0
  139. package/src/serve/hub-app/geometry-view.tsx +4 -4
  140. package/src/serve/hub-app/hash-route.ts +8 -2
  141. package/src/serve/hub-app/header-bar.tsx +102 -95
  142. package/src/serve/hub-app/health-svc.ts +3 -3
  143. package/src/serve/hub-app/help.tsx +10 -7
  144. package/src/serve/hub-app/home-st.ts +278 -0
  145. package/src/serve/hub-app/home-strip.css +113 -0
  146. package/src/serve/hub-app/home-strip.tsx +376 -0
  147. package/src/serve/hub-app/home.html +21 -0
  148. package/src/serve/hub-app/hub-app.tsx +221 -177
  149. package/src/serve/hub-app/hub-types.ts +0 -3
  150. package/src/serve/hub-app/hub.css +112 -52
  151. package/src/serve/hub-app/icons.tsx +34 -0
  152. package/src/serve/hub-app/index.html +7 -1
  153. package/src/serve/hub-app/keys.ts +60 -0
  154. package/src/serve/hub-app/layout-st.ts +55 -0
  155. package/src/serve/hub-app/live-svc.ts +35 -72
  156. package/src/serve/hub-app/machine-st.ts +333 -0
  157. package/src/serve/hub-app/md.ts +38 -5
  158. package/src/serve/hub-app/media-svc.ts +63 -44
  159. package/src/serve/hub-app/media-view.tsx +20 -20
  160. package/src/serve/hub-app/next-run.ts +24 -30
  161. package/src/serve/hub-app/palette-st.ts +92 -0
  162. package/src/serve/hub-app/palette-view.tsx +47 -0
  163. package/src/serve/hub-app/pill-row.tsx +65 -0
  164. package/src/serve/hub-app/planned-st.ts +107 -0
  165. package/src/serve/hub-app/planned-view.tsx +114 -0
  166. package/src/serve/hub-app/planned.css +42 -0
  167. package/src/serve/hub-app/processes-svc.ts +63 -0
  168. package/src/serve/hub-app/project-list.css +119 -0
  169. package/src/serve/hub-app/project-list.tsx +266 -0
  170. package/src/serve/hub-app/projects-svc.ts +253 -23
  171. package/src/serve/hub-app/prose-hits-st.ts +3 -3
  172. package/src/serve/hub-app/prose-hits-view.tsx +6 -6
  173. package/src/serve/hub-app/readme-svc.ts +38 -0
  174. package/src/serve/hub-app/readme-view.tsx +19 -0
  175. package/src/serve/hub-app/readme.css +11 -0
  176. package/src/serve/hub-app/repo-sections.tsx +61 -0
  177. package/src/serve/hub-app/repo-st.ts +340 -0
  178. package/src/serve/hub-app/runs-tabs.ts +1 -1
  179. package/src/serve/hub-app/screen-cache.ts +74 -0
  180. package/src/serve/hub-app/sidebar.tsx +84 -0
  181. package/src/serve/hub-app/test-view.tsx +4 -4
  182. package/src/serve/hub-app/tests-svc.ts +3 -3
  183. package/src/serve/hub-app/tests-view.tsx +41 -14
  184. package/src/serve/hub-app/timeline-chart.tsx +55 -39
  185. package/src/serve/hub-app/timeline-geometry.ts +40 -5
  186. package/src/serve/hub-app/timeline-svc.ts +7 -6
  187. package/src/serve/hub-app/timeline-view.tsx +33 -16
  188. package/src/serve/hub-app/tree-row.tsx +2 -2
  189. package/src/serve/hub-app/tree-svc.ts +2 -2
  190. package/src/serve/hub-app/wikis-view.tsx +10 -6
  191. package/src/serve/hub-runtime.ts +550 -0
  192. package/src/serve/other-repos.ts +8 -45
  193. package/src/serve/project-gutter-route.ts +121 -20
  194. package/src/serve/project-gutter.ts +33 -11
  195. package/src/serve/readme-route.ts +57 -0
  196. package/src/serve/repo-processes.ts +89 -0
  197. package/src/serve/run-serve.ts +110 -402
  198. package/src/serve/serve-hub.ts +155 -52
  199. package/src/serve/stale-restart.ts +5 -0
  200. package/src/serve/{gate-trigger.ts → tracked-watch.ts} +37 -65
  201. package/src/serve/warm-cell.ts +95 -0
  202. package/src/shipkit-files.ts +130 -0
  203. package/src/shipkit.ts +2 -1
  204. package/src/suite/kept-timings.ts +1 -1
  205. package/src/wiki/graph-positions.ts +1 -1
  206. package/src/checks/contracts.check.ts +0 -20
  207. package/src/serve/health-schedule.ts +0 -102
  208. package/src/serve/hub-app/hub-st.ts +0 -250
  209. package/src/serve/hub-app/project-gutter.css +0 -65
  210. package/src/serve/hub-app/project-gutter.tsx +0 -128
@@ -0,0 +1,42 @@
1
+ /* the planned page (planned-view.tsx): one quiet list, one font size. Each
2
+ row is a grid on the same three columns (when, project · check, mark), so
3
+ the columns line up without a header. The card on hover takes the pointer,
4
+ unlike .ftip, because it holds the run button. */
5
+ .planned{font-size:13px}
6
+ .plfind{display:block;width:240px;max-width:100%;box-sizing:border-box;margin-bottom:var(--s3);background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s1) var(--s2);font:inherit}
7
+ .plfind:focus{outline:none;border-color:var(--accent)}
8
+ .pllist{max-width:560px}
9
+ .plsec{margin-top:var(--s3);padding:var(--s1) var(--s2);color:var(--dim);text-transform:lowercase}
10
+ .plsec:first-child{margin-top:0}
11
+ .plr{display:grid;grid-template-columns:64px minmax(0,1fr) auto;align-items:center;column-gap:var(--s3);padding:var(--s0) var(--s2);border-radius:3px;white-space:nowrap}
12
+ .plr:hover{background:var(--panel)}
13
+ .plwhen{color:var(--dim);font-variant-numeric:tabular-nums}
14
+ .m-waiting .plwhen{color:var(--dim)}
15
+ .m-running .plwhen{color:var(--accent)}
16
+ .plwhat{min-width:0;overflow:hidden;text-overflow:ellipsis}
17
+ .plrepo,.plcheck{color:inherit;text-decoration:none}
18
+ .plrepo span{color:var(--pc)}
19
+ .plrepo:hover span,.plcheck:hover{text-decoration:underline}
20
+ .plface{width:14px;height:14px;margin-right:var(--s1);vertical-align:-2px;border-radius:3px;object-fit:contain}
21
+ .pldim{color:var(--dim)}
22
+ .plmark{display:flex;align-items:center;gap:var(--s1);color:var(--dim)}
23
+ .plmark .spin{width:9px;height:9px;border-width:1.5px}
24
+ .plsym{display:inline-block;width:7px;height:7px;border-radius:50%;flex:none;box-sizing:border-box}
25
+ .s-next{border:1px solid var(--dim)}
26
+ .s-due{background:var(--ink)}
27
+ .s-waiting{border:1px dashed var(--dim)}
28
+ .plwait{margin-top:var(--s3)}
29
+ .plwaitline{all:unset;cursor:pointer;color:var(--dim);font-size:12px}
30
+ .plwaitline:hover{color:var(--ink)}
31
+ .s-failed{width:auto;height:auto;border-radius:0;font-style:normal;color:var(--red)}
32
+ .s-failed::before{content:'✕'}
33
+ .m-failed .plmark{color:var(--red)}
34
+ .plcard{z-index:40;width:max-content;max-width:360px;background:var(--panel2);border:1px solid var(--line);border-radius:4px;box-shadow:0 2px 10px var(--shadow);padding:var(--s2) var(--s3);font-size:13px;color:var(--ink)}
35
+ .plcardin{display:flex;flex-direction:column;gap:var(--s1)}
36
+ .plct{margin-bottom:var(--s1)}
37
+ .plkv{display:grid;grid-template-columns:64px minmax(0,1fr);column-gap:var(--s2)}
38
+ .plkv > span:first-child{color:var(--dim)}
39
+ .plkv > span:last-child{white-space:pre-wrap;overflow-wrap:anywhere}
40
+ .plerr{color:var(--red)}
41
+ .l-ok{color:var(--ok)}.l-warn{color:var(--warn)}.l-red{color:var(--red)}.l-dim{color:var(--dim)}
42
+ .plgo{align-self:flex-start;margin-top:var(--s1);padding:0 var(--s2);font:inherit;line-height:22px}
@@ -0,0 +1,63 @@
1
+ // the repo's declared processes (serve.processes) as the hub supervises them,
2
+ // polled while its repo is the one shown. A repo that declares none never
3
+ // polls again after the first answer.
4
+ import { makeAutoObservable, observableRef, runInAction } from 'mobx'
5
+ import type { ProcessView } from '../repo-processes.ts'
6
+ import type { RepoSt } from './repo-st.ts'
7
+
8
+ export const PROCESSES_POLL_MS = 2_000
9
+
10
+ export class ProcessesSvc {
11
+ list: ProcessView[] = []
12
+ error = ''
13
+ private timer: ReturnType<typeof setTimeout> | null = null
14
+ /** bumped by stop, so a chain that left before it dies at its next await */
15
+ private gen = 0
16
+ private polling = false
17
+
18
+ constructor(private st: RepoSt) {
19
+ makeAutoObservable<this, 'st' | 'timer' | 'gen' | 'polling'>(this, { st: false, timer: false, gen: false, polling: false, list: observableRef })
20
+ }
21
+
22
+ start(): void {
23
+ if (this.polling) return
24
+ this.polling = true
25
+ const gen = ++this.gen
26
+ const tick = async (): Promise<void> => {
27
+ await this.load()
28
+ if (gen === this.gen && this.list.length > 0) this.timer = setTimeout(() => void tick(), PROCESSES_POLL_MS)
29
+ }
30
+ void tick()
31
+ }
32
+
33
+ stop(): void {
34
+ this.polling = false
35
+ this.gen++
36
+ if (this.timer != null) clearTimeout(this.timer)
37
+ this.timer = null
38
+ }
39
+
40
+ async load(): Promise<void> {
41
+ try {
42
+ const r = await fetch(this.st.url('/api/processes.json'))
43
+ if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
44
+ const j = (await r.json()) as { processes: ProcessView[] }
45
+ runInAction(() => {
46
+ this.list = j.processes
47
+ this.error = ''
48
+ })
49
+ } catch (e) {
50
+ runInAction(() => { this.error = `processes: ${(e as Error).message}` })
51
+ }
52
+ }
53
+
54
+ async act(id: string, act: 'start' | 'stop' | 'restart'): Promise<void> {
55
+ try {
56
+ const r = await fetch(this.st.url('/api/processes/act'), { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ id, act }) })
57
+ if (!r.ok) throw new Error(await r.text())
58
+ await this.load()
59
+ } catch (e) {
60
+ runInAction(() => { this.error = `${act} ${id}: ${(e as Error).message}` })
61
+ }
62
+ }
63
+ }
@@ -0,0 +1,119 @@
1
+ /* the project list, the page's left panel. Its classes are pj*: another
2
+ sheet of the page already styles .plist (prose-hits.css). It fills its
3
+ panel (hub-app.tsx, react-resizable-panels), whose width is the only one:
4
+ this sheet sizes the rows and never the list. Colours are hub.css tokens only, spacing is its scale:
5
+ tests/hub-css-invariants.test.ts reads this sheet too. Every icon is 18px
6
+ here AND as attributes on the img (ROW_ICON), so a sheet that fails to
7
+ apply still draws a row, never a 512px icon. */
8
+ .pjlist{flex:1;width:100%;height:100%;display:flex;flex-direction:column;background:var(--bg-side);min-width:0}
9
+ /* the home's global pages: a section of lines in the repo rows' style, then
10
+ a thin divider before the repos */
11
+ .pjhead{flex:none;display:flex;flex-direction:column;padding:var(--s1) 0;border-bottom:1px solid var(--line)}
12
+ /* the rows scroll, the head does not, and no scrollbar draws a band beside
13
+ them: WebKit (the macOS app) ignores scrollbar-width, hence the pseudo */
14
+ .pjfoot{flex:none;display:flex;align-items:center;gap:var(--s1);height:32px;padding:0 var(--s2);border-top:1px solid var(--line)}
15
+ /* the archived rows, just above the foot while its archived button is on:
16
+ the rows panel gives up the room, the foot stays put */
17
+ .pjarch{flex:none;border-top:1px solid var(--line);max-height:40%;overflow-y:auto;scrollbar-width:none;padding:var(--s1) 0}
18
+ .pjarch::-webkit-scrollbar{display:none}
19
+ .pjarow{text-decoration:none;display:flex;align-items:center;gap:var(--s2);width:100%;height:22px;padding:0 var(--s2) 0 var(--s3);border:0;border-radius:0;background:none;color:var(--dim);font-size:12px;text-align:left;cursor:pointer;opacity:.6}
20
+ .pjarow:hover,.pjarow.cur{opacity:1;background:var(--panel2)}
21
+ .pjarow span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
22
+ .pjarow img{flex:none;border-radius:3px;object-fit:contain}
23
+ .pjaini{flex:none;width:14px;height:14px;border-radius:3px;background:var(--panel2)}
24
+ /* the foot's two buttons: sort names the order the list is in, archived
25
+ carries its count and stays lit while its rows show */
26
+ .pgsort,.pjarbtn{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s1);background:transparent;border:1px solid var(--line);border-radius:3px;
27
+ color:var(--dim);font-size:11px;cursor:pointer}
28
+ .pgsort{flex:0 1 auto;min-width:0}
29
+ .pgsortl{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
30
+ .pgsort .hico,.pjarbtn .hico{flex:none;width:14px;height:14px}
31
+ .pgsort:hover,.pjarbtn:hover{color:var(--ink);border-color:var(--accent)}
32
+ .pjarbtn.on{color:var(--ink);background:var(--sel);border-color:var(--accent)}
33
+ .pjarbtn span{font-variant-numeric:tabular-nums}
34
+ /* the sort menu: over the foot, placed by floating-ui (floating.tsx) */
35
+ .pgmenu{z-index:30;min-width:180px;padding:var(--s1) 0;background:var(--panel);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px}
36
+ .pgmhead{padding:var(--s1) var(--s2);color:var(--dim);font-size:11px}
37
+ .pgmi{display:flex;align-items:center;gap:var(--s1);width:100%;height:24px;padding:0 var(--s2);background:none;border:0;border-radius:0;text-align:left;color:var(--ink);font-size:12px;cursor:pointer}
38
+ .pgmi:hover:not(:disabled){background:var(--panel2)}
39
+ .pgmi.on{font-weight:600}
40
+ .pgmi.manual{color:var(--dim);cursor:default;border-top:1px solid var(--line)}
41
+ .pgmi small{margin-left:auto;color:var(--dim);font-size:10px;font-weight:400}
42
+ .pgmck{flex:none;width:12px;color:var(--accent)}
43
+ .pjrows{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:var(--s1) 0;scrollbar-width:none}
44
+ .pjrows::-webkit-scrollbar{display:none}
45
+ .pjwait{display:flex;align-items:center;gap:var(--s2);padding:var(--s1) var(--s3);font-size:12px;color:var(--dim)}
46
+ .pjrow,.pjpl{position:relative;display:flex;align-items:center;gap:var(--s2);width:100%;height:28px;padding:0 var(--s3);
47
+ background:transparent;border:0;border-radius:0;text-align:left;color:var(--dim);font-size:13px;cursor:pointer}
48
+ .pjrow:hover,.pjpl:hover{background:var(--panel2);color:var(--ink)}
49
+ /* the shown repo: its row lit, marked on the left in its identity colour
50
+ (a global page's line: in the accent) */
51
+ .pjrow.cur,.pjpl.cur{background:var(--sel);color:var(--pjc,var(--ink));box-shadow:inset 3px 0 0 var(--pjc,var(--accent));font-weight:600}
52
+ .pjpl .hico{flex:none;width:18px;height:18px;color:inherit}
53
+ /* the selected repo while a global page is up: still marked, dimmer */
54
+ .pjrow.cur.away{background:none;font-weight:500}
55
+ .pjrow img,.pjrow .pgi{flex:none;width:18px;height:18px;border-radius:4px;object-fit:contain;pointer-events:none}
56
+ .pjname{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
57
+ /* the initial on a hue hashed from the name (projects-svc hueOf) */
58
+ .pgi{display:flex;align-items:center;justify-content:center;font-size:10px;font-weight:600;user-select:none;
59
+ color:var(--c);background:color-mix(in srgb,var(--c) 22%,var(--panel2))}
60
+ .pgi.h1{--c:var(--k1)}
61
+ .pgi.h2{--c:var(--k2)}
62
+ .pgi.h3{--c:var(--k3)}
63
+ .pgi.h4{--c:var(--k4)}
64
+ .pgi.h5{--c:var(--k5)}
65
+ .pgi.h6{--c:var(--k6)}
66
+ /* the row says three things and no more (projects-svc faceOf) */
67
+ /* closed in VS Code: the icon and the name dim, the badge does not */
68
+ .pjrow.dim img,.pjrow.dim .pgi,.pjrow.dim .pjname{opacity:.5}
69
+ /* ⌘1 to ⌘9 select the first nine rows (keys.ts): the hint sits at the row's end */
70
+ .pjkey{flex:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
71
+ .pjrow:hover .pjkey,.pjrow.cur .pjkey,.pjpl:hover .pjkey,.pjpl.cur .pjkey{opacity:1}
72
+ /* the one badge: open asks, at the end of the row */
73
+ .pga{flex:none;min-width:16px;height:16px;padding:0 var(--s1);border-radius:8px;
74
+ font-size:10px;font-weight:700;line-height:16px;text-align:center;color:var(--bg);background:var(--warn)}
75
+ /* Claude sessions running: a thin edge on the left that breathes */
76
+ .pgs{position:absolute;left:0;top:5px;bottom:5px;width:2px;border-radius:1px;background:var(--accent);pointer-events:none;animation:pgpulse 2.4s ease-in-out infinite}
77
+ /* the home only: the repo's hub state (starting, crashed) */
78
+ .pgd{flex:none;width:6px;height:6px;border-radius:3px;pointer-events:none}
79
+ .pgd.up{background:var(--ok)}.pgd.starting{background:var(--warn)}.pgd.crashed{background:var(--red)}
80
+ /* the home only: a global process this repo uses is off */
81
+ .pgw{flex:none;width:6px;height:6px;border-radius:1px;background:var(--warn)}
82
+ @keyframes pgpulse{0%,100%{opacity:1}50%{opacity:.25}}
83
+ @media (prefers-reduced-motion:reduce){.pgs{animation:none}}
84
+ /* the hover card: beside its row, placed by floating-ui (floating.tsx), one
85
+ at a time, never interactive. Header, then what needs a human, then the
86
+ label and value rows of the check page (.wfact .wk .wv from hub.css) */
87
+ .pgcard{z-index:20;width:300px;padding:var(--s3);pointer-events:none;
88
+ background:var(--panel);border:1px solid var(--line);border-radius:4px;box-shadow:0 6px 20px var(--shadow);font-size:12px;line-height:1.45;color:var(--ink)}
89
+ .pgch{display:flex;align-items:center;gap:var(--s2);min-width:0}
90
+ .pgcface{flex:none;display:flex}
91
+ .pgcface img,.pgcface .pgi{width:20px;height:20px;border-radius:4px;font-size:11px}
92
+ .pgcname{flex:1;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
93
+ .pgcthis{font-weight:400;font-size:11px;color:var(--dim)}
94
+ .pgcver{flex:none;color:var(--dim);font-size:11px;font-variant-numeric:tabular-nums}
95
+ .pgcline{margin-top:var(--s1);color:var(--dim);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
96
+ .pgcdesc{margin-top:var(--s1);color:var(--dim);font-size:11px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
97
+ /* NEEDS YOU: a warn toned bullet, set apart from the facts */
98
+ .pgcneeds{margin-top:var(--s2);padding:var(--s1) var(--s2);border-left:2px solid var(--warn);background:var(--panel2);border-radius:0 3px 3px 0}
99
+ .pgcneed{display:flex;gap:var(--s1);min-width:0;color:var(--warn)}
100
+ .pgcneed.red{color:var(--red);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
101
+ .pgcask{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
102
+ .pgcmore{flex:none}
103
+ .pgcfacts{margin-top:var(--s2)}
104
+ .pgcfacts .wk{flex-basis:64px;white-space:nowrap}
105
+ .pgcfacts .wv{text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;word-break:normal}
106
+ .pgcfacts .wv.warn{color:var(--warn)}
107
+ .pgerr{padding:0 var(--s3);font-size:12px;font-weight:700;color:var(--red);cursor:help;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
108
+ /* a drag: the row being moved fades, and the gap it would land in is a line
109
+ drawn on a row edge */
110
+ .pjrow.dragging{opacity:.4}
111
+ .pjrow.dropb::before,.pjrow.dropa::after{content:'';position:absolute;left:var(--s2);right:var(--s2);height:2px;border-radius:1px;background:var(--accent);pointer-events:none}
112
+ .pjrow.dropb::before{top:-1px}
113
+ .pjrow.dropa::after{bottom:-1px}
114
+ /* the separators of the page's panels: a 1px divider in the line colour, the
115
+ panels touching it, and a wider hit area that draws nothing (6px each side).
116
+ The accent shows only on hover and while dragging */
117
+ .rsep{position:relative;flex:none;width:1px;background:var(--line)}
118
+ .rsep::before{content:'';position:absolute;top:0;bottom:0;left:-6px;right:-6px;cursor:col-resize;background:transparent;z-index:6}
119
+ .rsep:hover,.rsep[data-separator="hover"],.rsep[data-separator="active"]{background:var(--accent)}
@@ -0,0 +1,266 @@
1
+ // the project list: one row per repo that shares this page's house wiki,
2
+ // down the left of the page, a small icon and the repo's name on one line,
3
+ // the shown repo's row marked in its identity colour. Its width is the
4
+ // panel the page gives it (hub-app.tsx, react-resizable-panels). A row says
5
+ // three things and no more (faceOf): the open asks as its one badge, a dim
6
+ // icon while VS Code does not have the folder open, a thin edge breathing
7
+ // while Claude sessions run in it; on the home, a dot while its hub starts
8
+ // or crashed and a mark while a global process it uses is off. Everything
9
+ // else is the hover card, placed by floating-ui beside the row. A click
10
+ // selects the repo in place (so does ⌘1 to ⌘9 for the first nine rows, the
11
+ // hint at each row's end), a double click opens the folder in VS Code, a
12
+ // drag reorders the list. The foot holds two buttons: sort opens a menu of
13
+ // labeled sorts (a pick sorts once, the button names the order the list is
14
+ // in), archived shows or hides the archived repos just above the foot
15
+ // (names only, no ⌘ number, a row opens the archived page, nothing loads),
16
+ // the foot never moving. On the home a section of four lines on top, in the
17
+ // repo rows' style, opens the overview, the picker, the activity and the
18
+ // plan, each with its shortcut at the line's end, the page shown lit. It
19
+ // renders from the first paint with the list this browser kept, so a load
20
+ // moves nothing.
21
+ import { autoUpdate, FloatingPortal, useDismiss, useFloating, useHover, useInteractions, useRole } from '@floating-ui/react'
22
+ import { observer } from 'mobx-react-lite'
23
+ import { useEffect, type CSSProperties } from 'react'
24
+ import { middleware, OPEN_MS } from './floating.tsx'
25
+ import { Icon, ICON } from './icons.tsx'
26
+ import { plainClick } from './home-st.ts'
27
+ import { PICKER_KEY, PLACE_KEY } from './keys.ts'
28
+ import { cardModel, faceOf, hueOf, SORTS, type Place, type Project, type ProjectsSvc } from './projects-svc.ts'
29
+
30
+ /** what the list needs of its page: the hub and the home both carry one */
31
+ export type ListHost = { projects: ProjectsSvc }
32
+
33
+ /** the lines on top of the list, in their order: the overview, the picker,
34
+ * then the places, their shortcuts ⌘J ⌘K ⌘L ⌘' top to bottom */
35
+ const PLACES: { id: Place | 'picker'; label: string; title: string }[] = [
36
+ { id: 'overview', label: 'overview', title: 'the overview: shipkit, every global process and every repo on this machine' },
37
+ { id: 'picker', label: 'jump to…', title: 'jump to a page, a repo or one of its resources' },
38
+ { id: 'activity', label: 'activity', title: 'what every repo committed lately, what it holds uncommitted, its branch and whether it is pushed' },
39
+ { id: 'planned', label: 'planned', title: 'every check the machine runs on its own: now, next, late, and why' },
40
+ ]
41
+
42
+ /** the icon's size on a row, in px, as attributes and in the sheet */
43
+ export const ROW_ICON = 18
44
+
45
+ export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: number }) => {
46
+ const s = app.projects
47
+ const gap = s.shownGap
48
+ const face = faceOf(p)
49
+ const open = s.hover === p.name
50
+ const f = useFloating({
51
+ open,
52
+ onOpenChange: (v) => (v ? s.hoverIn(p.name) : s.hoverOut(p.name)),
53
+ placement: 'right-start',
54
+ strategy: 'fixed',
55
+ middleware: middleware(8),
56
+ whileElementsMounted: autoUpdate,
57
+ })
58
+ const hover = useHover(f.context, { delay: { open: OPEN_MS, close: 0 }, enabled: s.drag == null })
59
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover])
60
+ // the drop line rides on a row edge, so showing it moves nothing
61
+ const edge = gap === i ? ' dropb' : gap === i + 1 && i === s.list.length - 1 ? ' dropa' : ''
62
+ const cur = s.isCurrent(p)
63
+ const cls = `pjrow${cur ? ' cur' : ''}${cur && s.away ? ' away' : ''}${face.dim ? ' dim' : ''}${s.drag === p.name ? ' dragging' : ''}${edge}`
64
+ const dot = s.dotOf(p.name)
65
+ const warn = s.warnOf(p.name)
66
+ // the shown repo's row is marked in its own colour
67
+ const mark = cur ? ({ '--pjc': s.identityOf(p.name).colorDark } as CSSProperties) : undefined
68
+ return (
69
+ <>
70
+ <button ref={f.refs.setReference} className={cls} aria-label={p.name} style={mark} draggable
71
+ {...getReferenceProps({
72
+ onClick: () => s.click(p.name),
73
+ onDoubleClick: () => s.doubleClick(p.name),
74
+ onDragStart: (e) => {
75
+ e.dataTransfer.effectAllowed = 'move'
76
+ e.dataTransfer.setData('text/plain', p.name)
77
+ s.dragStart(p.name)
78
+ },
79
+ onDragOver: (e) => {
80
+ if (s.drag == null) return
81
+ e.preventDefault()
82
+ const r = e.currentTarget.getBoundingClientRect()
83
+ s.dragOver(i, e.clientY - r.top, r.height)
84
+ },
85
+ onDrop: (e) => {
86
+ e.preventDefault()
87
+ s.drop()
88
+ },
89
+ onDragEnd: () => s.dragEnd(),
90
+ })}>
91
+ <Face p={p} px={ROW_ICON} />
92
+ <span className="pjname">{p.name}</span>
93
+ {face.badge != null ? <b className="pga">{face.badge}</b> : null}
94
+ {face.busy ? <i className="pgs" /> : null}
95
+ {dot != null ? <i className={`pgd ${dot}`} /> : null}
96
+ {warn != null ? <i className="pgw" title={warn} /> : null}
97
+ {i < 9 ? <kbd className="pjkey">⌘{i + 1}</kbd> : null}
98
+ </button>
99
+ {open ? (
100
+ <FloatingPortal>
101
+ <div ref={f.refs.setFloating} className="pgcard" role="tooltip" style={f.floatingStyles} {...getFloatingProps()}>
102
+ <Card p={p} now={Date.now()} />
103
+ </div>
104
+ </FloatingPortal>
105
+ ) : null}
106
+ </>
107
+ )
108
+ })
109
+
110
+ // the img carries its size as attributes too: an icon file is 512px, and
111
+ // without them a sheet that fails to apply draws it at that size
112
+ export const Face = ({ p, px }: { p: Project; px: number }) => (p.icon != null
113
+ ? <img src={p.icon} alt="" width={px} height={px} draggable={false} />
114
+ : <span className={`pgi h${hueOf(p.name)}`}>{(p.name.match(/[a-zA-Z]/)?.[0] ?? p.name.charAt(0)).toUpperCase()}</span>)
115
+
116
+ /** the card for one repo: header, what needs a human, then the fact rows,
117
+ * in the label and value shape the check page's WHAT widget uses */
118
+ export const Card = ({ p, now }: { p: Project; now: number }) => {
119
+ const m = cardModel(p, now)
120
+ return (
121
+ <>
122
+ <div className="pgch">
123
+ <span className="pgcface"><Face p={p} px={20} /></span>
124
+ <b className="pgcname">{m.head.name}{m.head.current ? <span className="pgcthis"> this hub</span> : null}</b>
125
+ {m.head.version == null ? null : <span className="pgcver">v{m.head.version.replace(/^v/, '')}</span>}
126
+ </div>
127
+ {m.head.branch == null ? null : <div className="pgcline">{m.head.branch}</div>}
128
+ {m.head.description == null ? null : <div className="pgcdesc">{m.head.description}</div>}
129
+ {m.needs == null ? null : (
130
+ <div className="pgcneeds">
131
+ {m.needs.asks == null ? null : (
132
+ <div className="pgcneed">
133
+ <span className="pgcask">{m.needs.asks.count} ask{m.needs.asks.count === 1 ? '' : 's'}: {m.needs.asks.titles.join(' · ')}</span>
134
+ {m.needs.asks.more > 0 ? <span className="pgcmore">+{m.needs.asks.more}</span> : null}
135
+ </div>
136
+ )}
137
+ {m.needs.gate == null ? null : <div className="pgcneed red">{m.needs.gate}</div>}
138
+ </div>
139
+ )}
140
+ {m.facts.length === 0 ? null : (
141
+ <div className="pgcfacts">
142
+ {m.facts.map((f) => (
143
+ <div key={f.k} className="wfact">
144
+ <span className="wk">{f.k}</span>
145
+ <span className={`wv${f.tone == null ? '' : ` ${f.tone}`}`}>{f.v}</span>
146
+ </div>
147
+ ))}
148
+ </div>
149
+ )}
150
+ </>
151
+ )
152
+ }
153
+
154
+ export const ProjectList = observer(({ app }: { app: ListHost }) => {
155
+ useEffect(() => app.projects.start(), [app])
156
+ const s = app.projects
157
+ return (
158
+ <nav className="pjlist" aria-label="projects">
159
+ {s.onPlace == null && s.onPicker == null && s.error === '' ? null : (
160
+ <div className="pjhead">
161
+ {PLACES.map((pl) => {
162
+ const id = pl.id
163
+ const run = id === 'picker' ? s.onPicker : s.onPlace == null ? null : () => s.onPlace?.(id)
164
+ if (run == null) return null
165
+ const on = id === 'picker' ? s.pickerOpen() : s.place === id
166
+ const key = id === 'picker' ? PICKER_KEY : PLACE_KEY[id]
167
+ return (
168
+ <button key={id} className={`pjpl${on ? ' cur' : ''}`} aria-label={pl.label} title={`${pl.title} (${key})`} onClick={run}>
169
+ <Icon d={ICON[id === 'picker' ? 'search' : id === 'overview' ? 'home' : id]} />
170
+ <span className="pjname">{pl.label}</span>
171
+ <kbd className="pjkey">{key}</kbd>
172
+ </button>
173
+ )
174
+ })}
175
+ {s.error !== '' ? <span className="pgerr" title={s.error}>! {s.error}</span> : null}
176
+ </div>
177
+ )}
178
+ <div className="pjrows">
179
+ {s.list.length === 0 && s.error === '' ? <span className="pjwait"><span className="spin" />reading the repos…</span> : null}
180
+ {s.list.map((p, i) => <Row key={p.name} app={app} p={p} i={i} />)}
181
+ </div>
182
+ {s.archivedOpen && s.archived.length > 0 && s.onArchived != null ? (
183
+ <div className="pjarch">
184
+ {s.archived.map((p) => (
185
+ <a key={p.name} className="pjarow" href="/archived" title={`${p.name} is archived: nothing runs there`} onClick={(e) => {
186
+ if (!plainClick(e)) return
187
+ e.preventDefault()
188
+ s.onArchived?.()
189
+ }}>
190
+ {p.icon != null ? <img src={p.icon} alt="" width={14} height={14} draggable={false} /> : <i className="pjaini" />}
191
+ <span>{p.name}</span>
192
+ </a>
193
+ ))}
194
+ </div>
195
+ ) : null}
196
+ <div className="pjfoot">
197
+ <SortButton s={s} />
198
+ {s.archived.length === 0 || s.onArchived == null ? null : (
199
+ <button className={`pjarbtn${s.archivedOpen ? ' on' : ''}`} aria-pressed={s.archivedOpen}
200
+ title={`${s.archivedOpen ? 'hide' : 'show'} the ${s.archived.length} archived repos: nothing runs there, a row opens the archived page`}
201
+ onClick={() => s.toggleArchivedOpen()}>
202
+ <Icon d={ICON.archived} /><span>{s.archived.length}</span>
203
+ </button>
204
+ )}
205
+ </div>
206
+ </nav>
207
+ )
208
+ })
209
+
210
+ /** the words for the order the list is in */
211
+ const orderWords = (s: ProjectsSvc): string => {
212
+ const m = s.sortMode
213
+ return m === 'manual' ? 'manual order' : SORTS.find((x) => x.id === m)!.label
214
+ }
215
+
216
+ /** the sort button: it names the order the list is in and opens the menu,
217
+ * and sorts nothing itself */
218
+ const SortButton = observer(({ s }: { s: ProjectsSvc }) => {
219
+ const f = useFloating({
220
+ open: s.sortMenuOpen,
221
+ onOpenChange: (v) => s.setSortMenu(v),
222
+ placement: 'top-start',
223
+ strategy: 'fixed',
224
+ middleware: middleware(6),
225
+ whileElementsMounted: autoUpdate,
226
+ })
227
+ const dismiss = useDismiss(f.context)
228
+ const role = useRole(f.context, { role: 'menu' })
229
+ const { getReferenceProps, getFloatingProps } = useInteractions([dismiss, role])
230
+ return (
231
+ <>
232
+ <button ref={f.refs.setReference} className="pgsort" title={`the list is in ${orderWords(s)}: pick a sort`}
233
+ {...getReferenceProps({ onClick: () => s.toggleSortMenu() })}>
234
+ <Icon d={ICON.sort} /><span className="pgsortl">{orderWords(s)}</span>
235
+ </button>
236
+ {s.sortMenuOpen ? (
237
+ <FloatingPortal>
238
+ <div ref={f.refs.setFloating} className="pgmenu" style={f.floatingStyles} {...getFloatingProps()}>
239
+ <SortMenu s={s} />
240
+ </div>
241
+ </FloatingPortal>
242
+ ) : null}
243
+ </>
244
+ )
245
+ })
246
+
247
+ /** the menu's lines: one per sort, the order the list is in checked. The
248
+ * manual order is a state a drag makes, never a pick */
249
+ export const SortMenu = observer(({ s }: { s: ProjectsSvc }) => {
250
+ const cur = s.sortMode
251
+ return (
252
+ <>
253
+ <div className="pgmhead">sort the list once by</div>
254
+ {SORTS.map((m) => (
255
+ <button key={m.id} role="menuitemradio" aria-checked={cur === m.id} className={`pgmi${cur === m.id ? ' on' : ''}`}
256
+ title={m.title} onClick={() => s.sortBy(m.id)}>
257
+ <span className="pgmck">{cur === m.id ? '✓' : ''}</span>{m.label}
258
+ </button>
259
+ ))}
260
+ <button role="menuitemradio" aria-checked={cur === 'manual'} className={`pgmi manual${cur === 'manual' ? ' on' : ''}`} disabled
261
+ title="a drag of a row makes the manual order, and every sort ends in it">
262
+ <span className="pgmck">{cur === 'manual' ? '✓' : ''}</span>manual order<small>drag a row</small>
263
+ </button>
264
+ </>
265
+ )
266
+ })