rvlib-shipkit 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +15 -10
- package/src/check.ts +26 -10
- package/src/checks/SK001-package-json-shape.check.ts +34 -5
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +103 -61
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +50 -0
- package/src/checks/SK028-readme.check.ts +83 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +119 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +7 -3
- package/src/collections/test-time.ts +10 -1
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +66 -17
- package/src/core/discover-checks.ts +10 -5
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +1 -1
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +8 -7
- package/src/core/run-ci.ts +46 -15
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +49 -9
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +138 -0
- package/src/home/activity.ts +256 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +10 -2
- package/src/home/home-routes.ts +34 -6
- package/src/home/home-start.ts +33 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +40 -7
- package/src/home/process-rows.ts +222 -0
- package/src/home/resources.ts +99 -20
- package/src/home/run-daemon.ts +12 -7
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +32 -5
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +20 -6
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +30 -2
- package/src/ledger/world-cache.ts +63 -14
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +17 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-words.ts +2 -0
- package/src/serve/hub-app/checks-svc.ts +2 -1
- package/src/serve/hub-app/col-tables-view.tsx +124 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +28 -0
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +50 -13
- package/src/serve/hub-app/home-strip.css +81 -23
- package/src/serve/hub-app/home-strip.tsx +243 -102
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +103 -161
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +101 -39
- package/src/serve/hub-app/icons.tsx +27 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +44 -9
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +65 -13
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +13 -9
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +26 -7
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +29 -9
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +34 -13
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/serve/warm-cell.ts +49 -16
- package/src/shipkit-files.ts +20 -4
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -1,12 +1,36 @@
|
|
|
1
|
-
// the few line icons of the home (the overview's section headers, the
|
|
2
|
-
//
|
|
3
|
-
// the page's tokens colour them
|
|
1
|
+
// the few line icons of the home (the overview's section headers, the places
|
|
2
|
+
// on top of the project list, a repo page's sections and its top bar): one 20
|
|
3
|
+
// unit path each, stroked in the text colour, so the page's tokens colour them
|
|
4
|
+
// and nothing else does.
|
|
4
5
|
export const ICON = {
|
|
5
6
|
shipkit: 'M3 7.5 10 4l7 3.5v5L10 16l-7-3.5zM3 7.5 10 11l7-3.5M10 11v5',
|
|
6
7
|
process: 'M2 10h3.5l2-5 5 10 2-5H18',
|
|
7
8
|
monitor: 'M3 4h14v9H3zM7 16.5h6M10 13v3.5',
|
|
9
|
+
archived: 'M2.5 4h15v3.5h-15zM4 7.5V16h12V7.5M8 10.5h4',
|
|
8
10
|
home: 'M3 9.5 10 3.5l7 6M5 8v8.5h10V8M8.5 16.5v-4.5h3v4.5',
|
|
9
11
|
planned: 'M5.5 3h9M5.5 17h9M6.5 3c0 4 7 4 7 7s-7 3-7 7M13.5 3c0 4-7 4-7 7s7 3 7 7',
|
|
12
|
+
browser: 'M10 2.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 1 0 0-15M2.5 10h15M10 2.5c-3 3-3 12 0 15M10 2.5c3 3 3 12 0 15',
|
|
13
|
+
window: 'M2.5 4h15v12h-15zM2.5 7.5h15M5 5.75h.01M7 5.75h.01',
|
|
14
|
+
activity: 'M6 3v14M6 6.5a2 2 0 1 0 0 .01M6 13.5a2 2 0 1 0 0 .01M10 6.5h7M10 13.5h7M10 10h5',
|
|
15
|
+
fork: 'M6 5v10M6 3a2 2 0 1 0 0 .01M6 17a2 2 0 1 0 0 .01M14 5a2 2 0 1 0 0 .01M14 7c0 4.5-8 3.5-8 7',
|
|
16
|
+
search: 'M8.5 3a5.5 5.5 0 1 0 0 11a5.5 5.5 0 1 0 0-11M12.5 12.5 17 17',
|
|
17
|
+
// a repo page's sections, then its two runs and its editor
|
|
18
|
+
checks: 'M4 10.5 7.5 14 16 5.5',
|
|
19
|
+
runs: 'M3 5h8M3 10h12M3 15h6',
|
|
20
|
+
asks: 'M3 4h14v9H8l-4 3.5V13H3z',
|
|
21
|
+
tests: 'M8 3h4M8.5 3v5L4 16.5h12L11.5 8V3M6.2 12.5h7.6',
|
|
22
|
+
media: 'M3 4h14v12H3zM3 13l4-4 4 4 2-2 4 4M13 7.5h.01',
|
|
23
|
+
wikis: 'M10 5.5C8.5 4 6 3.5 3 4v11c3-.5 5.5 0 7 1.5c1.5-1.5 4-2 7-1.5V4c-3-.5-5.5 0-7 1.5zM10 5.5v11',
|
|
24
|
+
admin: 'M4 5h12M4 10h12M4 15h12M7 3.5v3M13 8.5v3M9 13.5v3',
|
|
25
|
+
health: 'M2 10h3.5l2-5 5 10 2-5H18',
|
|
26
|
+
gate: 'M10 2.5 16 5v4.5c0 4-2.7 6.5-6 8c-3.3-1.5-6-4-6-8V5zM7.5 10l2 2 3.5-3.5',
|
|
27
|
+
// the VS Code mark, its ribbon drawn as one outline
|
|
28
|
+
vscode: 'M14 2.5l3.5 1.7v11.6L14 17.5zM14 2.5 4.5 12.3 2.5 11V9l2-1.3L14 17.5M14 6.8 10.2 10l3.8 3.2',
|
|
29
|
+
sort: 'M6 3.5v13M3 13.5l3 3 3-3M14 16.5v-13M11 6.5l3-3 3 3',
|
|
30
|
+
// a process's own switches on the overview
|
|
31
|
+
play: 'M6.5 4.5v11l9-5.5z',
|
|
32
|
+
stop: 'M5.5 5.5h9v9h-9z',
|
|
33
|
+
restart: 'M15.5 10a5.5 5.5 0 1 1-1.6-3.9M14.2 3v3.3h-3.3',
|
|
10
34
|
} as const
|
|
11
35
|
|
|
12
36
|
export const Icon = ({ d }: { d: string }) => (
|
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
<link rel="stylesheet" href="./prose-hits.css" />
|
|
10
10
|
<link rel="stylesheet" href="./home-strip.css" />
|
|
11
11
|
<link rel="stylesheet" href="./planned.css" />
|
|
12
|
+
<link rel="stylesheet" href="./activity.css" />
|
|
13
|
+
<link rel="stylesheet" href="./readme.css" />
|
|
14
|
+
<link rel="stylesheet" href="./dock.css" />
|
|
15
|
+
<link rel="stylesheet" href="./controls.css" />
|
|
12
16
|
</head>
|
|
13
17
|
<body>
|
|
14
18
|
<div id="root"></div>
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// the page's command keys, read the same on every layout: ⌘1 to ⌘9 select the
|
|
2
|
-
// nth repo of the project list,
|
|
3
|
-
// the picker.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
2
|
+
// nth repo of the project list, then the home row under the right hand: ⌘J
|
|
3
|
+
// the picker, ⌘K the overview, ⌘L the activity, ⌘' the plan. ⌘B cycles the
|
|
4
|
+
// dock at the bottom of the page (dock-st.ts): the line, slim, open.
|
|
5
|
+
// A digit and the quote are read from the physical key (`code`), so an
|
|
6
|
+
// AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
|
|
6
7
|
// ⌘ off a Mac. The macOS app binds none of these to a menu item, so they
|
|
7
8
|
// reach the page (tests/home-strip.test.tsx reads ShipkitGui.swift).
|
|
8
|
-
export type PageKey = { t: 'repo'; n: number } | { t: 'home' } | { t: 'planned' } | { t: 'picker' }
|
|
9
|
+
export type PageKey = { t: 'repo'; n: number } | { t: 'home' } | { t: 'planned' } | { t: 'activity' } | { t: 'picker' } | { t: 'dock' }
|
|
9
10
|
|
|
10
11
|
type KeyLike = { key: string; code: string; metaKey: boolean; ctrlKey: boolean; shiftKey: boolean; altKey: boolean }
|
|
11
12
|
|
|
@@ -14,12 +15,46 @@ export function pageKey(e: KeyLike): PageKey | null {
|
|
|
14
15
|
const digit = /^Digit([1-9])$/.exec(e.code)
|
|
15
16
|
if (digit != null) return { t: 'repo', n: Number(digit[1]) }
|
|
16
17
|
if (e.shiftKey) return null
|
|
18
|
+
if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
|
|
17
19
|
const k = e.key.toLowerCase()
|
|
18
|
-
if (k === 'j') return { t: '
|
|
19
|
-
if (k === '
|
|
20
|
-
if (k === '
|
|
20
|
+
if (k === 'j') return { t: 'picker' }
|
|
21
|
+
if (k === 'k') return { t: 'home' }
|
|
22
|
+
if (k === 'l') return { t: 'activity' }
|
|
23
|
+
if (k === 'b') return { t: 'dock' }
|
|
21
24
|
return null
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/** the shortcut each place of the home carries on its button */
|
|
25
|
-
export const PLACE_KEY = { overview: '⌘
|
|
28
|
+
export const PLACE_KEY = { overview: '⌘K', planned: "⌘'", activity: '⌘L' } as const
|
|
29
|
+
/** the picker's shortcut, on the first button, before home */
|
|
30
|
+
export const PICKER_KEY = '⌘J'
|
|
31
|
+
/** the dock's shortcut, on the dock's own header */
|
|
32
|
+
export const DOCK_KEY = '⌘B'
|
|
33
|
+
|
|
34
|
+
/** the tree's filter, on a repo page: a plain `/` focuses it, and its
|
|
35
|
+
* badge at the input's right prints this */
|
|
36
|
+
export const SEARCH_KEY = '/'
|
|
37
|
+
|
|
38
|
+
export function isSearchKey(e: Pick<KeyLike, 'key' | 'metaKey' | 'ctrlKey' | 'altKey'>): boolean {
|
|
39
|
+
return e.key === SEARCH_KEY && !e.metaKey && !e.ctrlKey && !e.altKey
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** a repo page's sections, each on ⌥ and its letter. ⌥ because every other
|
|
43
|
+
* layer is spoken for: the plain letters are the severity filter (a n r)
|
|
44
|
+
* and the chart's follow key, ⌘ with these letters is copy, reload, select
|
|
45
|
+
* all, new tab, minimize and close in the browser and the macOS app, and ⌥
|
|
46
|
+
* plus a letter only types a character inside a text field, where the page
|
|
47
|
+
* leaves the key alone. Read from the physical key (`code`): on a Mac ⌥C
|
|
48
|
+
* types ç, and AZERTY moves the letters */
|
|
49
|
+
export type SectionId = 'checks' | 'runs' | 'asks' | 'tests' | 'media' | 'wikis' | 'admin'
|
|
50
|
+
export const SECTION_LETTER: Record<SectionId, string> = { checks: 'C', runs: 'R', asks: 'A', tests: 'T', media: 'M', wikis: 'W', admin: 'D' }
|
|
51
|
+
/** the badge at the end of a section's line */
|
|
52
|
+
export const sectionKeyLabel = (id: SectionId): string => `⌥${SECTION_LETTER[id]}`
|
|
53
|
+
|
|
54
|
+
export function sectionKey(e: KeyLike): SectionId | null {
|
|
55
|
+
if (!e.altKey || e.metaKey || e.ctrlKey || e.shiftKey) return null
|
|
56
|
+
const m = /^Key([A-Z])$/.exec(e.code)
|
|
57
|
+
if (m == null) return null
|
|
58
|
+
const hit = Object.entries(SECTION_LETTER).find(([, l]) => l === m[1])
|
|
59
|
+
return hit == null ? null : (hit[0] as SectionId)
|
|
60
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// (chart-view.ts, timeline-chart.tsx).
|
|
1
|
+
// A repo's live runs, as the page holds them: `/api/live.json` read every
|
|
2
|
+
// second while the repo shows (the runs of the last ten minutes and the ones
|
|
3
|
+
// in flight), which the header's run state reads, and the chart of the runs
|
|
4
|
+
// page over it: the whole record (the recent runs of /api/live.json over the
|
|
5
|
+
// older ones of /api/timeline.json), opened on its last ten minutes and fit
|
|
6
|
+
// to all of it on demand (chart-view.ts, timeline-chart.tsx). What runs in
|
|
7
|
+
// every repo at once is the dock's (dock-st.ts).
|
|
9
8
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
10
9
|
import type { LiveView } from '../../ledger/live-timeline.ts'
|
|
11
10
|
import type { TimelineInvocation } from '../../ledger/run-timeline.ts'
|
|
@@ -15,16 +14,15 @@ import { chartHeight } from './timeline-geometry.ts'
|
|
|
15
14
|
|
|
16
15
|
/** how often the page asks. A strip a second behind the runner is still live */
|
|
17
16
|
export const LIVE_POLL_MS = 1_000
|
|
18
|
-
/** the
|
|
19
|
-
export const FOLDED_WINDOW_MS = 120_000
|
|
17
|
+
/** the span the chart opens on */
|
|
20
18
|
export const OPEN_WINDOW_MS = 600_000
|
|
21
|
-
/** the most the
|
|
19
|
+
/** the most the chart grows to: past it the lanes scroll inside it */
|
|
22
20
|
export const OPEN_MAX_H = 320
|
|
23
|
-
/** how long the
|
|
21
|
+
/** how long the chart keeps a height its lanes no longer fill,
|
|
24
22
|
* so it grows at once and never shrinks under the eye every second */
|
|
25
23
|
export const HEIGHT_HOLD_MS = 10_000
|
|
26
24
|
|
|
27
|
-
/** the height the
|
|
25
|
+
/** the height the chart shows: up at once, down only once the
|
|
28
26
|
* lanes have wanted less for HEIGHT_HOLD_MS */
|
|
29
27
|
export type HeldHeight = { h: number; lowSince: number | null }
|
|
30
28
|
export function settleHeight(prev: HeldHeight, wanted: number, now: number, holdMs = HEIGHT_HOLD_MS): HeldHeight {
|
|
@@ -33,32 +31,20 @@ export function settleHeight(prev: HeldHeight, wanted: number, now: number, hold
|
|
|
33
31
|
return now - prev.lowSince >= holdMs ? { h: wanted, lowSince: null } : prev
|
|
34
32
|
}
|
|
35
33
|
|
|
36
|
-
/** the whole record the
|
|
34
|
+
/** the whole record the chart draws: every run of the live view,
|
|
37
35
|
* then the older runs of the record it does not carry, newest first */
|
|
38
36
|
export function mergeRuns(recent: TimelineInvocation[], record: TimelineInvocation[]): TimelineInvocation[] {
|
|
39
37
|
const seen = new Set(recent.map((i) => i.runId))
|
|
40
38
|
return [...recent, ...record.filter((i) => !seen.has(i.runId))].sort((a, b) => b.started - a.started)
|
|
41
39
|
}
|
|
42
|
-
/** the lanes the folded strip has room for */
|
|
43
|
-
export const FOLDED_LANES = 5
|
|
44
|
-
|
|
45
|
-
/** where the fold is remembered. localStorage is per origin and every
|
|
46
|
-
* repo's hub has its own port, so this is per repo already */
|
|
47
|
-
export const OPEN_KEY = 'hub:liveOpen'
|
|
48
|
-
|
|
49
|
-
const readOpen = (): boolean => {
|
|
50
|
-
try { return globalThis.localStorage?.getItem(OPEN_KEY) === '1' } catch { return false }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
40
|
/** the server clock now: the view's stamp plus what the page clock has
|
|
54
41
|
* counted since it arrived, since the page may be another machine */
|
|
55
|
-
export const serverNow = (view:
|
|
42
|
+
export const serverNow = (view: { now: number }, receivedAt: number, clientNow: number): number => view.now + Math.max(0, clientNow - receivedAt)
|
|
56
43
|
|
|
57
|
-
/** what the chart draws:
|
|
58
|
-
*
|
|
59
|
-
export function liveSource(view: LiveView | null, now: number,
|
|
44
|
+
/** what the runs page's chart draws: the whole record, opening on its last
|
|
45
|
+
* ten minutes */
|
|
46
|
+
export function liveSource(view: LiveView | null, now: number, record: TimelineInvocation[] = []): ChartSource | null {
|
|
60
47
|
if (view == null) return null
|
|
61
|
-
if (!open) return { invocations: view.invocations, now, windowMs: FOLDED_WINDOW_MS, laneCap: Math.max(1, FOLDED_LANES - view.repos.length), zoomable: false, cap: view.cap }
|
|
62
48
|
return { invocations: mergeRuns(view.invocations, record), now, windowMs: null, zoomable: true, cap: view.cap, homeSpanMs: OPEN_WINDOW_MS }
|
|
63
49
|
}
|
|
64
50
|
|
|
@@ -67,10 +53,7 @@ export class LiveSvc {
|
|
|
67
53
|
/** the page clock when `view` arrived */
|
|
68
54
|
receivedAt = 0
|
|
69
55
|
error = ''
|
|
70
|
-
/**
|
|
71
|
-
note = ''
|
|
72
|
-
open = readOpen()
|
|
73
|
-
/** the unfolded chart's height, held so it does not jump every second */
|
|
56
|
+
/** the chart's height, held so it does not jump every second */
|
|
74
57
|
held: HeldHeight = { h: 0, lowSince: null }
|
|
75
58
|
chart: ChartView
|
|
76
59
|
private timer: ReturnType<typeof setInterval> | null = null
|
|
@@ -78,7 +61,7 @@ export class LiveSvc {
|
|
|
78
61
|
private started = false
|
|
79
62
|
|
|
80
63
|
constructor(private st: RepoSt) {
|
|
81
|
-
this.chart = new ChartView(null, () => liveSource(this.view, this.now, this.
|
|
64
|
+
this.chart = new ChartView(null, () => liveSource(this.view, this.now, this.st.timeline.data?.invocations ?? []))
|
|
82
65
|
makeAutoObservable<this, 'st' | 'timer' | 'inFlight' | 'started'>(this, { st: false, timer: false, inFlight: false, started: false, view: observableRef, chart: false })
|
|
83
66
|
}
|
|
84
67
|
|
|
@@ -87,18 +70,9 @@ export class LiveSvc {
|
|
|
87
70
|
return this.view == null ? this.st.clock.now : serverNow(this.view, this.receivedAt, this.st.clock.now)
|
|
88
71
|
}
|
|
89
72
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
try { globalThis.localStorage?.setItem(OPEN_KEY, this.open ? '1' : '0') } catch { /* private window: the fold lasts the tab */ }
|
|
93
|
-
// either fold starts live: the folded strip is a trail, nothing else
|
|
94
|
-
this.chart.attach()
|
|
95
|
-
this.opened()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** unfolded: back to the last ten minutes, the older record fetched, and
|
|
99
|
-
* a height from the lanes as they are */
|
|
73
|
+
/** back to the last ten minutes, the older record fetched, and a height
|
|
74
|
+
* from the lanes as they are */
|
|
100
75
|
private opened(): void {
|
|
101
|
-
if (!this.open) return
|
|
102
76
|
this.chart.home()
|
|
103
77
|
this.chart.attach()
|
|
104
78
|
this.held = { h: 0, lowSince: null }
|
|
@@ -108,8 +82,10 @@ export class LiveSvc {
|
|
|
108
82
|
|
|
109
83
|
/** the height the lanes want now, as the held rule lets it show */
|
|
110
84
|
settle(now: number): void {
|
|
85
|
+
// no runs page on screen, no chart to size: the record is not laid out
|
|
86
|
+
if (this.chart.viewW === 0) return
|
|
111
87
|
const l = this.chart.layout
|
|
112
|
-
const lanes =
|
|
88
|
+
const lanes = l?.lanes ?? 0
|
|
113
89
|
this.held = settleHeight(this.held, Math.min(OPEN_MAX_H, chartHeight(lanes)), now)
|
|
114
90
|
}
|
|
115
91
|
|
|
@@ -139,7 +115,7 @@ export class LiveSvc {
|
|
|
139
115
|
this.view = v
|
|
140
116
|
this.receivedAt = Date.now()
|
|
141
117
|
this.error = ''
|
|
142
|
-
|
|
118
|
+
this.settle(Date.now())
|
|
143
119
|
})
|
|
144
120
|
// a run that ended, from a terminal as much as from a button,
|
|
145
121
|
// wrote new numbers: the rows and the timeline read them now
|
|
@@ -158,20 +134,4 @@ export class LiveSvc {
|
|
|
158
134
|
get running(): boolean {
|
|
159
135
|
return (this.view?.runs.length ?? 0) > 0
|
|
160
136
|
}
|
|
161
|
-
|
|
162
|
-
/** a repo lane was clicked: its hub, when it serves */
|
|
163
|
-
async openRepo(repo: string): Promise<void> {
|
|
164
|
-
try {
|
|
165
|
-
const r = (await (await fetch(this.st.url(`/api/repo-hub?repo=${encodeURIComponent(repo)}`))).json()) as { url: string | null }
|
|
166
|
-
if (r.url != null) window.open(r.url, '_blank')
|
|
167
|
-
else this.say(`${repo} has no hub serving`)
|
|
168
|
-
} catch (e) {
|
|
169
|
-
this.say(`${repo}: ${(e as Error).message}`)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private say(text: string): void {
|
|
174
|
-
runInAction(() => { this.note = text })
|
|
175
|
-
setTimeout(() => runInAction(() => { if (this.note === text) this.note = '' }), 4_000)
|
|
176
|
-
}
|
|
177
137
|
}
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
import { makeAutoObservable, observableRef, runInAction } from 'mobx'
|
|
9
9
|
import type { HomeAnswer, WidgetsAnswer } from '../../home/home-routes.ts'
|
|
10
10
|
import type { WorkerView } from '../../home/hubs.ts'
|
|
11
|
-
import type { ResourceView } from '../../home/resources.ts'
|
|
11
|
+
import type { OpenHow, ResourceView } from '../../home/resources.ts'
|
|
12
12
|
import type { PmAnswer, Role } from '../../home/pm.ts'
|
|
13
|
+
import { processGroups, type ProcessGroup } from '../../home/process-rows.ts'
|
|
14
|
+
import { ClockSvc } from './clock-svc.ts'
|
|
13
15
|
import type { HomeSt } from './home-st.ts'
|
|
14
16
|
import type { HubDot } from './projects-svc.ts'
|
|
15
17
|
|
|
@@ -22,6 +24,9 @@ export const MACHINE_TIMEOUT_MS = 8_000
|
|
|
22
24
|
* on the next load (the overview) while the daemon and pm are asked again */
|
|
23
25
|
export const MACHINE_KEY = 'hub:machine'
|
|
24
26
|
|
|
27
|
+
/** how long a copied pid says so */
|
|
28
|
+
export const COPIED_MS = 1_200
|
|
29
|
+
|
|
25
30
|
type Kept = { data: HomeAnswer | null; pm: PmAnswer | null; widgets: WidgetsAnswer | null }
|
|
26
31
|
|
|
27
32
|
function loadKept(): Kept | null {
|
|
@@ -34,7 +39,12 @@ function loadKept(): Kept | null {
|
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
/** the poll failed because nothing behind the page answers, not on an error of an answer */
|
|
37
|
-
|
|
42
|
+
/** the web (no answer at all) or the daemon behind it (the web says so) */
|
|
43
|
+
class DownError extends Error {
|
|
44
|
+
constructor(msg: string, readonly who: 'web' | 'daemon') {
|
|
45
|
+
super(msg)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
38
48
|
|
|
39
49
|
export type MachineOpts = { reload?: () => void; timeoutMs?: number }
|
|
40
50
|
|
|
@@ -48,11 +58,20 @@ export class MachineSt {
|
|
|
48
58
|
pmDown = ''
|
|
49
59
|
/** the last action that failed */
|
|
50
60
|
error = ''
|
|
61
|
+
/** where open sends a global process's url: the default browser, or a
|
|
62
|
+
* shipkit app window */
|
|
51
63
|
/** why the daemon did not answer the last poll, or '' while it does */
|
|
52
64
|
down = ''
|
|
65
|
+
/** which one did not answer: the web itself, or the daemon behind it */
|
|
66
|
+
downWho: 'web' | 'daemon' = 'daemon'
|
|
53
67
|
/** the daemon answered once since the load: until then data, pm and
|
|
54
68
|
* widgets are what this browser kept, shown but never acted on */
|
|
55
69
|
fresh = false
|
|
70
|
+
/** the one second the process ages move on, ticking while the machine polls */
|
|
71
|
+
clock: ClockSvc
|
|
72
|
+
/** the pid a click just copied, which says so for COPIED_MS */
|
|
73
|
+
copiedPid: number | null = null
|
|
74
|
+
private copiedTimer: ReturnType<typeof setTimeout> | null = null
|
|
56
75
|
/** what was last kept, so an unchanged machine is not written again */
|
|
57
76
|
private kept = ''
|
|
58
77
|
private timer: ReturnType<typeof setInterval> | null = null
|
|
@@ -63,13 +82,14 @@ export class MachineSt {
|
|
|
63
82
|
private pollError = false
|
|
64
83
|
|
|
65
84
|
constructor(private st: HomeSt, private o: MachineOpts = {}) {
|
|
85
|
+
this.clock = new ClockSvc()
|
|
66
86
|
const k = loadKept()
|
|
67
87
|
if (k != null) {
|
|
68
88
|
this.data = k.data
|
|
69
89
|
this.pm = k.pm
|
|
70
90
|
this.widgets = k.widgets
|
|
71
91
|
}
|
|
72
|
-
makeAutoObservable<this, 'st' | 'timer' | 'wasDown' | 'polling' | 'widgetsPolling' | 'pollError' | 'o' | 'kept' | 'keep'>(this, { st: false, timer: false, wasDown: false, polling: false, widgetsPolling: false, pollError: false, o: false, kept: false, keep: false, data: observableRef, pm: observableRef, widgets: observableRef })
|
|
92
|
+
makeAutoObservable<this, 'st' | 'timer' | 'copiedTimer' | 'wasDown' | 'polling' | 'widgetsPolling' | 'pollError' | 'o' | 'kept' | 'keep'>(this, { st: false, clock: false, timer: false, copiedTimer: false, wasDown: false, polling: false, widgetsPolling: false, pollError: false, o: false, kept: false, keep: false, data: observableRef, pm: observableRef, widgets: observableRef })
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
/** the repo the page shows, null on the overview and the plan */
|
|
@@ -85,6 +105,14 @@ export class MachineSt {
|
|
|
85
105
|
return this.worker(this.repo)
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
/** the processes table: shipkit's own first, then one group per repo in
|
|
109
|
+
* the project list's order, one row per process (process-rows.ts, what
|
|
110
|
+
* `shipkit global-processes` prints) */
|
|
111
|
+
get processGroups(): ProcessGroup[] {
|
|
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 })
|
|
114
|
+
}
|
|
115
|
+
|
|
88
116
|
resource(id: string): ResourceView | null {
|
|
89
117
|
return this.pm?.resources.find((r) => r.id === id) ?? null
|
|
90
118
|
}
|
|
@@ -110,10 +138,12 @@ export class MachineSt {
|
|
|
110
138
|
if (this.repo != null && this.data != null) void this.hub(this.repo, 'ensure')
|
|
111
139
|
})
|
|
112
140
|
this.timer = setInterval(() => void this.load(), MACHINE_POLL_MS)
|
|
141
|
+
this.clock.start()
|
|
113
142
|
}
|
|
114
143
|
|
|
115
144
|
stop(): void {
|
|
116
145
|
if (this.timer != null) clearInterval(this.timer)
|
|
146
|
+
this.clock.stop()
|
|
117
147
|
}
|
|
118
148
|
|
|
119
149
|
/** pm is polled beside the daemon: either one down leaves the other shown */
|
|
@@ -127,10 +157,8 @@ export class MachineSt {
|
|
|
127
157
|
this.pmDown = ''
|
|
128
158
|
})
|
|
129
159
|
} catch (e) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.pmDown = (e as Error).message
|
|
133
|
-
})
|
|
160
|
+
// the last answer stays, its rows unknown (processGroups): the table keeps its lines
|
|
161
|
+
runInAction(() => { this.pmDown = (e as Error).message })
|
|
134
162
|
}
|
|
135
163
|
}
|
|
136
164
|
|
|
@@ -162,9 +190,9 @@ export class MachineSt {
|
|
|
162
190
|
try {
|
|
163
191
|
r = await fetch('/api/home.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
|
|
164
192
|
} catch (e) {
|
|
165
|
-
throw new DownError(`the home does not answer: ${(e as Error).message}
|
|
193
|
+
throw new DownError(`the home does not answer: ${(e as Error).message}`, 'web')
|
|
166
194
|
}
|
|
167
|
-
if (r.headers.get('x-shipkit-daemon') === 'down') throw new DownError(await r.text())
|
|
195
|
+
if (r.headers.get('x-shipkit-daemon') === 'down') throw new DownError(await r.text(), 'daemon')
|
|
168
196
|
if (!r.ok) throw new Error(`/api/home.json answered ${r.status}: ${await r.text()}`)
|
|
169
197
|
const j = (await r.json()) as HomeAnswer
|
|
170
198
|
const back = this.wasDown
|
|
@@ -187,7 +215,10 @@ export class MachineSt {
|
|
|
187
215
|
const msg = (e as Error).message
|
|
188
216
|
if (e instanceof DownError) {
|
|
189
217
|
this.wasDown = true
|
|
190
|
-
runInAction(() => {
|
|
218
|
+
runInAction(() => {
|
|
219
|
+
this.down = msg
|
|
220
|
+
this.downWho = e.who
|
|
221
|
+
})
|
|
191
222
|
} else {
|
|
192
223
|
this.pollError = true
|
|
193
224
|
runInAction(() => {
|
|
@@ -243,12 +274,33 @@ export class MachineSt {
|
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
/** a global process: start, stop, restart, take over the copy started
|
|
246
|
-
* elsewhere, open its url
|
|
247
|
-
*
|
|
248
|
-
processAct(id: string, act: 'start' | 'stop' | 'restart' | 'takeover' | 'open' | 'app' | 'action', action?: string): Promise<boolean> {
|
|
277
|
+
* elsewhere, open its url (started first when it is down), or run one
|
|
278
|
+
* of its actions */
|
|
279
|
+
processAct(id: string, act: 'start' | 'stop' | 'restart' | 'takeover' | 'open' | 'chrome' | 'app' | 'action', action?: string): Promise<boolean> {
|
|
249
280
|
return this.post('/api/pm/resource', { id, act, action })
|
|
250
281
|
}
|
|
251
282
|
|
|
283
|
+
/** the url of a global process in the default browser, in Chrome, or as
|
|
284
|
+
* its own app (started first when it is down) */
|
|
285
|
+
openProcess(id: string, how: OpenHow): Promise<boolean> {
|
|
286
|
+
return this.processAct(id, how === 'browser' ? 'open' : how)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** a process's pid to the clipboard, and the "copied" flash on it; a
|
|
290
|
+
* refusal (plain http, an unfocused page) is said in the strip */
|
|
291
|
+
async copyPid(pid: number): Promise<void> {
|
|
292
|
+
try {
|
|
293
|
+
if (globalThis.navigator?.clipboard == null) throw new Error('this page has no clipboard')
|
|
294
|
+
await navigator.clipboard.writeText(String(pid))
|
|
295
|
+
} catch (e) {
|
|
296
|
+
runInAction(() => { this.error = `could not copy pid ${pid}: ${(e as Error).message}` })
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
runInAction(() => { this.copiedPid = pid })
|
|
300
|
+
if (this.copiedTimer != null) clearTimeout(this.copiedTimer)
|
|
301
|
+
this.copiedTimer = setTimeout(() => runInAction(() => { this.copiedPid = null }), COPIED_MS)
|
|
302
|
+
}
|
|
303
|
+
|
|
252
304
|
/** restart the daemon or the web through pm */
|
|
253
305
|
restartChild(id: Role): Promise<boolean> {
|
|
254
306
|
return this.post('/api/pm/child', { id, act: 'restart' })
|
package/src/serve/hub-app/md.ts
CHANGED
|
@@ -8,13 +8,15 @@ import sanitizeHtml from 'sanitize-html'
|
|
|
8
8
|
|
|
9
9
|
marked.use({ gfm: true, breaks: false, async: false })
|
|
10
10
|
|
|
11
|
+
const CARD_TAGS = [
|
|
12
|
+
'h1', 'h2', 'h3', 'h4', 'p', 'ul', 'ol', 'li',
|
|
13
|
+
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
14
|
+
'code', 'pre', 'strong', 'em', 'b', 'i', 'a', 'br', 'blockquote', 'hr',
|
|
15
|
+
]
|
|
16
|
+
|
|
11
17
|
export const renderMd = (src: string): string =>
|
|
12
18
|
sanitizeHtml(marked.parse(src) as string, {
|
|
13
|
-
allowedTags:
|
|
14
|
-
'h1', 'h2', 'h3', 'h4', 'p', 'ul', 'ol', 'li',
|
|
15
|
-
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
|
16
|
-
'code', 'pre', 'strong', 'em', 'b', 'i', 'a', 'br', 'blockquote', 'hr',
|
|
17
|
-
],
|
|
19
|
+
allowedTags: CARD_TAGS,
|
|
18
20
|
allowedAttributes: { a: ['href', 'target', 'rel'] },
|
|
19
21
|
allowedSchemes: ['http', 'https'],
|
|
20
22
|
allowProtocolRelative: false,
|
|
@@ -37,3 +39,34 @@ export const renderMdInline = (src: string): string =>
|
|
|
37
39
|
a: (tagName, attribs) => ({ tagName, attribs: { ...attribs, target: '_blank', rel: 'noreferrer' } }),
|
|
38
40
|
},
|
|
39
41
|
})
|
|
42
|
+
|
|
43
|
+
/** where a README image loads from: an http(s) url as written, a path
|
|
44
|
+
* relative to the repo root through `fileUrl`, anything else (data:, file:,
|
|
45
|
+
* `//host`) nowhere */
|
|
46
|
+
export const readmeImageSrc = (src: string, fileUrl: (rel: string) => string): string | null => {
|
|
47
|
+
const s = src.trim()
|
|
48
|
+
if (/^https?:\/\//i.test(s)) return s
|
|
49
|
+
if (s === '' || s.startsWith('//') || /^[a-z][a-z0-9+.-]*:/i.test(s)) return null
|
|
50
|
+
const rel = s.replace(/[?#].*$/, '').replace(/^(\.\/|\/)+/, '')
|
|
51
|
+
return rel === '' ? null : fileUrl(rel.split('/').map(encodeURIComponent).join('/'))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** a whole README: the card allowlist plus images and the few tags READMEs
|
|
55
|
+
* lean on. A relative image is rewritten to the repo's file route, an image
|
|
56
|
+
* with no loadable source is dropped */
|
|
57
|
+
export const renderReadme = (src: string, fileUrl: (rel: string) => string): string =>
|
|
58
|
+
sanitizeHtml(marked.parse(src) as string, {
|
|
59
|
+
allowedTags: [...CARD_TAGS, 'h5', 'h6', 'img', 'details', 'summary', 'kbd', 'sup', 'sub', 'del'],
|
|
60
|
+
allowedAttributes: { a: ['href', 'target', 'rel'], img: ['src', 'alt', 'title', 'width', 'height'] },
|
|
61
|
+
allowedSchemes: ['http', 'https'],
|
|
62
|
+
allowProtocolRelative: false,
|
|
63
|
+
transformTags: {
|
|
64
|
+
a: (tagName, attribs) => ({ tagName, attribs: { ...attribs, target: '_blank', rel: 'noreferrer' } }),
|
|
65
|
+
img: (tagName, attribs) => {
|
|
66
|
+
const { src: raw, ...rest } = attribs
|
|
67
|
+
const src = readmeImageSrc(raw ?? '', fileUrl)
|
|
68
|
+
return { tagName, attribs: src == null ? rest : { ...rest, src } }
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
exclusiveFilter: (frame) => frame.tag === 'img' && (frame.attribs.src ?? '') === '',
|
|
72
|
+
})
|
|
@@ -44,7 +44,7 @@ export type NextRunFacts = Pick<CheckFacts, 'when' | 'whenReason'>
|
|
|
44
44
|
export function planWords(row: PlanRow, now: number): { words: string; at: number | null } {
|
|
45
45
|
if (row.state === 'running') return { words: 'running now', at: row.dueAt }
|
|
46
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: '
|
|
47
|
+
if (row.state === 'late') return { words: 'waits for the repo to be active', at: row.dueAt }
|
|
48
48
|
if (row.state === 'next' && row.dueAt != null) return { words: `in ${aheadWords(row.dueAt - now)}`, at: row.dueAt }
|
|
49
49
|
return { words: 'on the next save that moves what it reads', at: null }
|
|
50
50
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// the one picker of the page (cmd+
|
|
2
|
-
// home's places (home, planned) first, then the shown repo's own jump
|
|
1
|
+
// the one picker of the page (cmd+j, keys.ts), a node of the root: the
|
|
2
|
+
// home's places (home, activity, planned, in the list's order) first, then the shown repo's own jump
|
|
3
3
|
// targets, then every other house repo as a place to go. Another repo's
|
|
4
4
|
// resources are never listed: a jump to a repo selects it in place, and its
|
|
5
|
-
// own targets are one more cmd+
|
|
5
|
+
// own targets are one more cmd+j away.
|
|
6
6
|
import { makeAutoObservable } from 'mobx'
|
|
7
7
|
import type { HomeSt } from './home-st.ts'
|
|
8
8
|
import type { Jump } from './repo-st.ts'
|
|
@@ -24,24 +24,28 @@ export class PaletteSt {
|
|
|
24
24
|
const st = this.st
|
|
25
25
|
if (st.machine == null) return []
|
|
26
26
|
return [
|
|
27
|
-
{ id: 'place:home', label: 'home', section: 'place', run: () => st.navigate('/') },
|
|
28
|
-
{ id: 'place:
|
|
27
|
+
{ id: 'place:home', label: 'home', section: 'place', face: { place: 'home' }, run: () => st.navigate('/') },
|
|
28
|
+
{ id: 'place:activity', label: 'activity', section: 'place', face: { place: 'activity' }, run: () => st.navigate('/activity') },
|
|
29
|
+
{ id: 'place:planned', label: 'planned', section: 'place', face: { place: 'planned' }, run: () => st.navigate('/planned') },
|
|
30
|
+
{ id: 'place:archived', label: 'archived', section: 'place', face: { place: 'archived' }, run: () => st.navigate('/archived') },
|
|
29
31
|
]
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/** every
|
|
34
|
+
/** every house repo but the one on screen, as a place to go: on a
|
|
35
|
+
* global page that is every repo */
|
|
33
36
|
get others(): Jump[] {
|
|
34
37
|
const st = this.st
|
|
35
38
|
if (st.machine == null) return []
|
|
36
|
-
|
|
39
|
+
const on = st.shown?.key
|
|
40
|
+
return st.projects.list.filter((p) => p.name !== on).map((p) => ({ id: `repo:${p.name}`, label: p.name, section: 'repo', face: { repo: p.name }, run: () => st.select(p.name) }))
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
get jumps(): Jump[] {
|
|
40
44
|
return [...this.places, ...(this.st.shown?.jumps ?? []), ...this.others]
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
/** ranked: prefix matches first, then substring. With no query, home
|
|
44
|
-
* planned, then the shown repo's first targets, then every other repo,
|
|
47
|
+
/** ranked: prefix matches first, then substring. With no query, home,
|
|
48
|
+
* activity and planned, then the shown repo's first targets, then every other repo,
|
|
45
49
|
* so a jump elsewhere is always in reach */
|
|
46
50
|
get results(): Jump[] {
|
|
47
51
|
const q = this.query.trim().toLowerCase()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// the one picker, cmd+j (palette-st.ts): home, activity, planned and
|
|
2
|
+
// archived, the shown repo's targets, then every other repo. A place wears
|
|
3
|
+
// its button's icon, a repo its own icon, as the project list shows them.
|
|
4
|
+
import { observer } from 'mobx-react-lite'
|
|
5
|
+
import type { HomeSt } from './home-st.ts'
|
|
6
|
+
import { Icon, ICON } from './icons.tsx'
|
|
7
|
+
import { Face } from './project-list.tsx'
|
|
8
|
+
import type { JumpFace } from './repo-st.ts'
|
|
9
|
+
|
|
10
|
+
/** the icon a picker entry wears; an empty slot keeps the labels aligned */
|
|
11
|
+
const PalFace = observer(({ app, face }: { app: HomeSt; face: JumpFace | undefined }) => {
|
|
12
|
+
if (face != null && 'place' in face) return <span className="pface"><Icon d={ICON[face.place]} /></span>
|
|
13
|
+
const p = face == null ? null : app.projects.list.find((x) => x.name === face.repo) ?? null
|
|
14
|
+
return <span className="pface">{p == null ? null : <Face p={p} px={16} />}</span>
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
export const Palette = observer(({ app }: { app: HomeSt }) => {
|
|
18
|
+
const p = app.palette
|
|
19
|
+
if (!p.open) return null
|
|
20
|
+
return (
|
|
21
|
+
<div className="palov" onClick={() => p.close()}>
|
|
22
|
+
<div className="pal" onClick={(e) => e.stopPropagation()}>
|
|
23
|
+
<input autoFocus placeholder="jump to…" value={p.query}
|
|
24
|
+
onChange={(e) => p.setQuery(e.target.value)}
|
|
25
|
+
onKeyDown={(e) => {
|
|
26
|
+
if (e.key === 'ArrowDown') { e.preventDefault(); p.move(1) }
|
|
27
|
+
else if (e.key === 'ArrowUp') { e.preventDefault(); p.move(-1) }
|
|
28
|
+
else if (e.key === 'Enter') p.run()
|
|
29
|
+
else if (e.key === 'Escape') p.close()
|
|
30
|
+
}} />
|
|
31
|
+
<div className="prows">
|
|
32
|
+
{p.results.map((jp, n) => (
|
|
33
|
+
<div key={jp.id} className={`prow${n === p.idx ? ' on' : ''}`}
|
|
34
|
+
onMouseEnter={() => p.setIdx(n)}
|
|
35
|
+
onClick={() => p.run(jp)}>
|
|
36
|
+
<PalFace app={app} face={jp.face} />
|
|
37
|
+
<span className="plabel">{jp.label}</span>
|
|
38
|
+
<span className="pchip">{jp.section}</span>
|
|
39
|
+
{jp.key == null ? null : <kbd className="pjkey">{jp.key}</kbd>}
|
|
40
|
+
</div>
|
|
41
|
+
))}
|
|
42
|
+
{p.results.length === 0 ? <div className="prow none">no match</div> : null}
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
})
|