rvlib-shipkit 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -2
- package/package.json +2 -2
- package/src/assets/kits/identity-kit.ts +5 -2
- package/src/cli/cmd_Commands.ts +30 -5
- package/src/cli/cmd_FleetPush.ts +47 -0
- package/src/cli/cmd_GlobalProcesses.ts +5 -5
- package/src/cli/cmd_Review.ts +52 -11
- package/src/cli/cmd_Sound.ts +209 -0
- package/src/cli/cmd_Trailer.ts +11 -3
- package/src/cli/family-pages.ts +49 -0
- package/src/cli/kit/builtins.ts +1 -0
- package/src/cli/kit/verb.ts +11 -3
- package/src/cli/registry.ts +137 -0
- package/src/cli.ts +47 -133
- package/src/core/app-icon.ts +10 -6
- package/src/core/last-good.ts +217 -0
- package/src/core/load-probe.ts +71 -0
- package/src/core/reexec.ts +5 -3
- package/src/fleet/push-fleet.ts +80 -0
- package/src/home/home-routes.ts +12 -2
- package/src/home/lib-watch.ts +110 -20
- package/src/home/process-rows.ts +4 -2
- package/src/home/run-daemon.ts +1 -1
- package/src/repo-config.ts +3 -0
- package/src/review/format.ts +11 -3
- package/src/review/model.ts +66 -5
- package/src/review/paste.ts +30 -0
- package/src/review/project.ts +20 -4
- package/src/review/routes.ts +22 -4
- package/src/review/store.ts +15 -6
- package/src/review/unit.ts +19 -1
- package/src/scaffold/files/agent-index.md +2 -2
- package/src/scaffold/project-scaffold.ts +2 -2
- package/src/serve/hub-app/activity-st.ts +31 -0
- package/src/serve/hub-app/activity-view.tsx +9 -0
- package/src/serve/hub-app/activity.css +6 -0
- package/src/serve/hub-app/admin-svc.ts +1 -1
- package/src/serve/hub-app/admin-view.tsx +1 -1
- package/src/serve/hub-app/hash-route.ts +12 -4
- package/src/serve/hub-app/home-st.ts +7 -1
- package/src/serve/hub-app/home-strip.tsx +3 -1
- package/src/serve/hub-app/hub.css +38 -1
- package/src/serve/hub-app/icons.tsx +6 -0
- package/src/serve/hub-app/layout-st.ts +2 -2
- package/src/serve/hub-app/machine-st.ts +1 -1
- package/src/serve/hub-app/md.ts +32 -0
- package/src/serve/hub-app/project-list.css +9 -7
- package/src/serve/hub-app/project-list.tsx +5 -2
- package/src/serve/hub-app/projects-svc.ts +3 -2
- package/src/serve/hub-app/repo-st.ts +22 -7
- package/src/serve/hub-app/review/clip-st.ts +57 -7
- package/src/serve/hub-app/review/kind-view.tsx +78 -9
- package/src/serve/hub-app/review/review-answer.tsx +67 -58
- package/src/serve/hub-app/review/review-page.tsx +17 -15
- package/src/serve/hub-app/review/review-st.ts +191 -20
- package/src/serve/hub-app/review/review-views.tsx +154 -9
- package/src/serve/hub-app/review.css +73 -13
- package/src/serve/hub-app/wiki-st.ts +190 -0
- package/src/serve/hub-app/wikis-view.tsx +134 -5
- package/src/serve/hub-runtime.ts +4 -4
- package/src/serve/project-gutter-route.ts +25 -1
- package/src/serve/project-gutter.ts +1 -1
- package/src/serve/run-serve.ts +25 -0
- package/src/serve/serve-hub.ts +15 -1
- package/src/serve/stale-restart.ts +19 -2
- package/src/shot/client.ts +17 -5
- package/src/shot/shooter.ts +19 -3
- package/src/shot/shot-ask.ts +9 -1
- package/src/shot/stepped-clip.ts +53 -0
- package/src/trailer/adapter.ts +33 -4
- package/src/trailer/build.ts +182 -33
- package/src/trailer/ffmpeg.ts +21 -1
- package/src/trailer/fonts.ts +55 -0
- package/src/trailer/footage.ts +4 -4
- package/src/trailer/grammar.ts +429 -67
- package/src/trailer/graph.ts +130 -56
- package/src/trailer/look.ts +3 -0
- package/src/trailer/motion.ts +207 -0
- package/src/trailer/plan.ts +2 -2
- package/src/trailer/prune.ts +13 -2
- package/src/trailer/score/compose.ts +18 -2
- package/src/trailer/score/gm-render.ts +43 -0
- package/src/trailer/score/gm.ts +90 -0
- package/src/trailer/score/midi.ts +143 -0
- package/src/trailer/score/pattern.ts +13 -6
- package/src/trailer/score/score.ts +28 -14
- package/src/trailer/score/sound.ts +12 -1
- package/src/trailer/score/synth.ts +24 -2
- package/src/trailer/sound/audio.ts +148 -0
- package/src/trailer/sound/brand.ts +42 -0
- package/src/trailer/sound/comfy.ts +38 -0
- package/src/trailer/sound/eleven.ts +72 -0
- package/src/trailer/sound/fluid.ts +71 -0
- package/src/trailer/sound/gen.ts +139 -0
- package/src/trailer/sound/index.ts +60 -0
- package/src/trailer/sound/presets.ts +69 -0
- package/src/trailer/sound/run.ts +189 -0
- package/src/trailer/titles.ts +451 -235
- package/src/trailer/url.ts +18 -3
- package/src/trailer/vocab.ts +12 -1
- package/src/trailer/words.ts +29 -8
- package/src/wiki/graph-mount.ts +3 -0
- package/src/wiki/graph-page.ts +45 -9
- package/src/wiki/graph-pages.ts +128 -0
package/src/review/store.ts
CHANGED
|
@@ -90,6 +90,8 @@ export interface ViewInput {
|
|
|
90
90
|
after?: string
|
|
91
91
|
/** a list of small images shown as a wrapping grid, each with its label; never the first view */
|
|
92
92
|
gallery?: { file: string; label: string }[]
|
|
93
|
+
/** a line on this view alone: an option's description on a choice card */
|
|
94
|
+
note?: string
|
|
93
95
|
}
|
|
94
96
|
|
|
95
97
|
export interface AddInput {
|
|
@@ -123,6 +125,8 @@ export interface AddInput {
|
|
|
123
125
|
light?: 'review'
|
|
124
126
|
/** a draft card on a live shot: its image a live frame, no verdict until it is ready */
|
|
125
127
|
wip?: { live: string }
|
|
128
|
+
/** every view is an option he picks one of */
|
|
129
|
+
choice?: boolean
|
|
126
130
|
now?: Date
|
|
127
131
|
}
|
|
128
132
|
|
|
@@ -228,12 +232,13 @@ function soundLength(file: string): number {
|
|
|
228
232
|
/** the waveform a sound view shows, one size for every sound so a pair reads side by side */
|
|
229
233
|
const WAVE_SIZE = { w: 1600, h: 240 }
|
|
230
234
|
|
|
231
|
-
/** view `n`'s waveforms, drawn into `folder` beside their place and renamed
|
|
235
|
+
/** view `n`'s waveforms, drawn into `folder` beside their place and renamed: each column's peak on a square root scale,
|
|
236
|
+
* so a voice's quiet syllables show; drawn white on transparent, the page fills it with its own colour (a mask) */
|
|
232
237
|
async function writeWaves(v: Loaded, n: number, folder: string): Promise<void> {
|
|
233
238
|
if (!v.audio) return
|
|
234
239
|
const draw = async (from: string, name: string): Promise<void> => {
|
|
235
240
|
const to = join(folder, `${prefix(n)}${name}.png`)
|
|
236
|
-
await ffmpeg(['-i', from, '-filter_complex', `showwavespic=s=${WAVE_SIZE.w}x${WAVE_SIZE.h}:split_channels=0:colors=
|
|
241
|
+
await ffmpeg(['-i', from, '-filter_complex', `showwavespic=s=${WAVE_SIZE.w}x${WAVE_SIZE.h}:split_channels=0:scale=sqrt:filter=peak:colors=white`, '-frames:v', '1', '-f', 'image2', '-c:v', 'png', `${to}.tmp`])
|
|
237
242
|
renameSync(`${to}.tmp`, to)
|
|
238
243
|
}
|
|
239
244
|
await draw(v.audio.from, 'wave')
|
|
@@ -245,10 +250,10 @@ const prefix = (n: number): string => (n > 0 ? `v${n}-` : '')
|
|
|
245
250
|
/** the file name of tile `k` of gallery view `n` */
|
|
246
251
|
const tileName = (n: number, k: number, path: string): string => `v${n}-g${k}${extname(path).toLowerCase()}`
|
|
247
252
|
|
|
248
|
-
function viewOf(v: Loaded, n: number, marks: readonly ReviewMark[] = []): ReviewView {
|
|
253
|
+
function viewOf(v: Loaded, n: number, marks: readonly ReviewMark[] = [], note?: string): ReviewView {
|
|
249
254
|
if (v.tiles) return { label: v.label, gallery: v.tiles.map((t, k): GalleryTile => ({ file: tileName(n, k, t.path), label: t.label, size: t.size })) }
|
|
250
255
|
const audio = v.audio ? { audio: { duration: v.audio.duration, wave: `${prefix(n)}wave.png`, ...(v.audio.before ? { waveBefore: `${prefix(n)}before-wave.png` } : {}) } } : {}
|
|
251
|
-
return { label: v.label, files: filesOf(v, n), size: v.size, ...clipOf(v, n), ...audio, ...(marks.length > 0 ? { marks: [...marks] } : {}) }
|
|
256
|
+
return { label: v.label, files: filesOf(v, n), size: v.size, ...clipOf(v, n), ...audio, ...(marks.length > 0 ? { marks: [...marks] } : {}), ...(note?.trim() ? { note: note.trim() } : {}) }
|
|
252
257
|
}
|
|
253
258
|
|
|
254
259
|
/** the file names of view `n` in the folder: `image.png` on the first, `v2-before.png` on a later one */
|
|
@@ -295,6 +300,8 @@ export async function addItem(dir: string, input: AddInput): Promise<Item> {
|
|
|
295
300
|
if (given.length === 0) throw new Error('give --image, --video or --audio, or --before and --after, or --view "<label>" <image>')
|
|
296
301
|
for (const [i, v] of given.entries()) if (i > 0 && !v.label.trim()) throw new Error('--view: every view after the first needs its label')
|
|
297
302
|
if (given[0]?.gallery) throw new Error('a gallery is never the first view: give the card an --image (or a pair, or a clip) first')
|
|
303
|
+
if (input.choice && given.length < 2) throw new Error('--choice: a choice has two options or more, one --view each')
|
|
304
|
+
for (const v of given) if (v.note !== undefined && v.gallery) throw new Error(`--view "${v.label}": a note goes on an image, a clip or a sound, never a gallery`)
|
|
298
305
|
const loaded = await Promise.all(given.map((v, i) => loadView(v, i === 0 ? 'the first view' : `--view "${v.label}"`)))
|
|
299
306
|
if (input.supersedes !== undefined && !existsSync(join(dir, input.supersedes, 'item.json'))) throw new Error(`--supersedes ${input.supersedes}: no such card in ${dir}`)
|
|
300
307
|
if (input.supersedes !== undefined && readItem(dir, input.supersedes)?.item.origin.by !== 'agent')
|
|
@@ -347,13 +354,14 @@ export async function addItem(dir: string, input: AddInput): Promise<Item> {
|
|
|
347
354
|
what: input.what.trim(),
|
|
348
355
|
...(topic ? { topic } : {}),
|
|
349
356
|
...(input.commit ? { commit: input.commit } : {}),
|
|
350
|
-
views: loaded.map((v, n) => viewOf(v, n, input.marks?.[n])),
|
|
357
|
+
views: loaded.map((v, n) => viewOf(v, n, input.marks?.[n], given[n]?.note)),
|
|
351
358
|
...(input.supersedes ? { supersedes: input.supersedes } : {}),
|
|
352
359
|
...(answers.length > 0 ? { answers } : {}),
|
|
353
360
|
...(input.fresh && input.why ? { why: input.why.trim() } : {}),
|
|
354
361
|
...(input.supersedes && input.keeps?.trim() ? { keeps: input.keeps.trim() } : {}),
|
|
355
362
|
...(input.light ? { light: input.light } : {}),
|
|
356
363
|
...(input.wip ? { wip: { live: input.wip.live, frame: 1 } } : {}),
|
|
364
|
+
...(input.choice ? { choice: true as const } : {}),
|
|
357
365
|
}
|
|
358
366
|
const problems = itemProblems(item)
|
|
359
367
|
if (problems.length > 0) throw new Error(problems.join('; '))
|
|
@@ -383,6 +391,7 @@ export async function addView(dir: string, id: string, v: ViewInput, o: { marks?
|
|
|
383
391
|
const x = readItem(dir, id)
|
|
384
392
|
if (!x) throw new Error(`no card ${id}`)
|
|
385
393
|
if (verdictOf(x)) throw new Error(`${id} has his answer already: a new version passes --supersedes ${id}`)
|
|
394
|
+
if (v.note !== undefined && v.gallery) throw new Error(`"${v.label}": a note goes on an image, a clip or a sound, never a gallery`)
|
|
386
395
|
if (!v.label.trim()) throw new Error('add-view: the view needs its label')
|
|
387
396
|
const loaded = await loadView(v, `the view "${v.label}"`)
|
|
388
397
|
const n = x.item.views.length
|
|
@@ -391,7 +400,7 @@ export async function addView(dir: string, id: string, v: ViewInput, o: { marks?
|
|
|
391
400
|
copyFiles(loaded, n, folder)
|
|
392
401
|
await writePoster(loaded, n, folder)
|
|
393
402
|
await writeWaves(loaded, n, folder)
|
|
394
|
-
const item: Item = { ...x.item, views: [...x.item.views, viewOf(loaded, n, o.marks)], ...(x.item.light || o.light ? { light: 'review' as const } : {}) }
|
|
403
|
+
const item: Item = { ...x.item, views: [...x.item.views, viewOf(loaded, n, o.marks, v.note)], ...(x.item.light || o.light ? { light: 'review' as const } : {}) }
|
|
395
404
|
writeJson(join(folder, 'item.json'), item)
|
|
396
405
|
return item
|
|
397
406
|
}
|
package/src/review/unit.ts
CHANGED
|
@@ -11,10 +11,26 @@ export interface Fact {
|
|
|
11
11
|
value: string
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
/** a link the hub opens
|
|
14
|
+
/** a link the hub opens. `host`: a page of the tool that frames the hub (pkm-multi's admin), its url relative to it:
|
|
15
|
+
* in a frame it is posted to the host (`{ shipkit: 'open', url }`), alone it opens in a new tab */
|
|
15
16
|
export interface Link {
|
|
16
17
|
label: string
|
|
17
18
|
url: string
|
|
19
|
+
host?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** how a part's change reads: new, changed, gone or the same */
|
|
23
|
+
export type PartTone = 'new' | 'changed' | 'gone' | 'same'
|
|
24
|
+
|
|
25
|
+
/** one part of a subject the hub draws as a card of its own: a message of a post, a page of a script. `icon` an image
|
|
26
|
+
* url (a data url carries a logo), `media` a repo relative file the hub serves beside the text, `badge` its change */
|
|
27
|
+
export interface ViewPart {
|
|
28
|
+
label: string
|
|
29
|
+
detail?: string
|
|
30
|
+
icon?: string
|
|
31
|
+
badge?: { text: string; tone: PartTone }
|
|
32
|
+
media?: string
|
|
33
|
+
text: ViewText
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
/** a text the hub shows, or a change of one shown as a diff */
|
|
@@ -30,6 +46,8 @@ export interface KindDef<S extends Subject = Subject> extends SpecKind {
|
|
|
30
46
|
/** a url the hub opens in a frame: the subject live */
|
|
31
47
|
live?(x: ReviewItem): string | null
|
|
32
48
|
text?(x: ReviewItem): ViewText | null
|
|
49
|
+
/** the subject in parts, each a card: a post's messages, with their logo, media and change */
|
|
50
|
+
parts?(x: ReviewItem): ViewPart[]
|
|
33
51
|
/** the `sk review add` flags this kind reads, each with its help line: `--where <zone>:<x>,<y>` */
|
|
34
52
|
flags?: Record<string, string>
|
|
35
53
|
/** the subject (and its topic) a card of this kind gets from those flags, every flag as the list of its values;
|
|
@@ -18,8 +18,8 @@ Read the matching page before touching a surface:
|
|
|
18
18
|
|
|
19
19
|
| read when touching… | page |
|
|
20
20
|
|---|---|
|
|
21
|
-
| any source file |
|
|
22
|
-
| recent changes and why |
|
|
21
|
+
| any source file | → agent/coding.md |
|
|
22
|
+
| recent changes and why | → agent/changelog.md |{{CLI_ROW}}
|
|
23
23
|
|
|
24
24
|
Grow this table: a subsystem, a service, a runbook each earn their own page,
|
|
25
25
|
cross-linked from here.
|
|
@@ -236,7 +236,7 @@ export function installedLib(root: string): { version?: string; exports?: Record
|
|
|
236
236
|
/** the agent index lines that load the essentials page and list the full one */
|
|
237
237
|
const cliIndexVars = (name: string, cli: boolean): { CLI_INCLUDE: string; CLI_ROW: string } => ({
|
|
238
238
|
CLI_INCLUDE: cli ? `\n@${name}-essentials.md` : '',
|
|
239
|
-
CLI_ROW: cli ? `\n| every command of the project CLI |
|
|
239
|
+
CLI_ROW: cli ? `\n| every command of the project CLI | → ${cliPages(name).all} |` : '',
|
|
240
240
|
})
|
|
241
241
|
|
|
242
242
|
/** an ADOPTED repo asking for the project CLI (`sk start <name> --cli`): the
|
|
@@ -257,7 +257,7 @@ export function adoptCli(root: string, opts: { name: string; print?: (line: stri
|
|
|
257
257
|
addScripts(root, written, p, opts.name, false)
|
|
258
258
|
const index = join(root, 'agent/index.md')
|
|
259
259
|
const include = `@${opts.name}-essentials.md`
|
|
260
|
-
if (existsSync(index) && !readFileSync(index, 'utf8').split('\n').includes(include)) p(`next: in agent/index.md, load the everyday commands with the line ${include} and list
|
|
260
|
+
if (existsSync(index) && !readFileSync(index, 'utf8').split('\n').includes(include)) p(`next: in agent/index.md, load the everyday commands with the line ${include} and list → ${cliPages(opts.name).all} in its table`)
|
|
261
261
|
const deps = { ...(pkg as { dependencies?: object }).dependencies, ...(pkg as { devDependencies?: object }).devDependencies }
|
|
262
262
|
p(`ok: project CLI written: ${written.join(', ')} — run \`bun run cli\``)
|
|
263
263
|
if (!('rvlib-shipkit' in deps)) p('next: bun add -d rvlib-shipkit (the CLI imports rvlib-shipkit/cli-kit)')
|
|
@@ -10,6 +10,11 @@ import { clampCap, DEFAULT_CAP, DEFAULT_MINUTES, idleRow, WINDOW_MINUTES, type A
|
|
|
10
10
|
export { ageShort, CAP_MAX, CAP_MIN, clampCap, DEFAULT_CAP, dirtWords, filesWords, upstreamWords, windowWords } from '../../home/activity-shape.ts'
|
|
11
11
|
import type { HomeSt } from './home-st.ts'
|
|
12
12
|
import { ownTrouble, reach } from './reach.ts'
|
|
13
|
+
import type { PushResult } from '../../fleet/push-fleet.ts'
|
|
14
|
+
|
|
15
|
+
/** the repos holding commits no remote has: ahead of their upstream, or on a branch with none */
|
|
16
|
+
export const unpushedCount = (rows: Pick<ActivityRow, 'ahead' | 'upstream' | 'branch'>[]): number =>
|
|
17
|
+
rows.filter((r) => (r.ahead ?? 0) > 0 || (r.upstream == null && r.branch != null)).length
|
|
13
18
|
|
|
14
19
|
export const ACTIVITY_POLL_MS = 60_000
|
|
15
20
|
const MINUTES_KEY = 'home:activity:minutes'
|
|
@@ -38,6 +43,10 @@ function keep(key: string, n: number): void {
|
|
|
38
43
|
export class ActivitySt {
|
|
39
44
|
data: ActivityAnswer | null = null
|
|
40
45
|
error = ''
|
|
46
|
+
/** the push button is running */
|
|
47
|
+
pushing = false
|
|
48
|
+
/** what the last push said, one line a repo that needed it */
|
|
49
|
+
pushed: PushResult[] | null = null
|
|
41
50
|
minutes: number = keptMinutes()
|
|
42
51
|
/** the most commits a repo shows before a click shows the rest */
|
|
43
52
|
cap: number = clampCap(kept(CAP_KEY) ?? DEFAULT_CAP)
|
|
@@ -118,6 +127,28 @@ export class ActivitySt {
|
|
|
118
127
|
this.st.select(name)
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
/** repos holding commits no remote has */
|
|
131
|
+
get unpushed(): number {
|
|
132
|
+
return unpushedCount(this.data?.rows ?? [])
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** push every house repo that needs it, through the daemon (fleet/push-fleet.ts), then read git again */
|
|
136
|
+
async pushAll(): Promise<void> {
|
|
137
|
+
if (this.pushing) return
|
|
138
|
+
runInAction(() => { this.pushing = true; this.pushed = null })
|
|
139
|
+
try {
|
|
140
|
+
const r = await reach('/api/home/push-all', { method: 'POST', signal: AbortSignal.timeout(180_000) })
|
|
141
|
+
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
142
|
+
const res = (await r.json()) as PushResult[]
|
|
143
|
+
runInAction(() => { this.pushed = res.filter((x) => x.action !== 'none'); this.error = '' })
|
|
144
|
+
} catch (e) {
|
|
145
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.machine, (m) => `push every repo: ${m}`) })
|
|
146
|
+
} finally {
|
|
147
|
+
runInAction(() => { this.pushing = false })
|
|
148
|
+
void this.poll(true)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
121
152
|
/** the refresh button: a new git pass now, whatever the countdown says */
|
|
122
153
|
refresh(): void {
|
|
123
154
|
if (!this.loading) void this.poll(true)
|
|
@@ -189,6 +189,12 @@ export const ActivityView = observer(({ app }: { app: HomeSt }) => {
|
|
|
189
189
|
<span className="acdim">commits</span>
|
|
190
190
|
</label>
|
|
191
191
|
<Next app={app} />
|
|
192
|
+
{st.unpushed > 0 || st.pushing ? (
|
|
193
|
+
<button type="button" className="acpill acpush" disabled={st.pushing} onClick={() => void st.pushAll()}
|
|
194
|
+
title="git push in every house repo with unpushed commits; never a force, a repo that fell behind waits for a pull (sk fleet push does the same)">
|
|
195
|
+
{st.pushing ? 'pushing…' : `↑ push ${st.unpushed} unpushed`}
|
|
196
|
+
</button>
|
|
197
|
+
) : null}
|
|
192
198
|
{d == null ? <span className="acdim">reading every repo…</span> : (
|
|
193
199
|
<>
|
|
194
200
|
<span className="acpill">{busy.length} busy</span>
|
|
@@ -198,6 +204,9 @@ export const ActivityView = observer(({ app }: { app: HomeSt }) => {
|
|
|
198
204
|
)}
|
|
199
205
|
</div>
|
|
200
206
|
{st.error !== '' ? <div className="hnote">{st.error}</div> : null}
|
|
207
|
+
{st.pushed == null || st.pushed.length === 0 ? null : (
|
|
208
|
+
<div className="acpushed">{st.pushed.map((p) => <div key={p.name} className={p.ok ? 'ok' : p.action === 'skip' ? 'skip' : 'fail'}>{p.line}</div>)}</div>
|
|
209
|
+
)}
|
|
201
210
|
{d != null && busy.length === 0 ? <div className="acdim">nothing committed or uncommitted in the last {windowWords(st.minutes)}</div> : null}
|
|
202
211
|
{busy.length + idle.length === 0 ? null : (
|
|
203
212
|
<table className={`actable${st.stale ? ' stale' : ''}`}>
|
|
@@ -72,3 +72,9 @@ button.acpill:disabled{cursor:default}
|
|
|
72
72
|
.acfork .hico{width:13px;height:13px}
|
|
73
73
|
.acgrp:hover .acfork{opacity:1}
|
|
74
74
|
.acfork:hover{color:var(--accent)}
|
|
75
|
+
.acpush{color:var(--ink);border-color:var(--accent)}
|
|
76
|
+
/* what the last push said: a line a repo, pushed in the ok colour, skipped dim, failed red */
|
|
77
|
+
.acpushed{margin:0 0 var(--s3);font:12px/1.5 ui-monospace,Menlo,monospace}
|
|
78
|
+
.acpushed .ok{color:var(--ok)}
|
|
79
|
+
.acpushed .skip{color:var(--dim)}
|
|
80
|
+
.acpushed .fail{color:var(--red)}
|
|
@@ -16,7 +16,7 @@ export class AdminSvc {
|
|
|
16
16
|
/** the colours the preview keeps while a picker moves, so a drag draws a
|
|
17
17
|
* few icons, not one per frame; null follows the repo's colours */
|
|
18
18
|
held: Identity | null = null
|
|
19
|
-
/** the repo has no
|
|
19
|
+
/** the repo has no mark, or it would not draw: the app wears the default icon */
|
|
20
20
|
noIcon = false
|
|
21
21
|
private rest: ReturnType<typeof setTimeout> | null = null
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ const IdentityBox = observer(({ app }: { app: RepoSt }) => {
|
|
|
27
27
|
<div className="adicon">
|
|
28
28
|
<img src={a.previewSrc} alt={`${name} app icon`} width={128} height={128} hidden={a.noIcon}
|
|
29
29
|
onLoad={() => a.previewLoaded()} onError={() => a.previewFailed()} />
|
|
30
|
-
{a.noIcon ? <span className="dim">no
|
|
30
|
+
{a.noIcon ? <span className="dim">no mark (.shipkit/identity/icon.png or icon.svg, public/logo.png or public/favicon.png): its app wears the default icon</span> : null}
|
|
31
31
|
</div>
|
|
32
32
|
<div className="adtext">
|
|
33
33
|
<b>identity</b>
|
|
@@ -36,7 +36,7 @@ export type HashSel =
|
|
|
36
36
|
/** the checks one owner ships: shipkit's, or the repo's own */
|
|
37
37
|
| { t: 'checks'; from: CheckOwner }
|
|
38
38
|
/** the review cards, `id` the open one, null for the first the list shows */
|
|
39
|
-
| { t: 'review'; id: string | null }
|
|
39
|
+
| { t: 'review'; id: string | null; q?: string }
|
|
40
40
|
/** a trailer's cuts, played with their notes: a trailer folder and one of its cuts, null for the first; the note
|
|
41
41
|
* open on it, and the notes filter when it is not the page's default (all) */
|
|
42
42
|
| { t: 'trailer'; trailer: string | null; cut: string | null; note?: string | null; filter?: 'open' | 'all' | null }
|
|
@@ -55,6 +55,8 @@ export type ParsedHash = {
|
|
|
55
55
|
/** a trailer address only: the note open, the notes filter */
|
|
56
56
|
note?: string
|
|
57
57
|
filter?: 'open' | 'all'
|
|
58
|
+
/** a review address only: its view and list filters, as `view=1&subject=place` */
|
|
59
|
+
q?: string
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
const ITEM = 'item:'
|
|
@@ -91,7 +93,8 @@ export function hashFor(sel: HashSel | null): string | null {
|
|
|
91
93
|
case 'git': return GIT
|
|
92
94
|
case 'when': return WHEN
|
|
93
95
|
case 'checks': return `${CHECKS}${sel.from}`
|
|
94
|
-
|
|
96
|
+
// page:review:<id>?view=1&subject=place, the query only when it says something
|
|
97
|
+
case 'review': return `${sel.id == null ? REVIEW : `${REVIEW}:${sel.id}`}${sel.q ? `?${sel.q}` : ''}`
|
|
95
98
|
case 'trailer': {
|
|
96
99
|
// page:trailer:<trailer>/<cut>/<note>?filter=all, each part only when it says something
|
|
97
100
|
const note = sel.trailer != null && sel.note ? `/${sel.cut ?? ''}/${sel.note}` : sel.cut == null ? '' : `/${sel.cut}`
|
|
@@ -111,7 +114,12 @@ export function parseHash(raw: string): ParsedHash {
|
|
|
111
114
|
if (raw === PLAN) return { kind: 'plan', col: null, id: '', tools: null }
|
|
112
115
|
if (raw === GIT) return { kind: 'git', col: null, id: '', tools: null }
|
|
113
116
|
if (raw === WHEN) return { kind: 'when', col: null, id: '', tools: null }
|
|
114
|
-
if (raw === REVIEW || raw.startsWith(`${REVIEW}:`)
|
|
117
|
+
if (raw === REVIEW || raw.startsWith(`${REVIEW}:`) || raw.startsWith(`${REVIEW}?`)) {
|
|
118
|
+
const qi = raw.indexOf('?')
|
|
119
|
+
const path = qi === -1 ? raw : raw.slice(0, qi)
|
|
120
|
+
const q = qi === -1 ? '' : raw.slice(qi + 1)
|
|
121
|
+
return { kind: 'review', col: null, id: path.slice(REVIEW.length + 1), tools: null, ...(q ? { q } : {}) }
|
|
122
|
+
}
|
|
115
123
|
// the trailer folder rides in `col`, its cut in `id`
|
|
116
124
|
if (raw === TRAILER || raw.startsWith(`${TRAILER}:`) || raw.startsWith(`${TRAILER}?`)) {
|
|
117
125
|
const qi = raw.indexOf('?')
|
|
@@ -173,7 +181,7 @@ export function selOf(p: ParsedHash): HashSel | null {
|
|
|
173
181
|
case 'git': return { t: 'git' }
|
|
174
182
|
case 'when': return { t: 'when' }
|
|
175
183
|
case 'checks': return { t: 'checks', from: p.id as CheckOwner }
|
|
176
|
-
case 'review': return { t: 'review', id: p.id === '' ? null : p.id }
|
|
184
|
+
case 'review': return { t: 'review', id: p.id === '' ? null : p.id, ...(p.q ? { q: p.q } : {}) }
|
|
177
185
|
case 'trailer': return { t: 'trailer', trailer: p.col, cut: p.id === '' ? null : p.id, ...(p.note ? { note: p.note } : {}), ...(p.filter ? { filter: p.filter } : {}) }
|
|
178
186
|
}
|
|
179
187
|
}
|
|
@@ -159,9 +159,15 @@ export class HomeSt {
|
|
|
159
159
|
return r
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/** the page is the embed page: one repo's page in another tool's frame (embed.ts) */
|
|
163
|
+
get embedded(): boolean {
|
|
164
|
+
return this.base !== undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
162
167
|
boot(): void {
|
|
163
168
|
this.machine?.boot()
|
|
164
|
-
|
|
169
|
+
// the embed page shows one repo's page alone: no dock, so no request for the house's runs
|
|
170
|
+
if (this.base === undefined) this.dock.boot()
|
|
165
171
|
this.disposers.push(reaction(() => this.title, (t) => { document.title = t }, { fireImmediately: true }))
|
|
166
172
|
// back, forward, a pasted link and a hand edited hash all arrive
|
|
167
173
|
// here; fromUrl is idempotent, so the two events firing together
|
|
@@ -256,7 +256,7 @@ const downCard = (m: MachineSt): Card[] => {
|
|
|
256
256
|
|
|
257
257
|
/** what is wrong with the machine itself, one card each: the machine not
|
|
258
258
|
* answering, a crashed child of pm, a crashed global process (its switch
|
|
259
|
-
* starts it again), a failed poll. While pm is silent its last answer says
|
|
259
|
+
* starts it again), a failed poll, the last good code running. While pm is silent its last answer says
|
|
260
260
|
* nothing here */
|
|
261
261
|
const machineCards = (m: MachineSt): Card[] => {
|
|
262
262
|
const pm = m.pmDown !== '' ? null : m.pm
|
|
@@ -265,6 +265,8 @@ const machineCards = (m: MachineSt): Card[] => {
|
|
|
265
265
|
...(pm?.children ?? []).filter((c) => c.status === 'crashed' || c.status === 'down').map((c): Card => ({ key: `child:${c.id}`, tone: 'red', head: `${c.id} ${STATUS_WORD[c.status]}`, body: c.note ?? c.tail.at(-1) ?? '' })),
|
|
266
266
|
...(pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r): Card => ({ key: `res:${r.id}`, tone: 'red', head: 'crashed', body: <ResourceChip m={m} r={r} /> })),
|
|
267
267
|
...(m.error !== '' ? [{ key: 'poll', tone: 'red' as const, body: m.error }] : []),
|
|
268
|
+
// the tree fails to load and the machine runs its last good code (core/last-good.ts)
|
|
269
|
+
...(m.data?.held != null ? [{ key: 'held', tone: 'warn' as const, head: 'last good code', body: m.data.held }] : []),
|
|
268
270
|
]
|
|
269
271
|
}
|
|
270
272
|
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
--k1:#8ca8c7;--k2:#a892c9;--k3:#6fb0b8;--k4:#c792b4;--k5:#9198a1;--k6:#8f95cf;
|
|
21
21
|
/* the review section's own hue: the one row of the tree meant to be seen first */
|
|
22
22
|
--review:#e879d4;
|
|
23
|
+
/* a wiki page's refs (wikis-view.tsx): read on demand, loaded with the page */
|
|
24
|
+
--ref-lazy:#e8984a;--ref-eager:#6fa8f0;
|
|
23
25
|
--wash:rgba(107,143,179,.15);--wash2:rgba(107,143,179,.11);--hair:rgba(107,143,179,.5);
|
|
24
26
|
--errbg:#2a1a1a;--errline:#4a2c2c;--errink:#e3bebe;
|
|
25
27
|
/* a running thing's wash, the green twin of errbg and errline */
|
|
@@ -39,6 +41,7 @@
|
|
|
39
41
|
--ground:#e3ede6;--groundline:#c6d6cb;--tag:#eef2f6;
|
|
40
42
|
--k1:#3f6690;--k2:#7055a3;--k3:#2b7c86;--k4:#a34e80;--k5:#5d646d;--k6:#4f56a8;
|
|
41
43
|
--review:#c0279d;
|
|
44
|
+
--ref-lazy:#b8600c;--ref-eager:#1f5fc8;
|
|
42
45
|
--wash:rgba(59,104,150,.12);--wash2:rgba(59,104,150,.08);--hair:rgba(59,104,150,.45);
|
|
43
46
|
--errbg:#fbe9e8;--errline:#efc4c2;--errink:#8a2520;
|
|
44
47
|
--okbg:#e6f3eb;--okline:#9ccdaf}
|
|
@@ -271,7 +274,8 @@ kbd{background:var(--panel2);border:1px solid var(--line);border-radius:4px;padd
|
|
|
271
274
|
background:conic-gradient(var(--chk1) 90deg,var(--chk2) 0 180deg,var(--chk1) 0 270deg,var(--chk2) 0) 0/34px 34px}
|
|
272
275
|
.stage img.roll{max-width:100%;max-height:100%;object-fit:contain;filter:drop-shadow(0 6px 20px var(--shadow));z-index:5;position:relative}
|
|
273
276
|
.stage .ph,.stagewrap .ph{color:var(--dim)}
|
|
274
|
-
|
|
277
|
+
/* every page card of a repo sits in this column, read from the left, never centred */
|
|
278
|
+
.stagewrap{flex:1;min-height:0;overflow-y:auto;display:flex;align-items:flex-start;justify-content:flex-start;padding:var(--s4)}
|
|
275
279
|
.stageaud{display:flex;flex-direction:column;align-items:center;gap:var(--s3);font-size:48px;padding:var(--s4)}
|
|
276
280
|
.stageaud audio{width:min(480px,80vw)}
|
|
277
281
|
|
|
@@ -703,6 +707,39 @@ a.tok{color:var(--accent)}
|
|
|
703
707
|
.gvframe{flex:1;min-height:0;position:relative;display:flex;border:1px solid var(--line);border-radius:3px;background:var(--bg)}
|
|
704
708
|
.gvwait{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:var(--s2);font-size:12px;color:var(--dim)}
|
|
705
709
|
.graphview iframe{position:relative;flex:1;min-height:0;width:100%;border:0;background:transparent}
|
|
710
|
+
/* the graph and its preview panel (wikis-view.tsx) */
|
|
711
|
+
.gvbody{flex:1;min-height:0}
|
|
712
|
+
.gvbody .gvframe{height:100%}
|
|
713
|
+
/* the wiki's search bar, its hits dropped under it over the graph */
|
|
714
|
+
.wsearch{position:relative;flex:0 1 360px;min-width:160px;display:flex}
|
|
715
|
+
.wsearch input{flex:1;min-width:0;height:22px;background:var(--bg);color:var(--ink);border:1px solid var(--line);border-radius:999px;padding:0 var(--s3);font:inherit;font-size:12px}
|
|
716
|
+
.wsearch input:focus{outline:none;border-color:var(--accent)}
|
|
717
|
+
.wslist{position:absolute;top:calc(100% + 4px);left:0;z-index:30;width:min(560px,80vw);max-height:60vh;overflow-y:auto;padding:var(--s1) 0;background:var(--panel);border:1px solid var(--line);border-radius:6px;box-shadow:0 6px 20px var(--shadow)}
|
|
718
|
+
.wshit{padding:var(--s1) var(--s3);cursor:pointer}
|
|
719
|
+
.wshit.on,.wshit:hover{background:var(--sel)}
|
|
720
|
+
.wshead{display:flex;align-items:baseline;gap:var(--s2);min-width:0}
|
|
721
|
+
.wshead b{flex:none;font-size:12.5px}
|
|
722
|
+
.wspath{min-width:0;color:var(--dim);font:11px/1.4 ui-monospace,Menlo,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
723
|
+
.wssnip{color:var(--dim);font-size:11.5px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
|
724
|
+
.wsnone{padding:var(--s1) var(--s3);color:var(--dim);font-size:12px}
|
|
725
|
+
/* the page open beside the graph */
|
|
726
|
+
.wprev{height:100%;display:flex;flex-direction:column;min-width:0;background:var(--panel);border:1px solid var(--line);border-radius:3px}
|
|
727
|
+
.wphead{flex:none;display:flex;align-items:center;gap:var(--s2);padding:var(--s2) var(--s3);border-bottom:1px solid var(--line);min-width:0}
|
|
728
|
+
.wptitle{flex:0 1 auto;min-width:0;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
729
|
+
.wphead .wspath{flex:1}
|
|
730
|
+
.wpplace{flex:none}
|
|
731
|
+
.wpplace button{padding:0 var(--s1)}
|
|
732
|
+
.wpplace .hico{width:14px;height:14px}
|
|
733
|
+
.wpbtn{flex:none;display:inline-flex;align-items:center;gap:var(--s1);height:22px;padding:0 var(--s2);border:1px solid var(--line);border-radius:999px;color:var(--dim);font-size:11.5px;text-decoration:none;background:transparent;cursor:pointer}
|
|
734
|
+
.wpbtn:hover{color:var(--ink);border-color:var(--accent)}
|
|
735
|
+
.wpbtn .hico{width:13px;height:13px}
|
|
736
|
+
.wpclose{flex:none;padding:0 var(--s2);font-size:14px;line-height:1.2}
|
|
737
|
+
/* the refs of a page: on demand (→, a link, a path) orange, loaded with it (@) blue and bold */
|
|
738
|
+
.wpbody a[data-ref=lazy]{color:var(--ref-lazy)}
|
|
739
|
+
.wpbody a[data-ref=lazy] code{color:var(--ref-lazy)}
|
|
740
|
+
.wpbody a[data-ref=eager]{color:var(--ref-eager);font-weight:700}
|
|
741
|
+
.wpbody{flex:1;min-height:0;overflow-y:auto;padding:var(--s2) var(--s4) var(--s4)}
|
|
742
|
+
.wpbody a[data-wiki]{text-decoration:underline;text-decoration-style:dotted;text-underline-offset:2px;cursor:pointer}
|
|
706
743
|
|
|
707
744
|
/* ── comfy item view (comfy-ts's own panel fills the stage) ── */
|
|
708
745
|
.comfyview{flex:1;min-height:0;display:flex;flex-direction:column;gap:var(--s2);padding:var(--s3) var(--s4) var(--s4)}
|
|
@@ -44,6 +44,12 @@ export const ICON = {
|
|
|
44
44
|
sun: 'M10 6.5a3.5 3.5 0 1 0 .01 0M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M4.3 15.7l1.4-1.4M14.3 5.7l1.4-1.4',
|
|
45
45
|
moon: 'M15.5 12.5A6.5 6.5 0 0 1 7.5 4.5a6.5 6.5 0 1 0 8 8z',
|
|
46
46
|
auto: 'M10 3a7 7 0 1 0 .01 0M10 3v14M10 5.5h4M10 8.5h6M10 11.5h6M10 14.5h4',
|
|
47
|
+
// where the wiki preview sits: a frame with its panel's edge
|
|
48
|
+
paneLeft: 'M3 4h14v12H3zM8 4v12M4.5 7h2M4.5 9.5h2',
|
|
49
|
+
paneRight: 'M3 4h14v12H3zM12 4v12M13.5 7h2M13.5 9.5h2',
|
|
50
|
+
paneBottom: 'M3 4h14v12H3zM3 11h14M6 13.5h8',
|
|
51
|
+
copy: 'M7 7h9v10H7zM4 13V3h9',
|
|
52
|
+
editor: 'M13 3l4 3v8l-4 3-7-6 7-5zM3 7.5l3 2.5-3 2.5',
|
|
47
53
|
// the project list's fold: a panel with its rail marked
|
|
48
54
|
rail: 'M3 4h14v12H3zM7.5 4v12',
|
|
49
55
|
// a process's own switches on the overview
|
|
@@ -16,10 +16,10 @@ export const LIST: Bounds = { key: 'hub:list-width', min: 140, max: 360, def: 20
|
|
|
16
16
|
export const SIDEBAR: Bounds = { key: 'hub:sidebar-width', min: 150, max: 640, def: 330 }
|
|
17
17
|
|
|
18
18
|
/** the rail's icon sizes, in px: the foot's button cycles them */
|
|
19
|
-
export const RAIL_ICONS = { s:
|
|
19
|
+
export const RAIL_ICONS = { s: 24, m: 32, l: 40 } as const
|
|
20
20
|
export type RailIcon = keyof typeof RAIL_ICONS
|
|
21
21
|
/** the rail is its icon and this much on each side */
|
|
22
|
-
const RAIL_PAD =
|
|
22
|
+
const RAIL_PAD = 6
|
|
23
23
|
/** the project list folded: its icons only, a row's name in its hover card */
|
|
24
24
|
export const railPx = (size: RailIcon): number => RAIL_ICONS[size] + 2 * RAIL_PAD
|
|
25
25
|
const RAIL_KEY = 'hub:list-rail'
|
|
@@ -110,7 +110,7 @@ export class MachineSt {
|
|
|
110
110
|
* `shipkit global-processes` prints) */
|
|
111
111
|
get processGroups(): ProcessGroup[] {
|
|
112
112
|
const silent = { ...(this.down === '' ? {} : { [this.downWho]: this.down }), ...(this.pmDown === '' ? {} : { pm: this.pmDown }) }
|
|
113
|
-
return processGroups(this.pm, this.st.projects.list.map((p) => p.name), { daemonVersion: this.data?.version ?? null, silent })
|
|
113
|
+
return processGroups(this.pm, this.st.projects.list.map((p) => p.name), { daemonVersion: this.data?.version ?? null, daemonHeld: this.data?.held ?? null, silent })
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
resource(id: string): ResourceView | null {
|
package/src/serve/hub-app/md.ts
CHANGED
|
@@ -70,3 +70,35 @@ export const renderReadme = (src: string, fileUrl: (rel: string) => string): str
|
|
|
70
70
|
},
|
|
71
71
|
exclusiveFilter: (frame) => frame.tag === 'img' && (frame.attribs.src ?? '') === '',
|
|
72
72
|
})
|
|
73
|
+
|
|
74
|
+
/** a wiki page in the wikis page's preview: the card allowlist, and every ref
|
|
75
|
+
* the server resolved (`links`, token → node, graph-pages.ts) as an anchor
|
|
76
|
+
* that opens that page in place instead of a new tab, marked by its kind so
|
|
77
|
+
* the sheet tells them apart: `@` loads with the page (eager), `→`, a link or
|
|
78
|
+
* a path in a code span is read on demand (lazy, written `→ path` here
|
|
79
|
+
* whatever the page wrote) */
|
|
80
|
+
export const renderWikiPage = (src: string, links: Record<string, string>): string => {
|
|
81
|
+
const to = (t: string, eager = false): string | null => (links[t] == null ? null : `#wiki${eager ? '-eager' : ''}:${encodeURIComponent(links[t]!)}`)
|
|
82
|
+
// fences stay as written: a ref in a code block is an example, not a link
|
|
83
|
+
const linked = src.split(/(^```[\s\S]*?^```\s*$)/m).map((part, i) => i % 2 === 1 ? part : part
|
|
84
|
+
.replace(/\]\(([^)\s:]+\.md)\)/g, (m, t: string) => (to(t) == null ? m : `](${to(t)})`))
|
|
85
|
+
.replace(/→(\s*)([\w][\w./-]*\.md)\b/g, (m, sp: string, t: string) => (to(t) == null ? m : `→${sp}[${t}](${to(t)})`))
|
|
86
|
+
.replace(/(^|[\s(])@((?:\.{1,2}\/)*[\w][\w./-]*\.md)/gm, (m, pre: string, t: string) => (to(t, true) == null ? m : `${pre}[@${t}](${to(t, true)})`))
|
|
87
|
+
// a code span that is exactly a page's path: a lazy ref, its arrow added unless the page wrote one
|
|
88
|
+
.replace(/(→\s*)?(?<!\[)`([\w.][\w./-]*\.md)`/g, (m, arrow: string | undefined, t: string) => (to(t) == null ? m : `${arrow ?? '→ '}[\`${t}\`](${to(t)})`)),
|
|
89
|
+
).join('')
|
|
90
|
+
return sanitizeHtml(marked.parse(linked) as string, {
|
|
91
|
+
allowedTags: [...CARD_TAGS, 'h5', 'h6', 'del'],
|
|
92
|
+
allowedAttributes: { a: ['href', 'target', 'rel', 'data-wiki', 'data-ref'] },
|
|
93
|
+
allowedSchemes: ['http', 'https'],
|
|
94
|
+
allowProtocolRelative: false,
|
|
95
|
+
transformTags: {
|
|
96
|
+
a: (tagName, attribs): sanitizeHtml.Tag => {
|
|
97
|
+
const href = attribs.href ?? ''
|
|
98
|
+
const ref = /^#wiki(-eager)?:(.*)$/.exec(href)
|
|
99
|
+
if (ref != null) return { tagName, attribs: { href, 'data-wiki': decodeURIComponent(ref[2]!), 'data-ref': ref[1] != null ? 'eager' : 'lazy' } }
|
|
100
|
+
return { tagName, attribs: { ...attribs, target: '_blank', rel: 'noreferrer' } }
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
@@ -69,8 +69,8 @@
|
|
|
69
69
|
.pgi.h5{--c:var(--k5)}
|
|
70
70
|
.pgi.h6{--c:var(--k6)}
|
|
71
71
|
/* the row says three things and no more (projects-svc faceOf) */
|
|
72
|
-
/* closed in VS Code: the
|
|
73
|
-
.pjrow.dim
|
|
72
|
+
/* closed in VS Code: the name dims, never the icon (the icon is how a repo is found, and the rail has no name) */
|
|
73
|
+
.pjrow.dim .pjname{opacity:.5}
|
|
74
74
|
/* ⌘1 to ⌘9 select the first nine rows (keys.ts): the hint sits at the row's end */
|
|
75
75
|
.pjkey{flex:none;font:10px/1 ui-monospace,Menlo,monospace;color:var(--dim);opacity:.6;background:none;border:0;padding:0}
|
|
76
76
|
.pjrow:hover .pjkey,.pjrow.cur .pjkey,.pjpl:hover .pjkey,.pjpl.cur .pjkey{opacity:1}
|
|
@@ -128,12 +128,14 @@
|
|
|
128
128
|
.pjlist.rail .pjname,.pjlist.rail .pjkey,.pjlist.rail .pjstep,.pjlist.rail .pgsortl,.pjlist.rail .pjarow span,.pjlist.rail .pgerr{display:none}
|
|
129
129
|
.pjlist.rail .pjrow,.pjlist.rail .pjpl,.pjlist.rail .pjarow{justify-content:center;padding:0}
|
|
130
130
|
/* the rail's icon size (layout-st RAIL_ICONS, the foot's S M L button): the repo icons and the row under them grow */
|
|
131
|
-
.pjlist.rail .pjrow{height:calc(var(--ri,
|
|
132
|
-
.pjlist.rail .pjrow img,.pjlist.rail .pjrow .pgi{width:var(--ri,
|
|
133
|
-
.pjlist.rail .pjrow .pgi{font-size:calc(var(--ri,
|
|
131
|
+
.pjlist.rail .pjrow{height:calc(var(--ri,24px) + 6px)}
|
|
132
|
+
.pjlist.rail .pjrow img,.pjlist.rail .pjrow .pgi{width:var(--ri,24px);height:var(--ri,24px);border-radius:6px}
|
|
133
|
+
.pjlist.rail .pjrow .pgi{font-size:calc(var(--ri,24px) * .5)}
|
|
134
|
+
/* the app icon is a rounded plate already (core/app-icon.ts) */
|
|
135
|
+
.pjlist.rail .pjrow img.pjapp{border-radius:0}
|
|
134
136
|
.pjsize{font:600 10px/1 ui-monospace,Menlo,monospace}
|
|
135
|
-
.pjlist.rail .pga{position:absolute;top:
|
|
136
|
-
.pjlist.rail .pgd,.pjlist.rail .pgw{position:absolute;bottom:var(--
|
|
137
|
+
.pjlist.rail .pga{position:absolute;top:0;right:calc(50% - var(--ri,24px) / 2 - 4px);min-width:14px;height:14px;padding:0 var(--s0);border-radius:7px;font-size:9px;line-height:14px}
|
|
138
|
+
.pjlist.rail .pgd,.pjlist.rail .pgw{position:absolute;bottom:var(--s0);right:calc(50% - var(--ri,24px) / 2 - 2px)}
|
|
137
139
|
.pjlist.rail .pjfoot{flex-direction:column;height:auto;padding:var(--s1) 0}
|
|
138
140
|
.pjlist.rail .pjfoot button{justify-content:center;width:28px;padding:0}
|
|
139
141
|
.pjlist.rail .pjarbtn span{display:none}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the shown repo's row marked in its identity colour. Its width is the
|
|
4
4
|
// panel the page gives it (hub-app.tsx, react-resizable-panels). A row says
|
|
5
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
|
|
6
|
+
// name (never the icon) while VS Code does not have the folder open, a thin edge breathing
|
|
7
7
|
// while Claude sessions run in it; on the home, a dot while its hub starts
|
|
8
8
|
// or crashed and a mark while a global process it uses is off. Everything
|
|
9
9
|
// else is the hover card, placed by floating-ui beside the row. A click
|
|
@@ -72,6 +72,7 @@ export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: numb
|
|
|
72
72
|
const warn = s.warnOf(p.name)
|
|
73
73
|
// the shown repo's row is marked in its own colour
|
|
74
74
|
const mark = cur ? (idVars(s.identityOf(p.name)) as CSSProperties) : undefined
|
|
75
|
+
// folded, the rail wears the repo's app icon: its mark on its identity plate
|
|
75
76
|
return (
|
|
76
77
|
<>
|
|
77
78
|
<button ref={f.refs.setReference} className={cls} aria-label={p.name} style={mark} draggable
|
|
@@ -95,7 +96,9 @@ export const Row = observer(({ app, p, i }: { app: ListHost; p: Project; i: numb
|
|
|
95
96
|
},
|
|
96
97
|
onDragEnd: () => s.dragEnd(),
|
|
97
98
|
})}>
|
|
98
|
-
|
|
99
|
+
{app.layout?.listRail === true && p.appIcon != null
|
|
100
|
+
? <img className="pjapp" src={p.appIcon} alt="" width={ROW_ICON} height={ROW_ICON} draggable={false} />
|
|
101
|
+
: <Face p={p} px={ROW_ICON} />}
|
|
99
102
|
<span className="pjname">{p.name}</span>
|
|
100
103
|
{face.badge != null ? <b className="pga">{face.badge}</b> : null}
|
|
101
104
|
{face.busy ? <i className="pgs" /> : null}
|
|
@@ -134,8 +134,9 @@ export function sortNames(list: Project[], mode: SortMode): string[] {
|
|
|
134
134
|
export type HubDot = 'starting' | 'crashed' | null
|
|
135
135
|
|
|
136
136
|
/** what the icon face shows, and nothing more: at most ONE badge (the open
|
|
137
|
-
* asks, the only fact that needs a human's hands), dim while VS Code
|
|
138
|
-
* not have the folder open, and a pulsing edge
|
|
137
|
+
* asks, the only fact that needs a human's hands), a dim name while VS Code
|
|
138
|
+
* does not have the folder open (the icon stays whole), and a pulsing edge
|
|
139
|
+
* while Claude sessions run */
|
|
139
140
|
export type Face = { badge: number | null; dim: boolean; busy: boolean }
|
|
140
141
|
export const faceOf = (p: Project): Face => ({
|
|
141
142
|
badge: p.asks > 0 ? p.asks : null,
|