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,78 @@
1
+ /* the activity page (activity-view.tsx): one table, a row group per repo. The
2
+ table is fixed width so a long title ellipses instead of scrolling the page
3
+ sideways; the file list floats on hover. State colours only: a branch green
4
+ on main, orange elsewhere; a commit orange until pushed; dirt red. */
5
+ .activity{font-size:13px;container-type:inline-size}
6
+ .actop{display:flex;align-items:center;flex-wrap:wrap;gap:var(--s3);margin-bottom:var(--s3)}
7
+ .acwin{display:inline-flex;border:1px solid var(--line);border-radius:4px;overflow:hidden}
8
+ .acwin button{border:0;border-radius:0;padding:var(--s0) var(--s2);background:none;color:var(--dim);font-size:12px}
9
+ .acwin button:hover{color:var(--ink);background:var(--panel2)}
10
+ .acwin button.on{color:var(--accent);background:var(--sel)}
11
+ .accap{display:inline-flex;align-items:center;gap:var(--s1);font-size:12px}
12
+ .accap input{width:44px;height:22px;box-sizing:border-box;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:4px;padding:var(--s0) var(--s1);font:inherit;font-variant-numeric:tabular-nums}
13
+ .accap input:focus{outline:none;border-color:var(--accent)}
14
+ .acnext{display:inline-flex;align-items:center;min-width:40px;color:var(--dim);font-size:12px;font-variant-numeric:tabular-nums}
15
+ /* the counts beside the controls, and the refresh button, as small rounded chips */
16
+ .acpill{display:inline-flex;align-items:center;gap:var(--s1);padding:0 var(--s2);border:1px solid var(--line);border-radius:999px;background:var(--hi3);color:var(--dim);font-size:11.5px;line-height:20px;font-variant-numeric:tabular-nums;white-space:nowrap}
17
+ button.acpill{cursor:pointer;font:inherit;font-size:11.5px}
18
+ button.acpill:hover:not(:disabled){color:var(--ink);border-color:var(--accent)}
19
+ button.acpill:disabled{cursor:default}
20
+ .acring{display:inline-block}
21
+ .acring.on{color:var(--accent);animation:acspin 1s linear infinite}
22
+ @keyframes acspin{to{transform:rotate(360deg)}}
23
+ .acdim{color:var(--dim)}
24
+ .actable{width:100%;table-layout:fixed;border-collapse:collapse}
25
+ .actable.stale{opacity:.5}
26
+ .actable th{text-align:left;font-weight:400;color:var(--dim);font-size:11.5px;padding:var(--s1) var(--s2);border-bottom:1px solid var(--line)}
27
+ .actable td{padding:var(--s0) var(--s2);vertical-align:top;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
28
+ .acgrp{border-bottom:1px solid var(--line)}
29
+ .acgrp tr:first-child td{padding-top:var(--s1)}
30
+ .acgrp tr:last-child td{padding-bottom:var(--s1)}
31
+ .acgrp:hover{background:var(--panel)}
32
+ .actable{--acname-w:170px}.actable,.acchktip{--acmark-w:10px}.c-name{width:var(--acname-w)}.c-status{width:52px}.c-branch{width:150px}
33
+ .c-sha{width:72px}.c-when{width:56px}.c-who{width:90px}
34
+ .acrepo{display:inline-flex;align-items:center;gap:var(--s1);max-width:100%;color:var(--ink);text-decoration:none}
35
+ .acrepo span{overflow:hidden;text-overflow:ellipsis}
36
+ .acrepo img{flex:none;border-radius:3px;object-fit:contain}
37
+ .acrepo:hover span{text-decoration:underline}
38
+ .t-ok{color:var(--ok)}.t-warn{color:var(--warn)}.t-red{color:var(--red)}
39
+ .acstatus{white-space:nowrap;font-variant-numeric:tabular-nums;cursor:default}
40
+ /* the status dot is a button: hover floats its checks, a click lists them under the row */
41
+ .acdot{border:0;background:none;padding:0 var(--s1);margin:0 calc(-1 * var(--s1));font:inherit;font-variant-numeric:tabular-nums;color:inherit;cursor:pointer;border-radius:3px}
42
+ .acdot:hover,.acdot[aria-expanded="true"]{background:var(--panel2)}
43
+ .acchktip{z-index:40;max-width:560px;max-height:60vh;overflow:auto;background:var(--panel2);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 10px var(--shadow);padding:var(--s1) var(--s2);color:var(--ink);font-size:12px}
44
+ .acchecks{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--s1);white-space:normal}
45
+ .acchecks li{display:flex;flex-direction:column;min-width:0}
46
+ .acchkhead{display:flex;align-items:baseline;gap:var(--s2);min-width:0}
47
+ .acchkmark{flex:none;width:var(--acmark-w)}
48
+ .acchecks .fail .acchkmark,.acchecks .fail .acchkid{color:var(--red)}
49
+ .acchecks .warn .acchkmark,.acchecks .warn .acchkid{color:var(--warn)}
50
+ .acchkid{flex:none;font:11.5px ui-monospace,Menlo,monospace;text-decoration:none}
51
+ .acchkid:hover{text-decoration:underline}
52
+ .acchktitle{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
53
+ .acchkline{margin-left:calc(var(--acmark-w) + var(--s2));color:var(--dim);font:11.5px/1.4 ui-monospace,Menlo,monospace;overflow-wrap:anywhere}
54
+ .acchkrow td{padding:var(--s1) var(--s2) var(--s2) calc(var(--acname-w) + var(--s2));white-space:normal}
55
+ .acdirty,.acdirt,.acerr{color:var(--red)}
56
+ .acdirt{cursor:help}
57
+ .acsha{font:11.5px ui-monospace,Menlo,monospace;color:var(--dim)}
58
+ .acwhen{color:var(--dim);font-variant-numeric:tabular-nums;text-align:right}
59
+ .acmark{display:inline-block;width:14px;font-size:11px}
60
+ .pushed .acmark{color:var(--ok)}
61
+ .unpushed{color:var(--warn)}
62
+ /* the last commit, from before the window: grey, its marker kept */
63
+ .actitle.old,.unpushed.old{color:var(--dim)}
64
+ .acside{display:inline-block;margin-right:var(--s1);padding:0 var(--s1);border:1px solid var(--line);border-radius:3px;color:var(--dim);font-size:11px}
65
+ .acwho{color:var(--dim)}
66
+ .acmore{border:0;background:none;padding:0;color:var(--accent);font-size:12px}
67
+ .acmore:hover{text-decoration:underline}
68
+ .acfiles{z-index:40;max-width:480px;max-height:60vh;overflow:hidden;background:var(--panel2);border:1px solid var(--line);border-radius:3px;box-shadow:0 2px 10px var(--shadow);padding:var(--s1) var(--s2);color:var(--ink);pointer-events:none}
69
+ .acfiles pre{margin:0;font:11.5px/1.4 ui-monospace,Menlo,monospace;white-space:pre-wrap;word-break:break-all}
70
+ /* narrow: the sha and the author go, the title keeps its room */
71
+ @container (max-width:680px){.c-sha,.acsha,.c-who,.acwho{display:none}.actable{--acname-w:120px}.c-branch{width:100px}.acchkrow td{padding-left:var(--s2)}}
72
+ /* the open in Fork button leads the commit list, quiet until its row is hovered */
73
+ .c-fork{width:22px}
74
+ .actable td.acforkcell{padding-right:0;overflow:visible}
75
+ .acfork{display:inline-flex;align-items:center;vertical-align:-2px;padding:0;border:0;background:none;color:var(--dim);cursor:pointer;opacity:.5}
76
+ .acfork .hico{width:13px;height:13px}
77
+ .acgrp:hover .acfork{opacity:1}
78
+ .acfork:hover{color:var(--accent)}
@@ -0,0 +1,41 @@
1
+ // A repo's admin page, as state: what administers the repo rather than works
2
+ // in it. Archive asks first (the confirmation is this state, so it survives a
3
+ // re-render and a test drives it headless), reload and keep active go to the
4
+ // machine by the repo's key. On a hub of its own there is no machine, and no
5
+ // admin page.
6
+ import { makeAutoObservable } from 'mobx'
7
+ import type { RepoSt } from './repo-st.ts'
8
+
9
+ export class AdminSvc {
10
+ /** the archive button was pressed: its confirmation shows in its place */
11
+ confirming = false
12
+
13
+ constructor(private st: RepoSt) {
14
+ makeAutoObservable<this, 'st'>(this, { st: false })
15
+ }
16
+
17
+ askArchive(): void {
18
+ this.confirming = true
19
+ }
20
+ cancel(): void {
21
+ this.confirming = false
22
+ }
23
+ /** archived, the repo leaves the project list and its page shows the
24
+ * archived note with its way back */
25
+ archive(): void {
26
+ this.confirming = false
27
+ void this.st.home.projects.setArchived(this.st.key, true)
28
+ }
29
+
30
+ /** its hub, stopped in the daemon and loaded again with its config read fresh */
31
+ reload(): Promise<boolean> {
32
+ return this.st.home.machine?.hub(this.st.key, 'restart') ?? Promise.resolve(false)
33
+ }
34
+
35
+ get active(): boolean {
36
+ return this.st.home.machine?.isActive(this.st.key) === true
37
+ }
38
+ setActive(on: boolean): Promise<boolean> {
39
+ return this.st.home.machine?.setActive(this.st.key, on) ?? Promise.resolve(false)
40
+ }
41
+ }
@@ -0,0 +1,42 @@
1
+ // THE ADMIN PAGE of a repo (`#page:admin`): what administers the repo rather
2
+ // than works in it, one row per control, each with the one sentence that says
3
+ // what it does. State in admin-svc.ts. Only on the home: a hub of its own has
4
+ // no machine to administer it.
5
+ import { observer } from 'mobx-react-lite'
6
+ import type { ReactNode } from 'react'
7
+ import type { RepoSt } from './repo-st.ts'
8
+
9
+ const Row = ({ title, says, children }: { title: string; says: string; children: ReactNode }) => (
10
+ <div className="adrow">
11
+ <div className="adtext">
12
+ <b>{title}</b>
13
+ <span className="dim">{says}</span>
14
+ </div>
15
+ <div className="adctl">{children}</div>
16
+ </div>
17
+ )
18
+
19
+ export const AdminPage = observer(({ app }: { app: RepoSt }) => {
20
+ const a = app.admin
21
+ const w = app.home.machine?.worker(app.key)
22
+ const name = app.name
23
+ return (
24
+ <div className="colcard checkcard">
25
+ <div className="chead">
26
+ <span className="tid">admin</span>
27
+ <h3 className="ctitle">{name}</h3>
28
+ </div>
29
+ <Row title="reload the hub" says={`stops ${name}'s hub in the daemon and loads it again, its repo.config.ts read fresh and its processes started again. The daemon already does it on its own when repo.config.ts changes: this is for a hub that is stuck.${w == null ? '' : ` Its hub is ${w.status} now.`}`}>
30
+ <button className="ghost" onClick={() => void a.reload()}>reload</button>
31
+ </Row>
32
+ <Row title="keep active" says="an active repo runs the checks the plan holds for it and gets its red pings. It is active on its own while an agent works in it or VS Code has it open: this keeps it active without either.">
33
+ <label className="adflag"><input type="checkbox" checked={a.active} onChange={(e) => void a.setActive(e.currentTarget.checked)} />active</label>
34
+ </Row>
35
+ <Row title="archive" says="it leaves the project list for the archived page, and nothing runs there: no hub, no watch, no plan rows, no processes, no widgets. Written to repo.config.ts as archived: true, and undone from the archived page.">
36
+ {a.confirming
37
+ ? <span className="adconfirm"><span>archive {name}?</span><button className="primary" onClick={() => a.archive()}>archive it</button><button className="ghost" onClick={() => a.cancel()}>cancel</button></span>
38
+ : <button className="ghost" onClick={() => a.askArchive()}>archive</button>}
39
+ </Row>
40
+ </div>
41
+ )
42
+ })
@@ -1,12 +1,12 @@
1
1
  // The view state of THE run chart: its zoom, the width it has on screen and
2
2
  // what is under the pointer, over the geometry (timeline-geometry.ts) and the
3
- // drawing (timeline-chart.tsx). The live strip owns it, and nothing else in
4
- // the hub draws a run. What differs is the source: folded, a rolling window
5
- // that ends now; unfolded, the whole record opened on its last minutes.
3
+ // drawing (timeline-chart.tsx). What differs is the source: the dock's, a
4
+ // rolling window that ends now with one group of lanes per repo; a repo's
5
+ // runs page's, its whole record opened on its last minutes.
6
6
  import { makeAutoObservable, observableRef } from 'mobx'
7
7
  import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
8
8
  import {
9
- anchorScroll, buildAxis, fitScale, fitWindow, layoutTimeline, maxZoom, PAD_PX, windowAxis, ZOOM_MIN, zoomStep, type Axis, type Layout, type Span,
9
+ anchorScroll, buildAxis, fitScale, fitWindow, layoutGroups, layoutTimeline, maxZoom, PAD_PX, windowAxis, ZOOM_MIN, zoomStep, type Axis, type ChartGroup, type Layout, type Span,
10
10
  } from './timeline-geometry.ts'
11
11
 
12
12
  /** what a chart draws. `windowMs` null = the whole record, with idle
@@ -23,6 +23,11 @@ export type ChartSource = {
23
23
  /** the span the view opens on: at its home zoom this much time, ending
24
24
  * now, fills the width. Absent = home is zoom 1, the whole source */
25
25
  homeSpanMs?: number
26
+ /** one group of lanes per key, over a rolling window only. `invocations`
27
+ * then holds every group's, and `laneCap` bounds each group */
28
+ groups?: ChartGroup[]
29
+ /** the fewest lanes a group takes, so its label fits beside them */
30
+ minLanes?: number
26
31
  }
