rvlib-shipkit 1.1.0 → 1.2.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.
- package/CHANGELOG.md +67 -0
- package/package.json +3 -1
- package/src/check.ts +24 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +43 -0
- package/src/checks/SK023-stores-under-cap.check.ts +49 -0
- package/src/checks/SK024-identity-colors.check.ts +49 -0
- package/src/cli/cmd_Check.ts +26 -1
- package/src/cli/cmd_Ci.ts +18 -5
- package/src/cli/cmd_Home.ts +170 -0
- package/src/cli.ts +22 -2
- package/src/comfy/comfy-panel.ts +1 -16
- package/src/core/attach.ts +14 -9
- package/src/core/check-file-names.ts +8 -0
- package/src/core/check-runner.ts +40 -18
- package/src/core/discover-checks.ts +36 -18
- package/src/core/identity-color.ts +203 -0
- package/src/core/identity.ts +91 -0
- package/src/core/job-follow.ts +102 -0
- package/src/core/job-run.ts +27 -0
- package/src/core/jobs.ts +87 -27
- package/src/core/legacy-shapes.ts +1 -14
- package/src/core/machine-db.ts +83 -0
- package/src/core/one-lib.ts +85 -0
- package/src/core/port-holders.ts +43 -0
- package/src/core/retention.ts +213 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +3 -0
- package/src/core/slots.ts +5 -0
- package/src/core/statement-end.ts +17 -0
- package/src/home/attach.ts +95 -0
- package/src/home/autostart.ts +108 -0
- package/src/home/global-units.ts +86 -0
- package/src/home/gui-app/ShipkitGui.swift +159 -0
- package/src/home/gui-app.ts +99 -0
- package/src/home/handoff.ts +51 -0
- package/src/home/home-launchd.ts +104 -0
- package/src/home/home-paths.ts +88 -0
- package/src/home/home-routes.ts +208 -0
- package/src/home/home-start.ts +105 -0
- package/src/home/home-state.ts +43 -0
- package/src/home/home-words.ts +12 -0
- package/src/home/hubs.ts +286 -0
- package/src/home/lib-watch.ts +139 -0
- package/src/home/machine-stores.ts +115 -0
- package/src/home/open-app.ts +16 -0
- package/src/home/pm-config.ts +37 -0
- package/src/home/pm.ts +141 -0
- package/src/home/resources.ts +338 -0
- package/src/home/run-daemon.ts +137 -0
- package/src/home/run-pm.ts +149 -0
- package/src/home/run-unit-action.ts +25 -0
- package/src/home/run-web.ts +37 -0
- package/src/home/serve-daemon.ts +31 -0
- package/src/home/serve-web.ts +172 -0
- package/src/home/supervise.ts +346 -0
- package/src/home/via-hub.ts +142 -0
- package/src/home/widgets.ts +102 -0
- package/src/ledger/check-stats.ts +19 -45
- package/src/ledger/db.ts +69 -17
- package/src/ledger/gate-checks.ts +5 -0
- package/src/ledger/git-scan.ts +31 -7
- package/src/ledger/ledger.ts +40 -14
- package/src/ledger/run-log.ts +58 -23
- package/src/ledger/store.ts +228 -103
- package/src/ledger/world-cache.ts +15 -13
- package/src/plan/cadence.ts +237 -0
- package/src/plan/planner.ts +303 -0
- package/src/plan/repo-source.ts +100 -0
- package/src/problems/problem-line.ts +1 -1
- package/src/repo-config.ts +51 -2
- package/src/serve/hub-app/check-view.tsx +9 -9
- package/src/serve/hub-app/check-words.ts +1 -1
- package/src/serve/hub-app/checks-svc.ts +12 -8
- package/src/serve/hub-app/col-card.tsx +4 -4
- package/src/serve/hub-app/cols-svc.ts +92 -13
- package/src/serve/hub-app/comfy-svc.ts +12 -6
- package/src/serve/hub-app/comfy-view.tsx +3 -3
- package/src/serve/hub-app/floating.tsx +49 -0
- package/src/serve/hub-app/geometry-view.tsx +4 -4
- package/src/serve/hub-app/hash-route.ts +1 -1
- package/src/serve/hub-app/header-bar.tsx +44 -12
- package/src/serve/hub-app/health-svc.ts +3 -3
- package/src/serve/hub-app/help.tsx +10 -7
- package/src/serve/hub-app/home-st.ts +241 -0
- package/src/serve/hub-app/home-strip.css +70 -0
- package/src/serve/hub-app/home-strip.tsx +245 -0
- package/src/serve/hub-app/home.html +17 -0
- package/src/serve/hub-app/hub-app.tsx +201 -93
- package/src/serve/hub-app/hub-types.ts +0 -3
- package/src/serve/hub-app/hub.css +43 -23
- package/src/serve/hub-app/icons.tsx +14 -0
- package/src/serve/hub-app/index.html +3 -1
- package/src/serve/hub-app/keys.ts +25 -0
- package/src/serve/hub-app/layout-st.ts +55 -0
- package/src/serve/hub-app/live-svc.ts +12 -9
- package/src/serve/hub-app/machine-st.ts +270 -0
- package/src/serve/hub-app/media-svc.ts +63 -44
- package/src/serve/hub-app/media-view.tsx +20 -20
- package/src/serve/hub-app/next-run.ts +24 -30
- package/src/serve/hub-app/palette-st.ts +88 -0
- package/src/serve/hub-app/planned-st.ts +171 -0
- package/src/serve/hub-app/planned-view.tsx +109 -0
- package/src/serve/hub-app/planned.css +39 -0
- package/src/serve/hub-app/processes-svc.ts +63 -0
- package/src/serve/hub-app/project-list.css +97 -0
- package/src/serve/hub-app/project-list.tsx +167 -0
- package/src/serve/hub-app/projects-svc.ts +139 -17
- package/src/serve/hub-app/prose-hits-st.ts +3 -3
- package/src/serve/hub-app/prose-hits-view.tsx +6 -6
- package/src/serve/hub-app/{hub-st.ts → repo-st.ts} +155 -84
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/screen-cache.ts +74 -0
- package/src/serve/hub-app/test-view.tsx +4 -4
- package/src/serve/hub-app/tests-svc.ts +3 -3
- package/src/serve/hub-app/tests-view.tsx +10 -10
- package/src/serve/hub-app/timeline-chart.tsx +24 -12
- package/src/serve/hub-app/timeline-svc.ts +5 -4
- package/src/serve/hub-app/timeline-view.tsx +13 -13
- package/src/serve/hub-app/tree-row.tsx +2 -2
- package/src/serve/hub-app/tree-svc.ts +2 -2
- package/src/serve/hub-app/wikis-view.tsx +10 -6
- package/src/serve/hub-runtime.ts +542 -0
- package/src/serve/project-gutter-route.ts +21 -0
- package/src/serve/project-gutter.ts +3 -2
- package/src/serve/repo-processes.ts +89 -0
- package/src/serve/run-serve.ts +110 -402
- package/src/serve/serve-hub.ts +142 -44
- package/src/serve/stale-restart.ts +5 -0
- package/src/serve/{gate-trigger.ts → tracked-watch.ts} +4 -64
- package/src/serve/warm-cell.ts +95 -0
- package/src/shipkit-files.ts +114 -0
- package/src/shipkit.ts +2 -1
- package/src/suite/kept-timings.ts +1 -1
- package/src/wiki/graph-positions.ts +1 -1
- package/src/serve/health-schedule.ts +0 -102
- package/src/serve/hub-app/project-gutter.css +0 -65
- package/src/serve/hub-app/project-gutter.tsx +0 -128
|
@@ -4,27 +4,15 @@
|
|
|
4
4
|
// structure is read-only (kit.ts is code-reviewed); this edits WORKSPACE
|
|
5
5
|
// state: picks, alts, prompt overlays, meta sidecars, geometry rects.
|
|
6
6
|
import { makeAutoObservable, runInAction } from 'mobx'
|
|
7
|
-
import type {
|
|
7
|
+
import type { RepoSt } from './repo-st.ts'
|
|
8
8
|
import type { AssetDetail, CatRow, Rect, SideAsset } from './hub-types.ts'
|
|
9
9
|
|
|
10
|
-
const MEDIA = '/media'
|
|
11
10
|
// thumbs change in place when a pick changes → ~8s cache buckets; roll
|
|
12
11
|
// files are immutable (one name = one file forever), so their URLs carry
|
|
13
12
|
// no bust — an <audio> src that never changes never restarts playback
|
|
14
13
|
const V = (): number => Date.now() >> 13
|
|
15
|
-
export const rollUrl = (id: string, name: string): string => `${MEDIA}/img?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`
|
|
16
|
-
export const thumbUrl = (id: string): string => `${MEDIA}/api/thumb?id=${encodeURIComponent(id)}&t=${V()}`
|
|
17
14
|
export const isAud = (n: string): boolean => ['mp3', 'wav', 'ogg', 'm4a'].includes(String(n ?? '').split('.').pop() ?? '')
|
|
18
15
|
|
|
19
|
-
const j = async (u: string): Promise<any> => {
|
|
20
|
-
const r = await fetch(MEDIA + u)
|
|
21
|
-
if (!r.ok) throw new Error(`${u}: ${r.status}`)
|
|
22
|
-
return r.json()
|
|
23
|
-
}
|
|
24
|
-
const post = async (u: string, b?: unknown): Promise<any> => {
|
|
25
|
-
const r = await fetch(MEDIA + u, { method: 'POST', body: JSON.stringify(b ?? {}) })
|
|
26
|
-
return r.json()
|
|
27
|
-
}
|
|
28
16
|
|
|
29
17
|
export class MediaSvc {
|
|
30
18
|
assets: SideAsset[] = []
|
|
@@ -80,7 +68,7 @@ export class MediaSvc {
|
|
|
80
68
|
/** stage overlay root (the image box) — registered by AssetStage, DOM only */
|
|
81
69
|
overlayHost: HTMLElement | null = null
|
|
82
70
|
|
|
83
|
-
constructor(private st:
|
|
71
|
+
constructor(private st: RepoSt) {
|
|
84
72
|
makeAutoObservable<this, 'st' | 'hbTimer' | 'rectTimer' | 'promptTimer' | 'savedPrompt' | 'overlayHost'>(
|
|
85
73
|
this, { st: false, hbTimer: false, rectTimer: false, promptTimer: false, savedPrompt: false, overlayHost: false })
|
|
86
74
|
if (typeof localStorage !== 'undefined') {
|
|
@@ -102,15 +90,41 @@ export class MediaSvc {
|
|
|
102
90
|
}
|
|
103
91
|
|
|
104
92
|
start(): void {
|
|
93
|
+
if (this.hbTimer != null || this.dead != null) return
|
|
105
94
|
void this.heartbeat()
|
|
106
95
|
this.hbTimer = setInterval(() => void this.heartbeat(), 1500)
|
|
107
96
|
}
|
|
108
|
-
|
|
97
|
+
/** polling only: a pending prompt or rect save still lands */
|
|
98
|
+
stop(): void {
|
|
109
99
|
if (this.hbTimer != null) clearInterval(this.hbTimer)
|
|
100
|
+
this.hbTimer = null
|
|
101
|
+
}
|
|
102
|
+
dispose(): void {
|
|
103
|
+
this.stop()
|
|
110
104
|
if (this.rectTimer != null) clearTimeout(this.rectTimer)
|
|
111
105
|
if (this.promptTimer != null) clearTimeout(this.promptTimer)
|
|
112
106
|
}
|
|
113
107
|
|
|
108
|
+
/** the media API, under this repo's base */
|
|
109
|
+
private get api(): string {
|
|
110
|
+
return this.st.url('/media')
|
|
111
|
+
}
|
|
112
|
+
rollUrl(id: string, name: string): string {
|
|
113
|
+
return `${this.api}/img?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`
|
|
114
|
+
}
|
|
115
|
+
thumbUrl(id: string): string {
|
|
116
|
+
return `${this.api}/api/thumb?id=${encodeURIComponent(id)}&t=${V()}`
|
|
117
|
+
}
|
|
118
|
+
private async j(u: string): Promise<any> {
|
|
119
|
+
const r = await fetch(this.api + u)
|
|
120
|
+
if (!r.ok) throw new Error(`${u}: ${r.status}`)
|
|
121
|
+
return r.json()
|
|
122
|
+
}
|
|
123
|
+
private async post(u: string, b?: unknown): Promise<any> {
|
|
124
|
+
const r = await fetch(this.api + u, { method: 'POST', body: JSON.stringify(b ?? {}) })
|
|
125
|
+
return r.json()
|
|
126
|
+
}
|
|
127
|
+
|
|
114
128
|
get curId(): string | null {
|
|
115
129
|
return this.st.sel?.t === 'asset' ? this.st.sel.id : null
|
|
116
130
|
}
|
|
@@ -145,7 +159,7 @@ export class MediaSvc {
|
|
|
145
159
|
if (this.dead != null) return
|
|
146
160
|
let s: any
|
|
147
161
|
try {
|
|
148
|
-
const r = await fetch(
|
|
162
|
+
const r = await fetch(this.api + '/api/state')
|
|
149
163
|
if (r.status === 404) {
|
|
150
164
|
// the mount failed at boot — it will not come back without a
|
|
151
165
|
// restart; say so once, loudly, and stop wasting the wire
|
|
@@ -174,6 +188,7 @@ export class MediaSvc {
|
|
|
174
188
|
})
|
|
175
189
|
if (this.curId != null) await this.refreshRolls()
|
|
176
190
|
if (this.catMode != null) await this.fillCat()
|
|
191
|
+
this.st.cols.mediaListed()
|
|
177
192
|
this.st.afterFirstBeat()
|
|
178
193
|
}
|
|
179
194
|
|
|
@@ -184,7 +199,7 @@ export class MediaSvc {
|
|
|
184
199
|
this.statusLine = ''
|
|
185
200
|
const a = this.assets.find((x) => x.id === id)
|
|
186
201
|
if (a != null) this.openCats.add(a.cat)
|
|
187
|
-
const r = (await j(`/api/asset?id=${encodeURIComponent(id)}`)) as AssetDetail
|
|
202
|
+
const r = (await this.j(`/api/asset?id=${encodeURIComponent(id)}`)) as AssetDetail
|
|
188
203
|
if (this.curId !== id) return // selection changed mid-fetch — never clobber
|
|
189
204
|
runInAction(() => {
|
|
190
205
|
this.detail = r
|
|
@@ -206,7 +221,7 @@ export class MediaSvc {
|
|
|
206
221
|
async refreshRolls(): Promise<void> {
|
|
207
222
|
const id = this.curId
|
|
208
223
|
if (id == null) return
|
|
209
|
-
const r = (await j(`/api/asset?id=${encodeURIComponent(id)}`)) as AssetDetail
|
|
224
|
+
const r = (await this.j(`/api/asset?id=${encodeURIComponent(id)}`)) as AssetDetail
|
|
210
225
|
if (this.curId !== id) return
|
|
211
226
|
runInAction(() => {
|
|
212
227
|
if (this.detail == null) this.detail = r
|
|
@@ -223,23 +238,23 @@ export class MediaSvc {
|
|
|
223
238
|
|
|
224
239
|
async generate(): Promise<void> {
|
|
225
240
|
if (this.curId == null) return
|
|
226
|
-
await post('/api/generate', { id: this.curId, prompt: this.prompt, count: +this.n })
|
|
241
|
+
await this.post('/api/generate', { id: this.curId, prompt: this.prompt, count: +this.n })
|
|
227
242
|
void this.heartbeat()
|
|
228
243
|
}
|
|
229
244
|
async genCat(cat: string, count: number): Promise<void> {
|
|
230
|
-
const r = await post('/api/generate-cat', { cat, count })
|
|
245
|
+
const r = await this.post('/api/generate-cat', { cat, count })
|
|
231
246
|
if (r?.error != null) runInAction(() => { this.statusLine = r.error })
|
|
232
247
|
void this.heartbeat()
|
|
233
248
|
}
|
|
234
249
|
clearQueue(): void {
|
|
235
|
-
void post('/api/queue/clear')
|
|
250
|
+
void this.post('/api/queue/clear')
|
|
236
251
|
}
|
|
237
252
|
stopCurrent(): void {
|
|
238
|
-
void post('/api/interrupt')
|
|
253
|
+
void this.post('/api/interrupt')
|
|
239
254
|
}
|
|
240
255
|
|
|
241
256
|
async pick(name: string): Promise<void> {
|
|
242
|
-
const res = await post('/api/pick', { id: this.curId, name })
|
|
257
|
+
const res = await this.post('/api/pick', { id: this.curId, name })
|
|
243
258
|
runInAction(() => {
|
|
244
259
|
this.statusLine = res.error != null ? `ship FAILED: ${res.error}` : res.shipped != null ? `shipped → ${res.shipped}` : 'picked (pick.json)'
|
|
245
260
|
this.active = name
|
|
@@ -247,11 +262,11 @@ export class MediaSvc {
|
|
|
247
262
|
await this.refreshRolls()
|
|
248
263
|
}
|
|
249
264
|
async alt(name: string): Promise<void> {
|
|
250
|
-
await post('/api/alt', { id: this.curId, name })
|
|
265
|
+
await this.post('/api/alt', { id: this.curId, name })
|
|
251
266
|
await this.refreshRolls()
|
|
252
267
|
}
|
|
253
268
|
async discard(name: string): Promise<void> {
|
|
254
|
-
await post('/api/discard', { id: this.curId, name })
|
|
269
|
+
await this.post('/api/discard', { id: this.curId, name })
|
|
255
270
|
runInAction(() => {
|
|
256
271
|
if (this.active === name) this.active = null
|
|
257
272
|
// the ✕ sits on the hovered tile and its unmount fires no
|
|
@@ -267,7 +282,7 @@ export class MediaSvc {
|
|
|
267
282
|
const fd = new FormData()
|
|
268
283
|
fd.append('id', this.curId)
|
|
269
284
|
fd.append('file', f)
|
|
270
|
-
const r = await fetch(`${
|
|
285
|
+
const r = await fetch(`${this.api}/api/upload`, { method: 'POST', body: fd }).then((x) => x.json()).catch((e: Error) => ({ error: String(e) }))
|
|
271
286
|
if (r.error != null) runInAction(() => { this.statusLine = `upload FAIL: ${r.error}` })
|
|
272
287
|
}
|
|
273
288
|
await this.refreshRolls()
|
|
@@ -290,7 +305,7 @@ export class MediaSvc {
|
|
|
290
305
|
const text = this.prompt
|
|
291
306
|
if (text === this.savedPrompt) return // unchanged — not a save
|
|
292
307
|
void (async () => {
|
|
293
|
-
const r = await post('/api/prompt', { id, prompt: text })
|
|
308
|
+
const r = await this.post('/api/prompt', { id, prompt: text })
|
|
294
309
|
runInAction(() => {
|
|
295
310
|
if (this.curId !== id) return // selection moved while the POST flew — a late response must not corrupt the new asset's baseline
|
|
296
311
|
if (r.error != null) {
|
|
@@ -330,7 +345,7 @@ export class MediaSvc {
|
|
|
330
345
|
}
|
|
331
346
|
const id = this.curId
|
|
332
347
|
const name = this.active
|
|
333
|
-
const r = await j(`/api/rect?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`).catch(() => null)
|
|
348
|
+
const r = await this.j(`/api/rect?id=${encodeURIComponent(id)}&name=${encodeURIComponent(name)}`).catch(() => null)
|
|
334
349
|
if (this.curId !== id || this.active !== name) return // roll switched mid-fetch
|
|
335
350
|
runInAction(() => {
|
|
336
351
|
this.rect = r?.rect ?? null
|
|
@@ -349,7 +364,7 @@ export class MediaSvc {
|
|
|
349
364
|
if (this.rectTimer != null) clearTimeout(this.rectTimer)
|
|
350
365
|
this.rectTimer = setTimeout(() => {
|
|
351
366
|
void (async () => {
|
|
352
|
-
const r = await post('/api/rect', { id, name, rect })
|
|
367
|
+
const r = await this.post('/api/rect', { id, name, rect })
|
|
353
368
|
runInAction(() => {
|
|
354
369
|
if (this.curId !== id || this.active !== name) return // selection moved on — never adopt a stale rect
|
|
355
370
|
if (r.rect != null) this.rect = r.rect
|
|
@@ -360,7 +375,7 @@ export class MediaSvc {
|
|
|
360
375
|
}
|
|
361
376
|
async legsDefault(): Promise<void> {
|
|
362
377
|
if (this.curId == null || this.active == null) return
|
|
363
|
-
const r = await post('/api/legs-default', { id: this.curId, name: this.active })
|
|
378
|
+
const r = await this.post('/api/legs-default', { id: this.curId, name: this.active })
|
|
364
379
|
if (r.rect != null) runInAction(() => { this.rect = r.rect })
|
|
365
380
|
}
|
|
366
381
|
removeLegs(): void {
|
|
@@ -374,7 +389,7 @@ export class MediaSvc {
|
|
|
374
389
|
async fillCat(): Promise<void> {
|
|
375
390
|
const mode = this.catMode
|
|
376
391
|
if (mode == null) return
|
|
377
|
-
const r = await j(`/api/catrects?cat=${encodeURIComponent(mode)}`)
|
|
392
|
+
const r = await this.j(`/api/catrects?cat=${encodeURIComponent(mode)}`)
|
|
378
393
|
if (this.catMode !== mode) return // navigated away mid-fetch
|
|
379
394
|
runInAction(() => { this.catRows = r.rows ?? [] })
|
|
380
395
|
}
|
|
@@ -384,10 +399,10 @@ export class MediaSvc {
|
|
|
384
399
|
const nv = Math.max(0.1, v || row.rect.size)
|
|
385
400
|
if (nv === row.rect.size) return // blur without a change is not a save
|
|
386
401
|
row.rect.size = nv
|
|
387
|
-
await post('/api/rect', { id, name, rect: row.rect })
|
|
402
|
+
await this.post('/api/rect', { id, name, rect: row.rect })
|
|
388
403
|
}
|
|
389
404
|
async toggleFlip(id: string, name: string): Promise<void> {
|
|
390
|
-
await post('/api/flip', { id, name })
|
|
405
|
+
await this.post('/api/flip', { id, name })
|
|
391
406
|
await this.fillCat()
|
|
392
407
|
}
|
|
393
408
|
async applyAllSize(): Promise<void> {
|
|
@@ -397,7 +412,7 @@ export class MediaSvc {
|
|
|
397
412
|
// one action for all mutations (post-await writes would land outside
|
|
398
413
|
// it — mobx strict mode), then the POSTs
|
|
399
414
|
runInAction(() => { for (const row of rows) row.rect!.size = v })
|
|
400
|
-
for (const row of rows) await post('/api/rect', { id: row.id, name: row.name, rect: row.rect })
|
|
415
|
+
for (const row of rows) await this.post('/api/rect', { id: row.id, name: row.name, rect: row.rect })
|
|
401
416
|
runInAction(() => { this.allMsg = 'applied' })
|
|
402
417
|
setTimeout(() => runInAction(() => { this.allMsg = '' }), 1200)
|
|
403
418
|
await this.fillCat()
|
|
@@ -433,12 +448,16 @@ export class MediaSvc {
|
|
|
433
448
|
if (kind != null && typeof localStorage !== 'undefined') localStorage.setItem(`hub:tools:${kind}`, [...this.tools].join(','))
|
|
434
449
|
this.syncHash()
|
|
435
450
|
}
|
|
436
|
-
/** the hash carries the working state: #<assetId>?tools=a,b — shareable,
|
|
437
|
-
|
|
438
|
-
|
|
451
|
+
/** the hash carries the working state: #<assetId>?tools=a,b — shareable,
|
|
452
|
+
* restored on load. null while no asset is open */
|
|
453
|
+
get hash(): string | null {
|
|
454
|
+
if (this.curId == null) return null
|
|
439
455
|
const t = [...this.tools].join(',')
|
|
440
|
-
|
|
441
|
-
|
|
456
|
+
return `${this.curId}${t === '' ? '' : `?tools=${t}`}`
|
|
457
|
+
}
|
|
458
|
+
private syncHash(): void {
|
|
459
|
+
const h = this.hash
|
|
460
|
+
if (h != null) this.st.replaceHash(h)
|
|
442
461
|
}
|
|
443
462
|
/** tabs = kit-declared presets first, then autos for tools no tab covers.
|
|
444
463
|
* a tab swaps the enabled tool SET — the canvas underneath never moves */
|
|
@@ -493,12 +512,12 @@ export class MediaSvc {
|
|
|
493
512
|
}
|
|
494
513
|
async loadMeta(): Promise<void> {
|
|
495
514
|
this.jsonMsg = ''
|
|
496
|
-
const r = this.curId != null && this.active != null ? await post('/api/meta', { id: this.curId, name: this.active }) : { text: '' }
|
|
515
|
+
const r = this.curId != null && this.active != null ? await this.post('/api/meta', { id: this.curId, name: this.active }) : { text: '' }
|
|
497
516
|
runInAction(() => { this.jsonText = r.text ?? '' })
|
|
498
517
|
}
|
|
499
518
|
async saveMeta(): Promise<void> {
|
|
500
519
|
if (this.curId == null || this.active == null) return
|
|
501
|
-
const r = await post('/api/meta', { id: this.curId, name: this.active, text: this.jsonText })
|
|
520
|
+
const r = await this.post('/api/meta', { id: this.curId, name: this.active, text: this.jsonText })
|
|
502
521
|
runInAction(() => { this.jsonMsg = r.error != null ? `FAIL: ${r.error}` : 'saved' })
|
|
503
522
|
}
|
|
504
523
|
/** frozen per mount: a roll/asset switch remounts with a fresh api */
|
|
@@ -513,14 +532,14 @@ export class MediaSvc {
|
|
|
513
532
|
/** the PICKED roll's name (null before any pick) — editors that
|
|
514
533
|
* target shipped art can warn when the shown roll differs */
|
|
515
534
|
picked: this.detail?.rolls.find((x) => x.picked)?.name ?? null,
|
|
516
|
-
imgUrl: rollUrl(id, name),
|
|
535
|
+
imgUrl: this.rollUrl(id, name),
|
|
517
536
|
getMeta: async () => {
|
|
518
|
-
const r = await post('/api/meta', { id, name })
|
|
537
|
+
const r = await this.post('/api/meta', { id, name })
|
|
519
538
|
if (r.error != null) throw new Error(r.error)
|
|
520
539
|
return r.text ? JSON.parse(r.text) : null
|
|
521
540
|
},
|
|
522
541
|
saveMeta: async (data: unknown) => {
|
|
523
|
-
const r = await post('/api/meta', { id, name, text: JSON.stringify(data) })
|
|
542
|
+
const r = await this.post('/api/meta', { id, name, text: JSON.stringify(data) })
|
|
524
543
|
if (r.error != null) throw new Error(r.error)
|
|
525
544
|
},
|
|
526
545
|
// an element covering the DISPLAYED roll image, so an editor can
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { runInAction } from 'mobx'
|
|
6
6
|
import { observer } from 'mobx-react-lite'
|
|
7
7
|
import { useEffect, useRef, useState, type DragEvent, type ReactNode } from 'react'
|
|
8
|
-
import type {
|
|
9
|
-
import { isAud,
|
|
8
|
+
import type { RepoSt } from './repo-st.ts'
|
|
9
|
+
import { isAud, type MediaSvc } from './media-svc.ts'
|
|
10
10
|
import { GeometryLayer, Rectbar, measureBox, type Box } from './geometry-view.tsx'
|
|
11
11
|
import type { CatRow, CustomPanelRef } from './hub-types.ts'
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@ const TEX = 1024
|
|
|
14
14
|
const MIX_UNIT = 24
|
|
15
15
|
const MIX_GROUND = 44
|
|
16
16
|
|
|
17
|
-
export const MediaTopbar = observer(({ app }: { app:
|
|
17
|
+
export const MediaTopbar = observer(({ app }: { app: RepoSt }) => {
|
|
18
18
|
const m = app.media
|
|
19
19
|
const fileRef = useRef<HTMLInputElement>(null)
|
|
20
20
|
const taRef = useRef<HTMLTextAreaElement>(null)
|
|
@@ -71,7 +71,7 @@ export const MediaTopbar = observer(({ app }: { app: HubSt }) => {
|
|
|
71
71
|
)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
-
export const InfoChips = observer(({ app }: { app:
|
|
74
|
+
export const InfoChips = observer(({ app }: { app: RepoSt }) => {
|
|
75
75
|
const info = app.media.detail?.info
|
|
76
76
|
if (info == null) return null
|
|
77
77
|
const chips: { k: string; v: ReactNode; warn?: boolean }[] = [{ k: 'backend', v: info.backend }]
|
|
@@ -91,7 +91,7 @@ export const InfoChips = observer(({ app }: { app: HubSt }) => {
|
|
|
91
91
|
)
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
-
export const AssetStage = observer(({ app }: { app:
|
|
94
|
+
export const AssetStage = observer(({ app }: { app: RepoSt }) => {
|
|
95
95
|
const m = app.media
|
|
96
96
|
const stageRef = useRef<HTMLDivElement>(null)
|
|
97
97
|
const imgRef = useRef<HTMLImageElement>(null)
|
|
@@ -116,9 +116,9 @@ export const AssetStage = observer(({ app }: { app: HubSt }) => {
|
|
|
116
116
|
else if (isAud(shown)) content = (
|
|
117
117
|
// key = the roll identity: heartbeats re-render but never recreate
|
|
118
118
|
// the element, so playback survives; the src carries no cache-bust
|
|
119
|
-
<div className="stageaud" key={`${m.curId}/${shown}`}>🔊<audio controls src={rollUrl(m.curId!, shown)} /></div>
|
|
119
|
+
<div className="stageaud" key={`${m.curId}/${shown}`}>🔊<audio controls src={m.rollUrl(m.curId!, shown)} /></div>
|
|
120
120
|
)
|
|
121
|
-
else content = <img className="roll" src={rollUrl(m.curId!, shown)} ref={imgRef} onLoad={remeasure} />
|
|
121
|
+
else content = <img className="roll" src={m.rollUrl(m.curId!, shown)} ref={imgRef} onLoad={remeasure} />
|
|
122
122
|
const imgShown = shown != null && !isAud(shown)
|
|
123
123
|
const measured = imgShown && box != null
|
|
124
124
|
return (
|
|
@@ -137,7 +137,7 @@ export const AssetStage = observer(({ app }: { app: HubSt }) => {
|
|
|
137
137
|
)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
-
export const ToolsBar = observer(({ app }: { app:
|
|
140
|
+
export const ToolsBar = observer(({ app }: { app: RepoSt }) => {
|
|
141
141
|
const m = app.media
|
|
142
142
|
const d = m.detail
|
|
143
143
|
if (d == null || m.curId == null) return null
|
|
@@ -177,7 +177,7 @@ export const ToolsBar = observer(({ app }: { app: HubSt }) => {
|
|
|
177
177
|
|
|
178
178
|
// variants live in a THIN resizable side bar — the stage keeps the room.
|
|
179
179
|
// closed = a slim rail with the count; the drag handle resizes; drop = upload
|
|
180
|
-
export const VariantsBar = observer(({ app }: { app:
|
|
180
|
+
export const VariantsBar = observer(({ app }: { app: RepoSt }) => {
|
|
181
181
|
const m = app.media
|
|
182
182
|
const rolls = m.detail?.rolls ?? []
|
|
183
183
|
const genHere = m.curId != null && m.pending.includes(m.curId)
|
|
@@ -213,7 +213,7 @@ export const VariantsBar = observer(({ app }: { app: HubSt }) => {
|
|
|
213
213
|
onMouseEnter={() => { if (!isAud(x.name)) runInAction(() => { m.hover = x.name }) }}
|
|
214
214
|
onMouseLeave={() => runInAction(() => { m.hover = null })}
|
|
215
215
|
onClick={() => m.setActive(x.name)}>
|
|
216
|
-
{isAud(x.name) ? <>🔊<audio controls src={rollUrl(m.curId!, x.name)} /></> : <img src={rollUrl(m.curId!, x.name)} />}
|
|
216
|
+
{isAud(x.name) ? <>🔊<audio controls src={m.rollUrl(m.curId!, x.name)} /></> : <img src={m.rollUrl(m.curId!, x.name)} />}
|
|
217
217
|
{x.picked ? <span className="st">★</span> : null}
|
|
218
218
|
<div className="btns">
|
|
219
219
|
<button className="b-pick" onClick={(e) => { e.stopPropagation(); void m.pick(x.name) }}>pick</button>
|
|
@@ -228,7 +228,7 @@ export const VariantsBar = observer(({ app }: { app: HubSt }) => {
|
|
|
228
228
|
})
|
|
229
229
|
|
|
230
230
|
// ── category grid + size-true lineup ─────────────────────────────────────
|
|
231
|
-
const MixLineup = observer(({ rows }: { rows: CatRow[] }) => {
|
|
231
|
+
const MixLineup = observer(({ m, rows }: { m: MediaSvc; rows: CatRow[] }) => {
|
|
232
232
|
const items = rows.filter((r) => !r.icon && r.rect != null).sort((a, b) => a.rect!.size - b.rect!.size || a.label.localeCompare(b.label))
|
|
233
233
|
const boxH = 420
|
|
234
234
|
const baseY = boxH - MIX_GROUND
|
|
@@ -241,7 +241,7 @@ const MixLineup = observer(({ rows }: { rows: CatRow[] }) => {
|
|
|
241
241
|
cx += slot / 2
|
|
242
242
|
const fx = rect.flip === true ? TEX - rect.x - rect.w : rect.x
|
|
243
243
|
sprites.push(
|
|
244
|
-
<img key={it.id} className="mixsprite" src={thumbUrl(it.id)} style={{
|
|
244
|
+
<img key={it.id} className="mixsprite" src={m.thumbUrl(it.id)} style={{
|
|
245
245
|
width: TEX * ts, height: TEX * ts,
|
|
246
246
|
left: cx - (fx + rect.w / 2) * ts, top: baseY - (rect.y + rect.h / 2) * ts,
|
|
247
247
|
transform: rect.flip === true ? 'scaleX(-1)' : undefined, zIndex: Math.round(cx),
|
|
@@ -262,7 +262,7 @@ const MixLineup = observer(({ rows }: { rows: CatRow[] }) => {
|
|
|
262
262
|
)
|
|
263
263
|
})
|
|
264
264
|
|
|
265
|
-
export const CatGrid = observer(({ app }: { app:
|
|
265
|
+
export const CatGrid = observer(({ app }: { app: RepoSt }) => {
|
|
266
266
|
const m = app.media
|
|
267
267
|
const cat = m.catMode
|
|
268
268
|
if (cat == null) return null
|
|
@@ -288,9 +288,9 @@ export const CatGrid = observer(({ app }: { app: HubSt }) => {
|
|
|
288
288
|
// inner controls stopPropagation
|
|
289
289
|
<div className="card" key={row.id} onClick={() => app.selectAsset(row.id)}>
|
|
290
290
|
{isAud(row.name)
|
|
291
|
-
? <div className="aud">🔊<audio controls src={rollUrl(row.id, row.name)} onClick={(e) => e.stopPropagation()} />{m.pending.includes(row.id) ? <div className="spin" /> : null}</div>
|
|
291
|
+
? <div className="aud">🔊<audio controls src={m.rollUrl(row.id, row.name)} onClick={(e) => e.stopPropagation()} />{m.pending.includes(row.id) ? <div className="spin" /> : null}</div>
|
|
292
292
|
: <div className="th">
|
|
293
|
-
<img src={thumbUrl(row.id)} style={fl ? { transform: 'scaleX(-1)' } : undefined} />
|
|
293
|
+
<img src={m.thumbUrl(row.id)} style={fl ? { transform: 'scaleX(-1)' } : undefined} />
|
|
294
294
|
{row.rect != null ? <div className="ovmini" style={{
|
|
295
295
|
left: pc(fl ? TEX - row.rect.x - row.rect.w : row.rect.x), top: pc(row.rect.y),
|
|
296
296
|
width: pc(row.rect.w), height: pc(row.rect.h),
|
|
@@ -314,13 +314,13 @@ export const CatGrid = observer(({ app }: { app: HubSt }) => {
|
|
|
314
314
|
)
|
|
315
315
|
})}
|
|
316
316
|
</div>
|
|
317
|
-
<MixLineup rows={m.catRows} />
|
|
317
|
+
<MixLineup m={m} rows={m.catRows} />
|
|
318
318
|
</div>
|
|
319
319
|
)
|
|
320
320
|
})
|
|
321
321
|
|
|
322
322
|
// ── dock: the active tools' panels, side by side under the stage ─────────
|
|
323
|
-
const PanelHost = observer(({ app, c }: { app:
|
|
323
|
+
const PanelHost = observer(({ app, c }: { app: RepoSt; c: CustomPanelRef }) => {
|
|
324
324
|
const m = app.media
|
|
325
325
|
const ref = useRef<HTMLDivElement>(null)
|
|
326
326
|
const key = `${m.curId}|${m.active}|${c.id}`
|
|
@@ -337,7 +337,7 @@ const PanelHost = observer(({ app, c }: { app: HubSt; c: CustomPanelRef }) => {
|
|
|
337
337
|
void (async () => {
|
|
338
338
|
try {
|
|
339
339
|
// repo-defined editor module, served by the media API
|
|
340
|
-
mod = (await import(`/media/${c.url}`)) as Record<string, unknown>
|
|
340
|
+
mod = (await import(app.url(`/media/${c.url}`))) as Record<string, unknown>
|
|
341
341
|
if (typeof mod.mount !== 'function') throw new Error(`${c.url} must export mount(el, api)`)
|
|
342
342
|
if (!dead) await (mod.mount as (el: HTMLElement, api: unknown) => unknown)(host, api)
|
|
343
343
|
} catch (e) {
|
|
@@ -357,7 +357,7 @@ const PanelHost = observer(({ app, c }: { app: HubSt; c: CustomPanelRef }) => {
|
|
|
357
357
|
// floating panels sit ON the canvas (drag by the title bar), so toggling
|
|
358
358
|
// them never reflows the stage — canvas size/position survive tab switches
|
|
359
359
|
const FloatPanel = observer(({ app, id, title, dx, dy, anchor, children }: {
|
|
360
|
-
app:
|
|
360
|
+
app: RepoSt; id: string; title: string; dx: number; dy: number; anchor?: 'tl' | 'bl'; children: ReactNode
|
|
361
361
|
}) => {
|
|
362
362
|
const m = app.media
|
|
363
363
|
const ref = useRef<HTMLDivElement>(null)
|
|
@@ -408,7 +408,7 @@ const FloatPanel = observer(({ app, id, title, dx, dy, anchor, children }: {
|
|
|
408
408
|
)
|
|
409
409
|
})
|
|
410
410
|
|
|
411
|
-
export const FloatPanels = observer(({ app }: { app:
|
|
411
|
+
export const FloatPanels = observer(({ app }: { app: RepoSt }) => {
|
|
412
412
|
const m = app.media
|
|
413
413
|
const customs = (m.detail?.asset.custom ?? []).filter((c) => m.tools.has(`c:${c.id}`))
|
|
414
414
|
const showJson = m.tools.has('json')
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// WHEN A CHECK RUNS NEXT, as a true statement. Nothing runs a check
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
import type {
|
|
1
|
+
// WHEN A CHECK RUNS NEXT, as a true statement. Nothing runs a check nobody
|
|
2
|
+
// asked for but the plan (plan/planner.ts), so the answer is where the check
|
|
3
|
+
// runs (`when`) and its row of that plan. Pure, so every case is pinned by a
|
|
4
|
+
// test rather than a screenshot.
|
|
5
|
+
import type { PlanRow } from '../../plan/planner.ts'
|
|
6
|
+
import type { CheckFacts } from './hub-types.ts'
|
|
6
7
|
|
|
7
8
|
export type NextRun = {
|
|
8
9
|
/** the value the page shows */
|
|
@@ -37,32 +38,25 @@ export function spanWords(ms: number): string {
|
|
|
37
38
|
return `${Math.round(ms / 1000)}s`
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
export type NextRunFacts = Pick<CheckFacts, 'when' | 'whenReason'>
|
|
41
|
+
export type NextRunFacts = Pick<CheckFacts, 'when' | 'whenReason'>
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
43
|
+
/** a plan row in a few words: why it is due, or what makes it due */
|
|
44
|
+
export function planWords(row: PlanRow, now: number): { words: string; at: number | null } {
|
|
45
|
+
if (row.state === 'running') return { words: 'running now', at: row.dueAt }
|
|
46
|
+
if (row.state === 'due') return { words: row.reason === 'asked' ? 'due now, asked' : row.reason === 'changed' ? 'due now, a save moved it' : 'due now', at: row.dueAt }
|
|
47
|
+
if (row.state === 'late') return { words: 'late, waits for the repo to be active', at: row.dueAt }
|
|
48
|
+
if (row.state === 'next' && row.dueAt != null) return { words: `in ${aheadWords(row.dueAt - now)}`, at: row.dueAt }
|
|
49
|
+
return { words: 'on the next save that moves what it reads', at: null }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function nextRun(u: NextRunFacts, row: PlanRow | null, now: number): NextRun {
|
|
47
53
|
if (u.when === 'off')
|
|
48
54
|
return { words: u.whenReason != null && u.whenReason !== '' ? `never: ${u.whenReason}` : 'never', at: null, help: 'it is off: nothing runs it until the switch below puts it somewhere' }
|
|
49
|
-
if (
|
|
50
|
-
return { words: '
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const at = pass + k * s.everyMs
|
|
57
|
-
return {
|
|
58
|
-
words: `in ${aheadWords(at - now)}`,
|
|
59
|
-
at,
|
|
60
|
-
help: `the health pass runs every ${spanWords(s.everyMs)}, and uses this check's cached answer until it runs out. The first pass after that runs it again`,
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
words: pass <= now ? 'due now' : `in ${aheadWords(pass - now)}`,
|
|
65
|
-
at: pass,
|
|
66
|
-
help: `the next health pass: the hub runs it every ${spanWords(s.everyMs)} while it serves, and at startup when the last one is older than that`,
|
|
67
|
-
}
|
|
55
|
+
if (u.when === 'release')
|
|
56
|
+
return { words: 'before a release', at: null, help: '`shipkit ci --full` and the release path run it. The plan and the quick gate you run while working do not' }
|
|
57
|
+
if (row == null)
|
|
58
|
+
return { words: u.when === 'always' ? 'next shipkit ci' : 'next shipkit check', at: null, help: 'no plan covers it here, so it runs when a terminal or a button runs it' }
|
|
59
|
+
const w = planWords(row, now)
|
|
60
|
+
const inactive = row.state === 'late' || !row.active ? ` While the repo is inactive: ${row.inactive}.` : ''
|
|
61
|
+
return { words: w.words, at: w.at, help: `planned by: ${row.rule}.${inactive}${row.error != null ? ` Error: ${row.error}.` : ''} A shipkit ci or check from a terminal runs it too` }
|
|
68
62
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// the one picker of the page (cmd+k, keys.ts), a node of the root: the
|
|
2
|
+
// home's places (home, planned) first, then the shown repo's own jump
|
|
3
|
+
// targets, then every other house repo as a place to go. Another repo's
|
|
4
|
+
// resources are never listed: a jump to a repo selects it in place, and its
|
|
5
|
+
// own targets are one more cmd+k away.
|
|
6
|
+
import { makeAutoObservable } from 'mobx'
|
|
7
|
+
import type { HomeSt } from './home-st.ts'
|
|
8
|
+
import type { Jump } from './repo-st.ts'
|
|
9
|
+
|
|
10
|
+
/** how many rows the picker shows */
|
|
11
|
+
export const PAL_ROWS = 12
|
|
12
|
+
|
|
13
|
+
export class PaletteSt {
|
|
14
|
+
open = false
|
|
15
|
+
query = ''
|
|
16
|
+
idx = 0
|
|
17
|
+
|
|
18
|
+
constructor(private st: HomeSt) {
|
|
19
|
+
makeAutoObservable<this, 'st'>(this, { st: false })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** the home's places, first in the list: a hub of its own has none */
|
|
23
|
+
get places(): Jump[] {
|
|
24
|
+
const st = this.st
|
|
25
|
+
if (st.machine == null) return []
|
|
26
|
+
return [
|
|
27
|
+
{ id: 'place:home', label: 'home', section: 'place', run: () => st.navigate('/') },
|
|
28
|
+
{ id: 'place:planned', label: 'planned', section: 'place', run: () => st.navigate('/planned') },
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** every other house repo, as a place to go */
|
|
33
|
+
get others(): Jump[] {
|
|
34
|
+
const st = this.st
|
|
35
|
+
if (st.machine == null) return []
|
|
36
|
+
return st.projects.list.filter((p) => p.name !== st.selected).map((p) => ({ id: `repo:${p.name}`, label: p.name, section: 'repo', run: () => st.select(p.name) }))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get jumps(): Jump[] {
|
|
40
|
+
return [...this.places, ...(this.st.shown?.jumps ?? []), ...this.others]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** ranked: prefix matches first, then substring. With no query, home and
|
|
44
|
+
* planned, then the shown repo's first targets, then every other repo,
|
|
45
|
+
* so a jump elsewhere is always in reach */
|
|
46
|
+
get results(): Jump[] {
|
|
47
|
+
const q = this.query.trim().toLowerCase()
|
|
48
|
+
if (q === '') {
|
|
49
|
+
const own = this.st.shown?.jumps ?? []
|
|
50
|
+
const places = this.places
|
|
51
|
+
const others = this.others
|
|
52
|
+
return [...places, ...own.slice(0, Math.max(4, PAL_ROWS - places.length - others.length)), ...others]
|
|
53
|
+
}
|
|
54
|
+
const pre: Jump[] = []
|
|
55
|
+
const sub: Jump[] = []
|
|
56
|
+
for (const j of this.jumps) {
|
|
57
|
+
const l = j.label.toLowerCase()
|
|
58
|
+
if (l.startsWith(q)) pre.push(j)
|
|
59
|
+
else if (l.includes(q)) sub.push(j)
|
|
60
|
+
}
|
|
61
|
+
return [...pre, ...sub].slice(0, PAL_ROWS * 2)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
toggle(): void {
|
|
65
|
+
this.open = !this.open
|
|
66
|
+
this.query = ''
|
|
67
|
+
this.idx = 0
|
|
68
|
+
}
|
|
69
|
+
close(): void {
|
|
70
|
+
this.open = false
|
|
71
|
+
}
|
|
72
|
+
setQuery(v: string): void {
|
|
73
|
+
this.query = v
|
|
74
|
+
this.idx = 0
|
|
75
|
+
}
|
|
76
|
+
setIdx(n: number): void {
|
|
77
|
+
this.idx = n
|
|
78
|
+
}
|
|
79
|
+
move(d: number): void {
|
|
80
|
+
const n = this.results.length
|
|
81
|
+
if (n > 0) this.idx = (this.idx + d + n) % n
|
|
82
|
+
}
|
|
83
|
+
run(j: Jump | undefined = this.results[this.idx]): void {
|
|
84
|
+
if (j == null) return
|
|
85
|
+
this.open = false
|
|
86
|
+
j.run()
|
|
87
|
+
}
|
|
88
|
+
}
|