27
32
 
28
33
  /** what the tooltip says, and the viewport point it is fixed at */
@@ -117,7 +122,9 @@ export class ChartView {
117
122
  get layout(): Layout | null {
118
123
  const s = this.src
119
124
  if (s == null || this.viewW === 0) return null
120
- return layoutTimeline(s.invocations, this.now, this.scale, { from: this.from ?? undefined, laneCap: s.laneCap, cap: s.cap })
125
+ const from = this.from
126
+ if (s.groups != null && from != null) return layoutGroups(s.groups, this.now, this.scale, { from, laneCap: s.laneCap, minLanes: s.minLanes, cap: s.cap })
127
+ return layoutTimeline(s.invocations, this.now, this.scale, { from: from ?? undefined, laneCap: s.laneCap, cap: s.cap })
121
128
  }
122
129
 
123
130
  private axisAt(scale: number): Axis {
@@ -8,7 +8,7 @@ import { useEffect, useRef } from 'react'
8
8
  import { CACHED } from '../../core/outcome-words.ts'
9
9
  import { durationTone, fmtMs } from './dur.ts'
10
10
  import { Help, TERMS } from './help.tsx'
11
- import type { HubSt } from './hub-st.ts'
11
+ import type { RepoSt } from './repo-st.ts'
12
12
  import type { SlowSuite, CheckFacts, RecentRun, WhenAt } from './hub-types.ts'
13
13
  import { renderMd, renderMdInline } from './md.ts'
14
14
  import { nextRun } from './next-run.ts'
@@ -89,7 +89,7 @@ export const copy = (text: string): void => {
89
89
 
90
90
  /** THE COMMAND, exactly as the gate spawns it from the repo root, or the one
91
91
  * line saying its body is a function inside the gate */
92
- export const CommandLine = ({ app, u }: { app: HubSt; u: CheckFacts }) => {
92
+ export const CommandLine = ({ app, u }: { app: RepoSt; u: CheckFacts }) => {
93
93
  const c = commandOf(u, app.root)
94
94
  if (c.kind === 'in-process')
95
95
  return (
@@ -132,7 +132,7 @@ export const CommandLine = ({ app, u }: { app: HubSt; u: CheckFacts }) => {
132
132
  * through the managed block, and leaves the file dirty: a change of place is
133
133
  * a change its owner reviews in a git client, never a commit made on their
134
134
  * behalf. Choosing never asks for the reason before it writes. */
135
- const RunWhen = observer(({ app, u }: { app: HubSt; u: CheckFacts }) => {
135
+ const RunWhen = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
136
136
  const s = app.checks
137
137
  const g = s.gate
138
138
  const shown = s.whenShown(u)
@@ -193,7 +193,7 @@ const Bars = ({ runs, ceiling, budget }: { runs: RecentRun[]; ceiling: number; b
193
193
  }
194
194
 
195
195
  /** how the last answer came: `ran 3m ago in 412ms`, or `cached 16m ago` */
196
- export const LastRunValue = observer(({ app, u }: { app: HubSt; u: CheckFacts }) => {
196
+ export const LastRunValue = observer(({ app, u }: { app: RepoSt; u: CheckFacts }) => {
197
197
  const l = lastRun(u)
198
198
  if (l == null) return <span className="dim">never here</span>
199
199
  return (
@@ -245,7 +245,7 @@ const LogLines = ({ lines }: { lines: string[] }) => (
245
245
 
246
246
  /** the lines of the run in flight, then the lines of the last one. It
247
247
  * scrolls itself while a run streams, so the newest line is on screen */
248
- const LastOutput = observer(({ app, lines }: { app: HubSt; lines: string[] }) => {
248
+ const LastOutput = observer(({ app, lines }: { app: RepoSt; lines: string[] }) => {
249
249
  const live = app.checks.liveLines
250
250
  const box = useRef<HTMLPreElement>(null)
251
251
  useEffect(() => { if (box.current != null) box.current.scrollTop = box.current.scrollHeight }, [live.length])
@@ -263,7 +263,7 @@ const LastOutput = observer(({ app, lines }: { app: HubSt; lines: string[] }) =>
263
263
  /** the slow suites declared under the test step, each with the case it
264
264
  * argued. Moving a suite between the tiers is the SAME managed block write,
265
265
  * and it demands both answers exactly as `defineRepo` does. */
266
- const SlowSuites = observer(({ app }: { app: HubSt }) => {
266
+ const SlowSuites = observer(({ app }: { app: RepoSt }) => {
267
267
  const s = app.checks
268
268
  const declared: SlowSuite[] = s.gate?.tests.slow ?? []
269
269
  return (
@@ -294,7 +294,7 @@ const SlowSuites = observer(({ app }: { app: HubSt }) => {
294
294
 
295
295
  // ── the pages ────────────────────────────────────────────────────────────
296
296
 
297
- export const CheckCard = observer(({ app, id }: { app: HubSt; id: string }) => {
297
+ export const CheckCard = observer(({ app, id }: { app: RepoSt; id: string }) => {
298
298
  const i = app.cols.find('checks', id)
299
299
  if (i == null) return <div className="ph">check vanished, the inventory moved on</div>
300
300
  const u = i.check
@@ -325,8 +325,8 @@ export const CheckCard = observer(({ app, id }: { app: HubSt; id: string }) => {
325
325
  <Remembered u={u} />
326
326
  {/* the help never depends on the second, so it reads the wall
327
327
  clock and the row does not repaint on every tick */}
328
- <Row k="next run" help={nextRun(u, app.cols.schedule, Date.now()).help}>
329
- <TimeAhead clock={app.clock} words={(now) => nextRun(u, app.cols.schedule, now)} />
328
+ <Row k="next run" help={nextRun(u, app.cols.planOf(u.checkId), Date.now()).help}>
329
+ <TimeAhead clock={app.clock} words={(now) => nextRun(u, app.cols.planOf(u.checkId), now)} />
330
330
  </Row>
331
331
  <Row k="last run" help={u.status == null
332
332
  ? 'no run of it is on record in this checkout'
@@ -18,7 +18,7 @@ const list = (xs: string[]): string => (xs.length < 2 ? xs.join('') : `${xs.slic
18
18
  const CADENCE: Record<WhenAt, string> = {
19
19
  always: 'on every run',
20
20
  release: 'on every release run',
21
- health: 'on every daily pass',
21
+ health: 'each time the plan runs it',
22
22
  off: 'nowhere, it is off',
23
23
  }
24
24
 
@@ -35,6 +35,8 @@ export function rememberedFact(u: CachePop): RememberedFact {
35
35
  return { v: 'until a file it reads changes', inputs, help: `keyed on ${list(inputs)}: the answer is ${CACHED} until a change under ${inputs.length > 1 ? 'them' : 'it'}, then it runs again` }
36
36
  if (u.reads === true && inputs.length === 0 && (u.ttl == null || u.ttl === '') && u.never == null)
37
37
  return { v: 'until a file it read changes', inputs: [], help: `keyed on what its last run read: the answer is ${CACHED} until one of those files, existence probes or listings changes, then it runs again. \`shipkit keys --why\` names them` }
38
+ if (u.ttl === 'forever')
39
+ return { v: 'until its declaration changes', inputs: [], help: 'a decided fact (`ttl: \'forever\'`): a clean answer is kept until the value it checks is declared differently, and it runs only while no answer exists for that value' }
38
40
  if (u.ttl != null && u.ttl !== '') {
39
41
  let every = u.ttl
40
42
  try { every = spanWords(parseDuration(u.ttl)) } catch { /* an unparsable ttl is shown as written */ }
@@ -8,7 +8,7 @@
8
8
  // dropped when the run ends and the next poll brings the same facts back
9
9
  // from the one place that owns them.
10
10
  import { makeAutoObservable, runInAction } from 'mobx'
11
- import type { HubSt } from './hub-st.ts'
11
+ import type { RepoSt } from './repo-st.ts'
12
12
  import { applyRunEvent, idleRun, startRun, stripText, type RunProgress } from './run-strip.ts'
13
13
  import type { GateBlock, Resource, ResourceState, SlowSuite, CheckEvent, WhenAt } from './hub-types.ts'
14
14
  import { WHEN_OPTION } from './check-words.ts'
@@ -42,6 +42,10 @@ export class ChecksSvc {
42
42
  * stay after the run: they ARE the last output until the next one */
43
43
  liveLines: string[] = []
44
44
  private clearTimer: ReturnType<typeof setTimeout> | null = null
45
+
46
+ dispose(): void {
47
+ if (this.clearTimer != null) clearTimeout(this.clearTimer)
48
+ }
45
49
  /** the managed block in repo.config.ts as the server last sent it: the
46
50
  * hash the next write hands back, and whether the file is waiting for
47
51
  * a commit. null until the first read */
@@ -56,7 +60,7 @@ export class ChecksSvc {
56
60
  /** the slow suite form under the test check */
57
61
  suite: SlowSuite = { file: '', catches: '', whyNotFast: '' }
58
62
 
59
- constructor(private st: HubSt) {
63
+ constructor(private st: RepoSt) {
60
64
  makeAutoObservable<this, 'st' | 'clearTimer'>(this, { st: false, clearTimer: false })
61
65
  }
62
66
 
@@ -89,7 +93,7 @@ export class ChecksSvc {
89
93
  * silent overwrite */
90
94
  async loadGate(): Promise<void> {
91
95
  try {
92
- const r = await fetch('/api/checks/when')
96
+ const r = await fetch(this.st.url('/api/checks/when'))
93
97
  if (!r.ok) throw new Error(await r.text())
94
98
  const b = (await r.json()) as GateBlock
95
99
  runInAction(() => { this.gate = b })
@@ -109,14 +113,14 @@ export class ChecksSvc {
109
113
  // the hash of the file the page last read: without it a write
110
114
  // could land on top of an edit this page never saw
111
115
  if (this.gate == null) await this.loadGate()
112
- const r = await fetch('/api/checks/when', { method: 'POST', body: JSON.stringify({ ...edit, expect: this.gate?.hash }) })
116
+ const r = await fetch(this.st.url('/api/checks/when'), { method: 'POST', body: JSON.stringify({ ...edit, expect: this.gate?.hash }) })
113
117
  if (!r.ok) throw new Error(await r.text())
114
118
  const b = (await r.json()) as GateBlock
115
119
  runInAction(() => { this.gate = b; this.gateNote = said })
116
120
  took = true
117
121
  // the rows carry `when`, so they are right only after the server
118
122
  // has read the config it just wrote
119
- await this.st.cols.poll()
123
+ await this.st.cols.poll(true)
120
124
  } catch (e) {
121
125
  runInAction(() => { this.gateNote = `refused: ${(e as Error).message}` })
122
126
  // whatever the file really says now, so the next try is not
@@ -227,7 +231,7 @@ export class ChecksSvc {
227
231
  this.note = `running ${label}…`
228
232
  })
229
233
  try {
230
- const r = await fetch('/api/check/run', { method: 'POST', body: JSON.stringify({ ...(only == null ? {} : { only }), ...(gate ? { gate } : {}) }) })
234
+ const r = await fetch(this.st.url('/api/check/run'), { method: 'POST', body: JSON.stringify({ ...(only == null ? {} : { only }), ...(gate ? { gate } : {}) }) })
231
235
  const rd = r.body!.getReader()
232
236
  const dec = new TextDecoder()
233
237
  let buf = ''
@@ -270,9 +274,9 @@ export class ChecksSvc {
270
274
  this.clearTimer = setTimeout(() => runInAction(() => { this.progress = idleRun() }), 5_000)
271
275
  // the numbers a run just produced live on the server, in the log it
272
276
  // wrote: one poll and the page is reading them rather than a memory
273
- await this.st.cols.poll()
277
+ await this.st.cols.poll(true)
274
278
  void this.st.health.load()
275
- void this.st.timeline.load()
279
+ void this.st.timeline.load(true)
276
280
  // a test page on screen reads the record the run just wrote
277
281
  const sel = this.st.sel
278
282
  if (sel?.t === 'tests') void this.st.tests.load(sel.check)
@@ -5,7 +5,7 @@
5
5
  // the file wins).
6
6
  import { runInAction } from 'mobx'
7
7
  import { observer } from 'mobx-react-lite'
8
- import type { HubSt } from './hub-st.ts'
8
+ import type { RepoSt } from './repo-st.ts'
9
9
  import type { Resource } from './hub-types.ts'
10
10
  import { renderMd } from './md.ts'
11
11
 
@@ -24,7 +24,7 @@ const CredHead = ({ cred }: { cred: ReturnType<typeof credOf> }) => (
24
24
  </>
25
25
  )
26
26
 
27
- export const ColCard = observer(({ app, col, id }: { app: HubSt; col: string; id: string }) => {
27
+ export const ColCard = observer(({ app, col, id }: { app: RepoSt; col: string; id: string }) => {
28
28
  const c = app.cols
29
29
  const i = c.find(col, id)
30
30
  if (i == null) return <div className="ph">item vanished — the truth file moved on</div>
@@ -103,7 +103,7 @@ export const ColCard = observer(({ app, col, id }: { app: HubSt; col: string; id
103
103
 
104
104
  // collection view — one card per visible instance, the stage-side twin of a
105
105
  // sidebar section (every section gets both views)
106
- export const ColGrid = observer(({ app, id }: { app: HubSt; id: string }) => {
106
+ export const ColGrid = observer(({ app, id }: { app: RepoSt; id: string }) => {
107
107
  const c = app.cols
108
108
  const col = c.sidebarCols.find((x) => x.id === id)
109
109
  if (col == null) return <div className="stagewrap"><div className="ph">collection vanished — the truth moved on</div></div>
@@ -155,7 +155,7 @@ const wireClass = (l: string): string => {
155
155
 
156
156
  // the RAW view of a run: every check has its own page, and this is the one
157
157
  // node that keeps the whole text reachable
158
- export const StatusCard = observer(({ app }: { app: HubSt }) => {
158
+ export const StatusCard = observer(({ app }: { app: RepoSt }) => {
159
159
  const h = app.health
160
160
  const body = h.live.length > 0
161
161
  ? h.live.join('\n')
@@ -4,20 +4,40 @@
4
4
  // file through /api/act/*; errors come back as 400 text shown verbatim on
5
5
  // the card.
6
6
  import { makeAutoObservable, runInAction } from 'mobx'
7
- import type { HubSt } from './hub-st.ts'
8
- import type { Col, ColMeta, HubProblem, Resource, Schedule } from './hub-types.ts'
7
+ import type { RepoSt } from './repo-st.ts'
8
+ import type { Col, ColMeta, HubProblem, Resource } from './hub-types.ts'
9
+ import type { PlanRow } from '../../plan/planner.ts'
10
+ import { readScreen, SCREEN_WRITE_MS, writeScreen } from './screen-cache.ts'
11
+
12
+ /** what /api/resources.json answers */
13
+ type Answer = { collections: ColMeta[]; resources: Resource[]; planned?: PlanRow[] | null; hub?: HubProblem[] }
9
14
 
10
15
  export class ColsSvc {
11
16
  /** exactly what the server last sent; `resources` is this plus the live
12
17
  * results of a run in flight */
13
18
  fetched: Resource[] = []
14
19
  colMeta: ColMeta[] = []
15
- /** the hub's daily health pass timer, as the last poll saw it */
16
- schedule: Schedule | null = null
20
+ /** this repo's rows of the plan, null when no plan covers it */
21
+ planned: PlanRow[] | null = null
17
22
  /** what the server says is wrong with the hub itself */
18
23
  hubProblems: HubProblem[] = []
19
24
  /** the last poll failed: the page cannot reach its own server */
20
25
  fetchErr: string | null = null
26
+ /** a first answer arrived: until then the tree and the page links show a
27
+ * spinner where they will be, never an empty repo */
28
+ loaded = false
29
+ /** what shows was kept by this browser from an earlier load, and the
30
+ * hub has not answered yet (screen-cache.ts) */
31
+ cached = false
32
+ /** the first media category the kept screen had, until the media
33
+ * heartbeat lists the assets */
34
+ cachedCat: string | null = null
35
+ /** when this repo's screen was last kept, 0 for never */
36
+ private keptAt = 0
37
+ /** the last answer, and the media category last kept with it */
38
+ private last: Answer | null = null
39
+ private keptCat: string | null | undefined = undefined
40
+ private restoring: Promise<void> | null = null
21
41
  actErr = ''
22
42
  remarkDraft = ''
23
43
  proofDraft = ''
@@ -25,30 +45,89 @@ export class ColsSvc {
25
45
  foldOverride = new Map<string, boolean>()
26
46
  private timer: ReturnType<typeof setInterval> | null = null
27
47
 
28
- constructor(private st: HubSt) {
29
- makeAutoObservable<this, 'st' | 'timer'>(this, { st: false, timer: false })
48
+ constructor(private st: RepoSt) {
49
+ makeAutoObservable<this, 'st' | 'timer' | 'keptAt' | 'restoring' | 'readKept' | 'last' | 'keptCat'>(this, { st: false, timer: false, keptAt: false, restoring: false, readKept: false, last: false, keptCat: false })
50
+ }
51
+
52
+ /** one check's row of the plan, null when no plan covers it */
53
+ planOf(check: string): PlanRow | null {
54
+ return this.planned?.find((r) => r.check === check) ?? null
55
+ }
56
+
57
+ /** where this repo's screen is kept: its name, or `self` on a hub of its own */
58
+ private get screenKey(): string {
59
+ return this.st.key === '' ? 'self' : this.st.key
60
+ }
61
+
62
+ /** paint the screen this browser kept, unless the hub answered first.
63
+ * One read per repo, whoever asks */
64
+ restore(): Promise<void> {
65
+ this.restoring ??= this.readKept()
66
+ return this.restoring
67
+ }
68
+
69
+ private async readKept(): Promise<void> {
70
+ const s = await readScreen(this.screenKey)
71
+ if (s == null) return
72
+ runInAction(() => {
73
+ if (this.loaded) return
74
+ this.colMeta = s.collections
75
+ this.fetched = s.resources
76
+ this.planned = s.planned
77
+ this.hubProblems = s.hub
78
+ this.cachedCat = s.mediaCat
79
+ this.cached = true
80
+ })
81
+ }
82
+
83
+ /** keep the screen for the next load: the first answer, then at most
84
+ * once a minute. The answer as parsed, never the observable copy, which
85
+ * IndexedDB cannot clone */
86
+ private keep(j: Answer, cat: string | null = this.st.media.assets[0]?.cat ?? this.cachedCat, force = false): void {
87
+ this.last = j
88
+ const now = Date.now()
89
+ if (!force && now - this.keptAt < SCREEN_WRITE_MS) return
90
+ this.keptAt = now
91
+ this.keptCat = cat
92
+ void writeScreen(this.screenKey, { at: now, collections: j.collections, resources: j.resources, planned: j.planned ?? null, hub: j.hub ?? [], mediaCat: cat })
93
+ }
94
+
95
+ /** the media heartbeat listed the assets: a first category the kept
96
+ * screen does not name yet is kept at once, so the header's media link
97
+ * is there on the next load */
98
+ mediaListed(): void {
99
+ const cat = this.st.media.assets[0]?.cat ?? null
100
+ if (this.last != null && cat !== this.keptCat) this.keep(this.last, cat, true)
30
101
  }
31
102
 
32
103
  start(): void {
104
+ if (this.timer != null) return
33
105
  void this.poll()
34
106
  this.timer = setInterval(() => void this.poll(), 10_000)
35
107
  }
36
- dispose(): void {
108
+ stop(): void {
37
109
  if (this.timer != null) clearInterval(this.timer)
110
+ this.timer = null
38
111
  }
39
112
 
40
- async poll(): Promise<void> {
113
+ /** `fresh`: this page just changed the truth (an act, a run that ended),
114
+ * so the answer waits on a walk that includes it rather than on the one
115
+ * the server keeps warm */
116
+ async poll(fresh = false): Promise<void> {
41
117
  try {
42
- const r = await fetch('/api/resources.json')
118
+ const r = await fetch(this.st.url(fresh ? '/api/resources.json?fresh=1' : '/api/resources.json'))
43
119
  if (!r.ok) throw new Error(await r.text())
44
- const j = (await r.json()) as { collections: ColMeta[]; resources: Resource[]; schedule?: Schedule | null; hub?: HubProblem[] }
120
+ const j = (await r.json()) as Answer
45
121
  runInAction(() => {
46
122
  this.colMeta = j.collections
47
123
  this.fetched = j.resources
48
- this.schedule = j.schedule ?? null
124
+ this.planned = j.planned ?? null
49
125
  this.hubProblems = j.hub ?? []
50
126
  this.fetchErr = null
127
+ this.loaded = true
128
+ this.cached = false
51
129
  })
130
+ this.keep(j)
52
131
  } catch (e) {
53
132
  // a dead server must SAY so, in the header, never leave a tree
54
133
  // that reads like a healthy repo. The last rows stay, dated by it
@@ -130,9 +209,9 @@ export class ColsSvc {
130
209
  private async act(path: string, body: Record<string, unknown>): Promise<boolean> {
131
210
  this.actErr = ''
132
211
  try {
133
- const r = await fetch(`/api/act/${path}`, { method: 'POST', body: JSON.stringify(body) })
212
+ const r = await fetch(this.st.url(`/api/act/${path}`), { method: 'POST', body: JSON.stringify(body) })
134
213
  if (!r.ok) throw new Error(await r.text())
135
- await this.poll()
214
+ await this.poll(true)
136
215
  return true
137
216
  } catch (e) {
138
217
  runInAction(() => { this.actErr = (e as Error).message })
@@ -4,7 +4,7 @@
4
4
  // it is something the panel SAID: the header never reports a state it guessed
5
5
  // from how the iframe looks.
6
6
  import { makeAutoObservable, runInAction } from 'mobx'
7
- import type { HubSt } from './hub-st.ts'
7
+ import type { RepoSt } from './repo-st.ts'
8
8
 
9
9
  export type ComfyBox = { url: string; up: boolean; err: string | null }
10
10
  /** what the panel says, narrowed to the messages this host acts on */
@@ -37,10 +37,16 @@ export class ComfySvc {
37
37
  private mountedFor = ''
38
38
  private frame: HTMLIFrameElement | null = null
39
39
  private handshake: ReturnType<typeof setTimeout> | null = null
40
+ private readonly listener = (e: MessageEvent): void => this.onMessage(e)
40
41
 
41
- constructor(private st: HubSt) {
42
- makeAutoObservable<this, 'st' | 'frame' | 'handshake'>(this, { st: false, frame: false, handshake: false })
43
- window.addEventListener('message', (e) => this.onMessage(e))
42
+ dispose(): void {
43
+ window.removeEventListener('message', this.listener)
44
+ if (this.handshake != null) clearTimeout(this.handshake)
45
+ }
46
+
47
+ constructor(private st: RepoSt) {
48
+ makeAutoObservable<this, 'st' | 'frame' | 'handshake' | 'listener'>(this, { st: false, frame: false, handshake: false, listener: false })
49
+ window.addEventListener('message', this.listener)
44
50
  }
45
51
 
46
52
  setFrame(el: HTMLIFrameElement | null): void {
@@ -78,7 +84,7 @@ export class ComfySvc {
78
84
  }
79
85
 
80
86
  async probeBox(): Promise<void> {
81
- const box = await fetch('/api/comfy/box')
87
+ const box = await fetch(this.st.url('/api/comfy/box'))
82
88
  .then((r) => (r.ok ? (r.json() as Promise<ComfyBox>) : null))
83
89
  .catch(() => null)
84
90
  runInAction(() => {
@@ -107,7 +113,7 @@ export class ComfySvc {
107
113
  })
108
114
  void this.probeBox()
109
115
  try {
110
- const r = await fetch('/api/comfy/panel', { method: 'POST', body: JSON.stringify({ restart }) })
116
+ const r = await fetch(this.st.url('/api/comfy/panel'), { method: 'POST', body: JSON.stringify({ restart }) })
111
117
  if (!r.ok) throw new Error(await r.text())
112
118
  const up = (await r.json()) as { url: string }
113
119
  runInAction(() => {
@@ -4,7 +4,7 @@
4
4
  import { observer } from 'mobx-react-lite'
5
5
  import { useEffect } from 'react'
6
6
  import type { ComfySvc } from './comfy-svc.ts'
7
- import type { HubSt } from './hub-st.ts'
7
+ import type { RepoSt } from './repo-st.ts'
8
8
 
9
9
  const PANEL_CHIP: Record<ComfySvc['state'], { label: string; cls: string; title: string }> = {
10
10
  idle: { label: 'idle', cls: '', title: 'nothing started yet' },
@@ -16,7 +16,7 @@ const PANEL_CHIP: Record<ComfySvc['state'], { label: string; cls: string; title:
16
16
 
17
17
  // the same panel over an asset page. A popup, so the asset, its rolls and its
18
18
  // prompt stay on screen behind it while a value is tried
19
- export const ComfyOverlay = observer(({ app }: { app: HubSt }) => {
19
+ export const ComfyOverlay = observer(({ app }: { app: RepoSt }) => {
20
20
  const c = app.comfy
21
21
  if (!c.overlay) return null
22
22
  const chip = PANEL_CHIP[c.state]
@@ -47,7 +47,7 @@ export const ComfyOverlay = observer(({ app }: { app: HubSt }) => {
47
47
  )
48
48
  })
49
49
 
50
- export const ComfyView = observer(({ app, id }: { app: HubSt; id: string }) => {
50
+ export const ComfyView = observer(({ app, id }: { app: RepoSt; id: string }) => {
51
51
  const c = app.comfy
52
52
  const wf = id.replace(/^comfy:/, '')
53
53
  useEffect(() => {