rvlib-shipkit 1.3.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 +37 -0
- 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 +12 -2
- package/src/check.ts +6 -4
- package/src/checks/SK001-package-json-shape.check.ts +20 -2
- package/src/checks/SK009-commit-ai-tells.check.ts +90 -72
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK027-agent-wiki-links.check.ts +30 -8
- package/src/checks/SK028-readme.check.ts +6 -2
- package/src/cli/cmd_CrossRepoActivity.ts +4 -1
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +6 -2
- 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/budget.ts +19 -9
- package/src/core/check-runner.ts +26 -5
- package/src/core/discover-checks.ts +8 -5
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/run-check.ts +2 -5
- package/src/core/run-ci.ts +3 -3
- package/src/core/wire.ts +38 -9
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- 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 +2 -1
- 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 +3 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +29 -5
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +12 -2
- package/src/ledger/gate-checks.ts +3 -3
- package/src/ledger/store.ts +11 -0
- package/src/ledger/world-cache.ts +17 -15
- package/src/repo-config.ts +6 -1
- package/src/serve/hub-app/activity-view.tsx +3 -3
- 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/dock.css +1 -1
- package/src/serve/hub-app/home-st.ts +1 -1
- package/src/serve/hub-app/home-strip.css +32 -17
- package/src/serve/hub-app/home-strip.tsx +99 -89
- package/src/serve/hub-app/hub-app.tsx +7 -1
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +24 -2
- package/src/serve/hub-app/icons.tsx +4 -0
- package/src/serve/hub-app/keys.ts +6 -6
- package/src/serve/hub-app/machine-st.ts +26 -37
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/repo-st.ts +1 -1
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/warm-cell.ts +49 -16
- 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
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
// (header-bar.tsx); on the overview and the plan, the page's name. At the
|
|
6
6
|
// far right, only machine wide trouble, and only while it lasts (pm or the
|
|
7
7
|
// daemon down, a child or a global process crashed). The places themselves
|
|
8
|
-
// are the place buttons at the top of the project list (
|
|
9
|
-
//
|
|
8
|
+
// are the place buttons at the top of the project list (the picker's ⌘J,
|
|
9
|
+
// then ⌘K, ⌘L, ⌘') and the first three entries of the
|
|
10
10
|
// picker. What administers one repo (reload its hub, keep it active, archive
|
|
11
11
|
// it) is its admin page (admin-view.tsx). NotUp stands in for the hub page
|
|
12
12
|
// while its hub cannot answer, with the fix. Every link of the home is a
|
|
13
13
|
// page of the same app: a plain click moves it in place. The daemon or pm
|
|
14
|
-
// down is said in
|
|
15
|
-
//
|
|
14
|
+
// down is said in MachineToasts, floating over the page column's bottom
|
|
15
|
+
// right corner, taking no room.
|
|
16
16
|
import { observer } from 'mobx-react-lite'
|
|
17
17
|
import type { ReactNode } from 'react'
|
|
18
18
|
import type { ProcessIcon, ResourceView } from '../../home/resources.ts'
|
|
19
19
|
import type { WorkerView } from '../../home/hubs.ts'
|
|
20
20
|
import { NOT_INSTALLED } from '../../home/home-words.ts'
|
|
21
21
|
import { splitWidgetHead } from '../../home/widget-text.ts'
|
|
22
|
-
import type { MachineSt
|
|
22
|
+
import type { MachineSt } from './machine-st.ts'
|
|
23
23
|
import type { Role } from '../../home/pm.ts'
|
|
24
24
|
import { processIcon, stateWords, type ProcessRow } from '../../home/process-rows.ts'
|
|
25
25
|
import { plainClick, repoPage, type HomeSt } from './home-st.ts'
|
|
@@ -64,81 +64,81 @@ export const ResourceChip = observer(({ m, r }: { m: MachineSt; r: ResourceView
|
|
|
64
64
|
const Mark = ({ i }: { i: ProcessIcon | null }) =>
|
|
65
65
|
i == null ? <i className="hpico" /> : 'text' in i ? <span className="hpico">{i.text}</span> : <img className="hpico" src={i.src} alt="" width={16} height={16} />
|
|
66
66
|
|
|
67
|
-
/** a
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
/** a process's state in words, on the one clock (`up 2h`, `crashed 3m ago`,
|
|
68
|
+
* `no answer`). What it has to say (its note, a refused take over) is a
|
|
69
|
+
* mark after the words and the hover, never a line of its own, so the
|
|
70
|
+
* table keeps its height whatever a process says */
|
|
71
|
+
const StateCell = observer(({ m, r }: { m: MachineSt; r: ProcessRow }) => {
|
|
72
|
+
const say = [r.note, r.takeOver?.ok === false ? `cannot take over: ${r.takeOver.why}` : null].filter((x) => x != null)
|
|
73
|
+
const title = [r.since == null ? null : `since ${stampWords(r.since)}`, ...say].filter((x) => x != null).join('\n')
|
|
74
|
+
return (
|
|
75
|
+
<td className={`gpstate ${r.dot}`}>
|
|
76
|
+
<span title={title === '' ? undefined : title}>{stateWords(r, m.clock.now)}</span>
|
|
77
|
+
{say.length > 0 ? <i className="gpwarn" /> : null}
|
|
78
|
+
</td>
|
|
79
|
+
)
|
|
80
|
+
})
|
|
71
81
|
|
|
72
82
|
/** the button columns a table holds: a kind no line needs takes none */
|
|
73
83
|
type BtnCols = { act: string | null; take: boolean }
|
|
74
84
|
|
|
75
85
|
/** one process as one line, like a commit on the activity page: its dot,
|
|
76
|
-
* icon and label (cut to its column, whole with its log on hover),
|
|
77
|
-
* as
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
|
|
86
|
+
* icon and label (cut to its column, whole with its log on hover), then
|
|
87
|
+
* its two switches as small icons, start or stop (play while stopped, stop
|
|
88
|
+
* while it runs) and restart, each in a column of its own so every play
|
|
89
|
+
* sits under every play, then its url: open in Chrome, open as its own
|
|
90
|
+
* app (a spinner while that app builds), then the url itself, a link to
|
|
91
|
+
* the default browser. Each starts the process first when it is down. Then
|
|
92
|
+
* its version, its pid (a click copies it), its state, and its
|
|
93
|
+
* declared actions and take over. One of shipkit's own wears a core tag and
|
|
94
|
+
* has its stop disabled: it is always on, restarted through pm (pm itself
|
|
95
|
+
* by launchd). A running line is tinted green, a crashed one red */
|
|
96
|
+
const ProcessLine = observer(({ m, r, repoIcon, head, cols }: { m: MachineSt; r: ProcessRow; repoIcon: string | null; head: ReactNode; cols: BtnCols }) => {
|
|
84
97
|
const on = r.status === 'up' || r.status === 'starting'
|
|
85
98
|
const about = [r.label, ...(r.core ? [r.description] : [`${r.repo}/${r.file}`, r.description, r.where != null ? `on ${r.where}` : null])].filter((x) => x != null).join(' · ')
|
|
86
99
|
const log = r.tail.length > 0 ? `${about}\n\n${r.tail.join('\n')}` : null
|
|
87
|
-
const
|
|
88
|
-
const note = r.takeOver?.ok === false ? `cannot take over: ${r.takeOver.why}` : r.note
|
|
100
|
+
const first = r.status === 'up' ? '' : `start ${r.label}, then `
|
|
89
101
|
const stopTitle = r.core ? `always on: ${r.id === 'pm' ? 'pm is the LaunchAgent com.shipkit.pm, launchd starts it again when it dies' : `restart ${r.label} instead`}`
|
|
90
102
|
: on && !r.canStop ? `${r.label} was started outside pm, so pm cannot stop it` : on ? `stop ${r.label}` : `start ${r.label}`
|
|
103
|
+
const stops = on || r.core
|
|
91
104
|
const copied = r.pid != null && m.copiedPid === r.pid
|
|
92
105
|
return (
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
<
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
106
|
+
<tr className={`gpline ${r.dot}`}>
|
|
107
|
+
{head}
|
|
108
|
+
<td className="gpname">
|
|
109
|
+
<Tip className="gplabel" tipClass="hpop" placement="bottom-start" interactive content={log} data-log={log == null ? undefined : 'yes'}>
|
|
110
|
+
<i className={`hdot ${r.dot}`} /><Mark i={processIcon(r, repoIcon)} /><span title={log != null ? undefined : about}>{r.label}</span>
|
|
111
|
+
{r.core ? <b className="gptag" title="built into shipkit: always on">core</b> : null}
|
|
112
|
+
</Tip>
|
|
113
|
+
</td>
|
|
114
|
+
<td className="gpb stop"><button className={`gpi${stops ? '' : ' go'}`} aria-label={stops ? 'stop' : 'start'} disabled={r.core || r.status === 'stopping' || r.status === 'unknown' || (on && !r.canStop)}
|
|
115
|
+
title={stopTitle} onClick={() => void m.processAct(r.id, on ? 'stop' : 'start')}><Icon d={stops ? ICON.stop : ICON.play} /></button></td>
|
|
116
|
+
<td className="gpb restart">{r.canRestart ? <button className="gpi" aria-label="restart" onClick={() => void (r.core ? m.restartChild(r.id as Role) : m.processAct(r.id, 'restart'))} title={r.core ? `stop the ${r.label} and start it again, through pm` : `stop ${r.label} and start it again`}><Icon d={ICON.restart} /></button> : null}</td>
|
|
117
|
+
<td className="gphost">
|
|
118
|
+
{r.url == null ? null : r.core ? <span>{r.host}</span> : (
|
|
119
|
+
<>
|
|
120
|
+
<button className="gpi" aria-label="open in Chrome" onClick={() => void m.openProcess(r.id, 'chrome')} title={`${first}open ${r.url} in Chrome`}><Icon d={ICON.browser} /></button>
|
|
121
|
+
<button className="gpi" aria-label="open as an app" disabled={r.appBusy} aria-busy={r.appBusy || undefined} onClick={() => void m.openProcess(r.id, 'app')}
|
|
122
|
+
title={r.appBusy ? `building the ${r.label} app, then opening it` : `${first}open ${r.url} as its own app, ${r.label} in the dock`}>{r.appBusy ? <span className="spin" /> : <Icon d={ICON.window} />}</button>
|
|
123
|
+
<a href={r.url} title={`${first}open ${r.url} in the default browser`} onClick={(e) => {
|
|
105
124
|
if (!plainClick(e)) return
|
|
106
125
|
e.preventDefault()
|
|
107
|
-
void m.openProcess(r.id)
|
|
126
|
+
void m.openProcess(r.id, 'browser')
|
|
108
127
|
}}>{r.host}</a>
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
))}</
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
title={stopTitle} onClick={() => void m.processAct(r.id, on ? 'stop' : 'start')}>{on || r.core ? 'stop' : 'start'}</button></td>
|
|
121
|
-
<td className="gpb open">{r.url != null && !r.core ? <button className="ghost" onClick={() => void m.openProcess(r.id)} title={r.status === 'up' ? `open ${r.url} ${via}` : `start ${r.label}, then open ${r.url} ${via}`}>open</button> : null}</td>
|
|
122
|
-
</tr>
|
|
123
|
-
{note == null ? null : <tr className={`gpline ${r.dot}`}><td colSpan={span} className="gpnote hnote" title={note}>{note}</td></tr>}
|
|
124
|
-
</>
|
|
128
|
+
</>
|
|
129
|
+
)}
|
|
130
|
+
</td>
|
|
131
|
+
<td className="gpver">{r.version ?? ''}</td>
|
|
132
|
+
<td className="gppid">{r.pid == null ? null : <button className={copied ? 'gpcopy on' : 'gpcopy'} title={`copy pid ${r.pid}`} onClick={() => void m.copyPid(r.pid!)}>{copied ? 'copied' : r.pid}</button>}</td>
|
|
133
|
+
<StateCell m={m} r={r} />
|
|
134
|
+
{cols.act == null ? null : <td className="gpb act">{r.actions.map((a) => (
|
|
135
|
+
<button key={a.id} className="ghost hsm" disabled={r.running != null} onClick={() => void m.processAct(r.id, 'action', a.id)}>{r.running === a.id ? <span className="spin" /> : null}{a.label}</button>
|
|
136
|
+
))}</td>}
|
|
137
|
+
{!cols.take ? null : <td className="gpb take">{r.takeOver?.ok === true ? <button className="ghost hsm" onClick={() => void m.processAct(r.id, 'takeover')} title={`stop the copy of ${r.label} started elsewhere (the process group listening on its port) and start one pm supervises, in one go`}>take over</button> : null}</td>}
|
|
138
|
+
</tr>
|
|
125
139
|
)
|
|
126
140
|
})
|
|
127
141
|
|
|
128
|
-
const OPENERS: { how: OpenHow; d: string; title: string }[] = [
|
|
129
|
-
{ how: 'browser', d: ICON.browser, title: 'open opens the default browser' },
|
|
130
|
-
{ how: 'app', d: ICON.window, title: 'open opens a shipkit app window' },
|
|
131
|
-
]
|
|
132
|
-
|
|
133
|
-
/** how every open button and url of the table opens, kept per browser */
|
|
134
|
-
const OpenHowPick = observer(({ m }: { m: MachineSt }) => (
|
|
135
|
-
<span className="seg hopen" role="group" aria-label="how open opens">
|
|
136
|
-
{OPENERS.map((o) => (
|
|
137
|
-
<button key={o.how} className={m.openHow === o.how ? 'on' : undefined} aria-pressed={m.openHow === o.how} title={o.title} onClick={() => m.setOpenHow(o.how)}><Icon d={o.d} /></button>
|
|
138
|
-
))}
|
|
139
|
-
</span>
|
|
140
|
-
))
|
|
141
|
-
|
|
142
142
|
/** the repo's icon and plain name, as on the activity page; a plain click shows it in place */
|
|
143
143
|
const ProcessRepo = observer(({ app, name, icon }: { app: HomeSt; name: string; icon: string | null }) => (
|
|
144
144
|
<a className="acrepo" href={repoPage(name)} onClick={(e) => {
|
|
@@ -154,35 +154,33 @@ const ProcessRepo = observer(({ app, name, icon }: { app: HomeSt; name: string;
|
|
|
154
154
|
/** the processes as one table in the activity page's style: shipkit's own
|
|
155
155
|
* first, then a row group per declaring repo in the project list's order,
|
|
156
156
|
* its name in the first column, at the top of its group, then one line per
|
|
157
|
-
* process. The header
|
|
158
|
-
* choice */
|
|
157
|
+
* process. The url's header carries the new window button */
|
|
159
158
|
const ProcessTable = observer(({ app, m }: { app: HomeSt; m: MachineSt }) => {
|
|
160
159
|
const groups = m.processGroups
|
|
161
160
|
const rows = groups.flatMap((g) => g.rows)
|
|
162
161
|
// wide enough for the longest set of declared actions, in the button font
|
|
163
162
|
const act = Math.max(0, ...rows.map((r) => r.actions.reduce((n, a) => n + a.label.length + 3, 0)))
|
|
164
163
|
const cols: BtnCols = { act: act === 0 ? null : `${act}ch`, take: rows.some((r) => r.takeOver?.ok === true) }
|
|
165
|
-
const btns = 3 + (cols.act == null ? 0 : 1) + (cols.take ? 1 : 0)
|
|
166
164
|
return (
|
|
167
165
|
<div className="gpwrap">
|
|
168
166
|
<table className="actable gptable">
|
|
169
167
|
<colgroup>
|
|
170
|
-
<col className="c-name" /><col className="c-gpname" /><col className="c-gphost" /><col className="c-gpver" /><col className="c-gppid" /><col className="c-gpstate" />
|
|
168
|
+
<col className="c-name" /><col className="c-gpname" /><col className="c-gpb stop" /><col className="c-gpb restart" /><col className="c-gphost" /><col className="c-gpver" /><col className="c-gppid" /><col className="c-gpstate" />
|
|
171
169
|
{cols.act == null ? null : <col className="c-gpb act" style={{ width: cols.act }} />}
|
|
172
170
|
{cols.take ? <col className="c-gpb take" /> : null}
|
|
173
|
-
<col className="c-gpb restart" /><col className="c-gpb stop" /><col className="c-gpb open" />
|
|
174
171
|
</colgroup>
|
|
175
172
|
<thead><tr>
|
|
176
|
-
<th>repo</th><th>process</th><th
|
|
177
|
-
<th
|
|
173
|
+
<th>repo</th><th>process</th><th colSpan={2} />
|
|
174
|
+
<th className="gphost gphead"><div><span>url</span><span className="sp" /><button className="ghost hsm" title="open one more window on this page" onClick={() => m.newWindow()}>new window</button></div></th>
|
|
175
|
+
<th className="gpver">version</th><th className="gppid">pid</th><th className="gpstate">state</th>
|
|
176
|
+
{cols.act == null ? null : <th />}{cols.take ? <th /> : null}
|
|
178
177
|
</tr></thead>
|
|
179
178
|
{groups.map((g) => {
|
|
180
179
|
const icon = g.core ? null : app.projects.list.find((p) => p.name === g.repo)?.icon ?? null
|
|
181
|
-
const
|
|
182
|
-
const head = <td rowSpan={lines} className="acname">{g.core ? <span className="acrepo gpcore"><Icon d={ICON.shipkit} /><span>{g.repo}</span></span> : <ProcessRepo app={app} name={g.repo} icon={icon} />}</td>
|
|
180
|
+
const head = <td rowSpan={g.rows.length} className="acname">{g.core ? <span className="acrepo gpcore"><Icon d={ICON.shipkit} /><span>{g.repo}</span></span> : <ProcessRepo app={app} name={g.repo} icon={icon} />}</td>
|
|
183
181
|
return (
|
|
184
182
|
<tbody key={g.core ? ':core' : g.repo} className="acgrp">
|
|
185
|
-
{g.rows.map((r, i) => <ProcessLine key={r.id} m={m} r={r} repoIcon={icon} head={i === 0 ? head : null} cols={cols}
|
|
183
|
+
{g.rows.map((r, i) => <ProcessLine key={r.id} m={m} r={r} repoIcon={icon} head={i === 0 ? head : null} cols={cols} />)}
|
|
186
184
|
</tbody>
|
|
187
185
|
)
|
|
188
186
|
})}
|
|
@@ -191,7 +189,10 @@ const ProcessTable = observer(({ app, m }: { app: HomeSt; m: MachineSt }) => {
|
|
|
191
189
|
)
|
|
192
190
|
})
|
|
193
191
|
|
|
194
|
-
/** every house repo's widgets: the html the daemon last rendered, each under
|
|
192
|
+
/** every house repo's widgets: the html the daemon last rendered, each under
|
|
193
|
+
* its title. An error is a mark on the title, its words on hover, so a card
|
|
194
|
+
* keeps its lines; a card with no html yet says it where the html goes. A
|
|
195
|
+
* widget file that failed to load is one mark after the cards */
|
|
195
196
|
const Monitor = observer(({ m }: { m: MachineSt }) => {
|
|
196
197
|
const w = m.widgets
|
|
197
198
|
if (w == null || (w.widgets.length === 0 && w.errors.length === 0)) return null
|
|
@@ -205,14 +206,18 @@ const Monitor = observer(({ m }: { m: MachineSt }) => {
|
|
|
205
206
|
<h3>
|
|
206
207
|
{x.title}
|
|
207
208
|
{head == null ? null : <span className="hdim"> · <span dangerouslySetInnerHTML={{ __html: head }} /></span>}
|
|
209
|
+
{x.error != null && x.html != null ? <i className="gpwarn" title={x.error} /> : null}
|
|
208
210
|
</h3>
|
|
209
|
-
{x.
|
|
210
|
-
|
|
211
|
+
{x.html != null ? <div dangerouslySetInnerHTML={{ __html: body }} />
|
|
212
|
+
: x.error != null ? <div className="hwerr">{x.error}</div>
|
|
213
|
+
: <p className="twait"><span className="spin" /></p>}
|
|
211
214
|
</section>
|
|
212
215
|
)
|
|
213
216
|
})}
|
|
217
|
+
{w.errors.length === 0 ? null : (
|
|
218
|
+
<span className="hwerr" title={w.errors.join('\n')}><i className="gpwarn" />{w.errors.length} widget file{w.errors.length === 1 ? '' : 's'} failed</span>
|
|
219
|
+
)}
|
|
214
220
|
</div>
|
|
215
|
-
{w.errors.map((e) => <p key={e} className="hnote">{e}</p>)}
|
|
216
221
|
</section>
|
|
217
222
|
)
|
|
218
223
|
})
|
|
@@ -248,10 +253,11 @@ const ShownRepo = observer(({ app, name }: { app: HomeSt; name: string }) => {
|
|
|
248
253
|
* well: the strip states trouble, never health */
|
|
249
254
|
const MachineAlerts = observer(({ m }: { m: MachineSt }) => (
|
|
250
255
|
<>
|
|
251
|
-
{
|
|
256
|
+
{/* pm silent, its last answer is kept for the table and says nothing here */}
|
|
257
|
+
{(m.pmDown !== '' ? [] : m.pm?.children ?? []).filter((c) => c.status === 'crashed' || c.status === 'down').map((c) => (
|
|
252
258
|
<span key={c.id} className="halert" title={c.note ?? c.tail.at(-1) ?? ''}><Dot s={c.status} />{c.id} {STATUS_WORD[c.status]}</span>
|
|
253
259
|
))}
|
|
254
|
-
{(m.pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r) => <ResourceChip key={r.id} m={m} r={r} />)}
|
|
260
|
+
{(m.pmDown !== '' ? [] : m.pm?.resources ?? []).filter((r) => r.status === 'crashed').map((r) => <ResourceChip key={r.id} m={m} r={r} />)}
|
|
255
261
|
{m.error !== '' ? <span className="herr" title={m.error}>{m.error}</span> : null}
|
|
256
262
|
</>
|
|
257
263
|
))
|
|
@@ -273,22 +279,23 @@ export const HomeStrip = observer(({ app }: { app: HomeSt }) => {
|
|
|
273
279
|
<MachineAlerts m={m} />
|
|
274
280
|
{live == null ? null : <RepoOpeners app={live} />}
|
|
275
281
|
</div>
|
|
276
|
-
<
|
|
282
|
+
<MachineToasts app={app} />
|
|
277
283
|
</>
|
|
278
284
|
)
|
|
279
285
|
})
|
|
280
286
|
|
|
281
|
-
/** the daemon or pm not answering,
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
|
|
287
|
+
/** the daemon or pm not answering, as toasts floating over the bottom right
|
|
288
|
+
* corner of the page column, just above the dock. They take no room, so
|
|
289
|
+
* one coming or going moves nothing, and nothing is drawn while all is
|
|
290
|
+
* well. Only a failed probe sets down or pmDown: before the first answer
|
|
291
|
+
* there is none */
|
|
292
|
+
export const MachineToasts = observer(({ app }: { app: HomeSt }) => {
|
|
286
293
|
const m = app.machine
|
|
287
|
-
if (m == null) return null
|
|
294
|
+
if (m == null || (m.down === '' && m.pmDown === '')) return null
|
|
288
295
|
return (
|
|
289
|
-
<div className="
|
|
290
|
-
{m.down !== '' ? <
|
|
291
|
-
{m.pmDown !== '' ? <
|
|
296
|
+
<div className="htoasts">
|
|
297
|
+
{m.down !== '' ? <div className="htoast" role="alert" title={m.down}><b>daemon down</b> {m.down}</div> : null}
|
|
298
|
+
{m.pmDown !== '' ? <div className="htoast" role="alert" title={m.pmDown}><b>pm down</b> {m.pmDown}</div> : null}
|
|
292
299
|
</div>
|
|
293
300
|
)
|
|
294
301
|
})
|
|
@@ -358,7 +365,10 @@ export const ArchivedView = observer(({ app }: { app: HomeSt }) => {
|
|
|
358
365
|
export const Overview = observer(({ app }: { app: HomeSt }) => {
|
|
359
366
|
const m = app.machine
|
|
360
367
|
if (m == null) return null
|
|
361
|
-
|
|
368
|
+
// the table waits for pm's first word and for the list's order, so its
|
|
369
|
+
// rows never arrive or reorder after it paints
|
|
370
|
+
const p = app.projects
|
|
371
|
+
if (m.data == null || (m.pm == null && m.pmDown === '') || (p.list.length === 0 && !p.fresh && p.error === '')) return <div className="hwait"><p className="twait"><span className="spin" />{m.down !== '' ? 'the daemon does not answer yet' : 'loading…'}</p></div>
|
|
362
372
|
return (
|
|
363
373
|
<div className="hall">
|
|
364
374
|
<div className="hstack">
|
|
@@ -11,6 +11,7 @@ import { observer } from 'mobx-react-lite'
|
|
|
11
11
|
import { StrictMode, useEffect, useLayoutEffect, useRef } from 'react'
|
|
12
12
|
import { createRoot } from 'react-dom/client'
|
|
13
13
|
import { ColCard, ColGrid, StatusCard } from './col-card.tsx'
|
|
14
|
+
import { AskTable, CheckTable } from './col-tables-view.tsx'
|
|
14
15
|
import { WIKIS, WikisView } from './wikis-view.tsx'
|
|
15
16
|
import { ComfyOverlay, ComfyView } from './comfy-view.tsx'
|
|
16
17
|
import type { Filter, RepoSt } from './repo-st.ts'
|
|
@@ -84,7 +85,12 @@ const Main = observer(({ app }: { app: RepoSt }) => {
|
|
|
84
85
|
: <div className="stagewrap"><ColCard app={app} col={app.sel.col} id={app.sel.id} /></div>}
|
|
85
86
|
</main>
|
|
86
87
|
)
|
|
87
|
-
if (app.sel?.t === 'col') return
|
|
88
|
+
if (app.sel?.t === 'col') return (
|
|
89
|
+
<main>
|
|
90
|
+
{errs}
|
|
91
|
+
{app.sel.id === 'checks' ? <CheckTable app={app} id={app.sel.id} /> : app.sel.id === 'asks' ? <AskTable app={app} id={app.sel.id} /> : <ColGrid app={app} id={app.sel.id} />}
|
|
92
|
+
</main>
|
|
93
|
+
)
|
|
88
94
|
if (app.sel?.t === 'tests') return <main>{errs}<div className="stagewrap"><TestsPage app={app} check={app.sel.check} /></div></main>
|
|
89
95
|
if (app.sel?.t === 'admin') return <main>{errs}<div className="stagewrap"><AdminPage app={app} /></div></main>
|
|
90
96
|
if (app.sel?.t === 'status') return <main>{errs}<div className="stagewrap"><StatusCard app={app} /></div></main>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Wire types the hub app reads — mirrors of the server shapes
|
|
2
2
|
// (studio-server.ts, collection-types.ts). Kept structural on purpose: the
|
|
3
3
|
// page renders what the API sends, the file is the truth.
|
|
4
|
-
import type { ResourceType, SuiteFacts, TestFacts, TestRun } from '../../collections/collection-types.ts'
|
|
4
|
+
import type { AskFacts, ResourceType, SuiteFacts, TestFacts, TestRun } from '../../collections/collection-types.ts'
|
|
5
5
|
|
|
6
6
|
export type SideAsset = {
|
|
7
7
|
id: string
|
|
@@ -62,7 +62,7 @@ export type Artifact = { copy?: string; url?: string; steps?: string[] }
|
|
|
62
62
|
* The list lives where the providers that emit it live, so a kind can never
|
|
63
63
|
* be known on one side of the wire only. Type only, so nothing server side
|
|
64
64
|
* reaches the bundle */
|
|
65
|
-
export type { ResourceType, SuiteFacts, TestFacts, TestRun }
|
|
65
|
+
export type { AskFacts, ResourceType, SuiteFacts, TestFacts, TestRun }
|
|
66
66
|
|
|
67
67
|
export type CheckStatus = 'pass' | 'warn' | 'fail' | 'skip'
|
|
68
68
|
|
|
@@ -73,6 +73,7 @@ export type RecentRun = { at: number; ms: number; status: CheckStatus; replayed:
|
|
|
73
73
|
// engine reaches the bundle: two declarations of one vocabulary is how a
|
|
74
74
|
// switch ends up writing a word the other half does not know
|
|
75
75
|
import type { WhenAt, WhenSource } from '../../core/when.ts'
|
|
76
|
+
import type { Line } from '../../core/wire.ts'
|
|
76
77
|
import type { CheckFrom } from '../../check.ts'
|
|
77
78
|
export type { CheckFrom, WhenAt, WhenSource }
|
|
78
79
|
|
|
@@ -154,7 +155,7 @@ export type CheckEvent = {
|
|
|
154
155
|
replayed: boolean
|
|
155
156
|
blocking: boolean | null
|
|
156
157
|
advisory: boolean
|
|
157
|
-
lines:
|
|
158
|
+
lines: Line[]
|
|
158
159
|
}
|
|
159
160
|
/** the colour of the tree label: red broken · warn taste pending · ok
|
|
160
161
|
* passing on a check that ran · idle the neutral default · quiet archived or out of play */
|
|
@@ -186,6 +187,8 @@ export type Resource = {
|
|
|
186
187
|
check?: CheckFacts
|
|
187
188
|
/** test files only: where it runs and what it last cost */
|
|
188
189
|
test?: TestFacts
|
|
190
|
+
/** asks only: kind, dates, what it blocks, its probe */
|
|
191
|
+
ask?: AskFacts
|
|
189
192
|
}
|
|
190
193
|
export type ColMeta = { id: string; title: string; media: string }
|
|
191
194
|
export type Col = ColMeta & { instances: Resource[] }
|
|
@@ -38,7 +38,8 @@ a,a:visited{color:var(--accent)}
|
|
|
38
38
|
.pjpanel,.appcol,.pside,.pmain{height:100%;min-width:0}
|
|
39
39
|
.apptop,.pdock{height:100%;min-height:0}
|
|
40
40
|
.pjpanel{display:flex}
|
|
41
|
-
.
|
|
41
|
+
/* the box the machine toasts float in (home-strip.css .htoasts) */
|
|
42
|
+
.appcol{position:relative;display:flex;flex-direction:column}
|
|
42
43
|
/* one region per repo visited, the shown one alone displayed */
|
|
43
44
|
.repo{flex:1;min-height:0;display:flex;flex-direction:column}
|
|
44
45
|
.repo[hidden]{display:none}
|
|
@@ -628,6 +629,27 @@ a.tok{color:var(--accent)}
|
|
|
628
629
|
.cghead{font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:var(--accent);
|
|
629
630
|
display:flex;gap:var(--s2);align-items:baseline;margin:0 0 var(--s3)}
|
|
630
631
|
.cghead .cnt{font-size:11px;color:var(--dim);letter-spacing:0;text-transform:none}
|
|
632
|
+
/* the checks and asks pages: one dense table each (col-tables-view.tsx), on
|
|
633
|
+
the activity table (.actable), a group header per state that folds */
|
|
634
|
+
.ctpage{flex:1;min-height:0;overflow-y:auto;padding:var(--s3) var(--s4);font-size:12.5px;container-type:inline-size}
|
|
635
|
+
.cttable{font-variant-numeric:tabular-nums}
|
|
636
|
+
.cttable td{padding:var(--s0) var(--s2);vertical-align:baseline}
|
|
637
|
+
.ctgrp td{padding:var(--s2) var(--s2) var(--s1);color:var(--dim);font-size:11.5px;border-bottom:1px solid var(--line);cursor:pointer;user-select:none}
|
|
638
|
+
.ctgrp:hover td{color:var(--ink)}
|
|
639
|
+
.ctgrp.g-fail td{color:var(--red)}
|
|
640
|
+
.ctgrp.g-warn td{color:var(--warn)}
|
|
641
|
+
.ctcaret{display:inline-block;width:14px}
|
|
642
|
+
.ctrow{cursor:pointer}
|
|
643
|
+
.ctrow:hover{background:var(--panel2)}
|
|
644
|
+
.ctdot{font-size:10px}
|
|
645
|
+
.ctid,.ctkind{font:11.5px ui-monospace,Menlo,monospace}
|
|
646
|
+
.ctfind{font:11.5px ui-monospace,Menlo,monospace;color:var(--dim)}
|
|
647
|
+
.ctdur{text-align:right;font:11.5px ui-monospace,Menlo,monospace}
|
|
648
|
+
.ctbudget,.ctplan,.ctblocks{color:var(--dim)}
|
|
649
|
+
.ctprobe{font-size:11.5px}
|
|
650
|
+
.cttable .c-dot{width:22px}.cttable .c-id{width:140px}.cttable .c-title{width:16%}.cttable .c-age{width:72px}.cttable .c-dur{width:152px}.cttable .c-plan{width:88px}
|
|
651
|
+
.cttable .c-kind{width:92px}.cttable .c-blocks{width:32%}.cttable .c-probe{width:84px}
|
|
652
|
+
@container (max-width:760px){.cttable .c-title{width:0}.cttable .cttitle{display:none}.cttable .c-plan{width:0}.cttable .ctplan{display:none}}
|
|
631
653
|
.cgcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:var(--s3)}
|
|
632
654
|
.cgcard{background:var(--panel);border:1px solid var(--line);border-radius:5px;padding:var(--s2) var(--s3);cursor:pointer;min-width:0}
|
|
633
655
|
.cgcard:hover{border-color:var(--accent)}
|
|
@@ -694,7 +716,7 @@ a.mint{color:var(--accent);font-size:12px;text-decoration:none}
|
|
|
694
716
|
a.mint:hover{text-decoration:underline}
|
|
695
717
|
.colcard h3 .stchip,.colcard h3 a.mint{margin-left:var(--s2)}
|
|
696
718
|
|
|
697
|
-
/* ── the cmd+
|
|
719
|
+
/* ── the cmd+j picker ── */
|
|
698
720
|
.palov{position:fixed;inset:0;background:var(--scrim);z-index:100;display:flex;justify-content:center;align-items:flex-start;
|
|
699
721
|
padding-top:14vh}
|
|
700
722
|
.pal{width:min(560px,90vw);background:var(--panel);border:1px solid var(--line);border-radius:3px;
|
|
@@ -27,6 +27,10 @@ export const ICON = {
|
|
|
27
27
|
// the VS Code mark, its ribbon drawn as one outline
|
|
28
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
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',
|
|
30
34
|
} as const
|
|
31
35
|
|
|
32
36
|
export const Icon = ({ d }: { d: string }) => (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// the page's command keys, read the same on every layout: ⌘1 to ⌘9 select the
|
|
2
2
|
// nth repo of the project list, then the home row under the right hand: ⌘J
|
|
3
|
-
// the
|
|
3
|
+
// the picker, ⌘K the overview, ⌘L the activity, ⌘' the plan. ⌘B cycles the
|
|
4
4
|
// dock at the bottom of the page (dock-st.ts): the line, slim, open.
|
|
5
5
|
// A digit and the quote are read from the physical key (`code`), so an
|
|
6
6
|
// AZERTY keyboard, where the key under 1 types `&`, selects the same repo. Ctrl stands in for
|
|
@@ -17,17 +17,17 @@ export function pageKey(e: KeyLike): PageKey | null {
|
|
|
17
17
|
if (e.shiftKey) return null
|
|
18
18
|
if (e.code === 'Quote' || e.key === "'") return { t: 'planned' }
|
|
19
19
|
const k = e.key.toLowerCase()
|
|
20
|
-
if (k === 'j') return { t: '
|
|
21
|
-
if (k === 'k') return { t: '
|
|
20
|
+
if (k === 'j') return { t: 'picker' }
|
|
21
|
+
if (k === 'k') return { t: 'home' }
|
|
22
22
|
if (k === 'l') return { t: 'activity' }
|
|
23
23
|
if (k === 'b') return { t: 'dock' }
|
|
24
24
|
return null
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
/** the shortcut each place of the home carries on its button */
|
|
28
|
-
export const PLACE_KEY = { overview: '⌘
|
|
29
|
-
/** the picker's shortcut, on the
|
|
30
|
-
export const PICKER_KEY = '⌘
|
|
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
31
|
/** the dock's shortcut, on the dock's own header */
|
|
32
32
|
export const DOCK_KEY = '⌘B'
|
|
33
33
|
|
|
@@ -8,7 +8,7 @@
|
|
|
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
13
|
import { processGroups, type ProcessGroup } from '../../home/process-rows.ts'
|
|
14
14
|
import { ClockSvc } from './clock-svc.ts'
|
|
@@ -27,18 +27,6 @@ export const MACHINE_KEY = 'hub:machine'
|
|
|
27
27
|
/** how long a copied pid says so */
|
|
28
28
|
export const COPIED_MS = 1_200
|
|
29
29
|
|
|
30
|
-
/** how open opens a global process's url, kept per browser */
|
|
31
|
-
export const OPEN_HOW_KEY = 'hub:open-how'
|
|
32
|
-
export type OpenHow = 'browser' | 'app'
|
|
33
|
-
|
|
34
|
-
function loadOpenHow(): OpenHow {
|
|
35
|
-
try {
|
|
36
|
-
return globalThis.localStorage?.getItem(OPEN_HOW_KEY) === 'app' ? 'app' : 'browser'
|
|
37
|
-
} catch {
|
|
38
|
-
return 'browser'
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
30
|
type Kept = { data: HomeAnswer | null; pm: PmAnswer | null; widgets: WidgetsAnswer | null }
|
|
43
31
|
|
|
44
32
|
function loadKept(): Kept | null {
|
|
@@ -51,7 +39,12 @@ function loadKept(): Kept | null {
|
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
/** the poll failed because nothing behind the page answers, not on an error of an answer */
|
|
54
|
-
|
|
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
|
+
}
|
|
55
48
|
|
|
56
49
|
export type MachineOpts = { reload?: () => void; timeoutMs?: number }
|
|
57
50
|
|
|
@@ -67,9 +60,10 @@ export class MachineSt {
|
|
|
67
60
|
error = ''
|
|
68
61
|
/** where open sends a global process's url: the default browser, or a
|
|
69
62
|
* shipkit app window */
|
|
70
|
-
openHow: OpenHow = loadOpenHow()
|
|
71
63
|
/** why the daemon did not answer the last poll, or '' while it does */
|
|
72
64
|
down = ''
|
|
65
|
+
/** which one did not answer: the web itself, or the daemon behind it */
|
|
66
|
+
downWho: 'web' | 'daemon' = 'daemon'
|
|
73
67
|
/** the daemon answered once since the load: until then data, pm and
|
|
74
68
|
* widgets are what this browser kept, shown but never acted on */
|
|
75
69
|
fresh = false
|
|
@@ -115,7 +109,8 @@ export class MachineSt {
|
|
|
115
109
|
* the project list's order, one row per process (process-rows.ts, what
|
|
116
110
|
* `shipkit global-processes` prints) */
|
|
117
111
|
get processGroups(): ProcessGroup[] {
|
|
118
|
-
|
|
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 })
|
|
119
114
|
}
|
|
120
115
|
|
|
121
116
|
resource(id: string): ResourceView | null {
|
|
@@ -162,10 +157,8 @@ export class MachineSt {
|
|
|
162
157
|
this.pmDown = ''
|
|
163
158
|
})
|
|
164
159
|
} catch (e) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
this.pmDown = (e as Error).message
|
|
168
|
-
})
|
|
160
|
+
// the last answer stays, its rows unknown (processGroups): the table keeps its lines
|
|
161
|
+
runInAction(() => { this.pmDown = (e as Error).message })
|
|
169
162
|
}
|
|
170
163
|
}
|
|
171
164
|
|
|
@@ -197,9 +190,9 @@ export class MachineSt {
|
|
|
197
190
|
try {
|
|
198
191
|
r = await fetch('/api/home.json', { signal: AbortSignal.timeout(this.o.timeoutMs ?? MACHINE_TIMEOUT_MS) })
|
|
199
192
|
} catch (e) {
|
|
200
|
-
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')
|
|
201
194
|
}
|
|
202
|
-
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')
|
|
203
196
|
if (!r.ok) throw new Error(`/api/home.json answered ${r.status}: ${await r.text()}`)
|
|
204
197
|
const j = (await r.json()) as HomeAnswer
|
|
205
198
|
const back = this.wasDown
|
|
@@ -222,7 +215,10 @@ export class MachineSt {
|
|
|
222
215
|
const msg = (e as Error).message
|
|
223
216
|
if (e instanceof DownError) {
|
|
224
217
|
this.wasDown = true
|
|
225
|
-
runInAction(() => {
|
|
218
|
+
runInAction(() => {
|
|
219
|
+
this.down = msg
|
|
220
|
+
this.downWho = e.who
|
|
221
|
+
})
|
|
226
222
|
} else {
|
|
227
223
|
this.pollError = true
|
|
228
224
|
runInAction(() => {
|
|
@@ -278,16 +274,16 @@ export class MachineSt {
|
|
|
278
274
|
}
|
|
279
275
|
|
|
280
276
|
/** a global process: start, stop, restart, take over the copy started
|
|
281
|
-
* elsewhere, open its url
|
|
282
|
-
*
|
|
283
|
-
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> {
|
|
284
280
|
return this.post('/api/pm/resource', { id, act, action })
|
|
285
281
|
}
|
|
286
282
|
|
|
287
|
-
/** the url of a global process
|
|
288
|
-
* first when it is down) */
|
|
289
|
-
openProcess(id: string): Promise<boolean> {
|
|
290
|
-
return this.processAct(id,
|
|
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)
|
|
291
287
|
}
|
|
292
288
|
|
|
293
289
|
/** a process's pid to the clipboard, and the "copied" flash on it; a
|
|
@@ -305,13 +301,6 @@ export class MachineSt {
|
|
|
305
301
|
this.copiedTimer = setTimeout(() => runInAction(() => { this.copiedPid = null }), COPIED_MS)
|
|
306
302
|
}
|
|
307
303
|
|
|
308
|
-
setOpenHow(how: OpenHow): void {
|
|
309
|
-
this.openHow = how
|
|
310
|
-
try {
|
|
311
|
-
globalThis.localStorage?.setItem(OPEN_HOW_KEY, how)
|
|
312
|
-
} catch { /* private window or blocked site data: the pick lasts the tab */ }
|
|
313
|
-
}
|
|
314
|
-
|
|
315
304
|
/** restart the daemon or the web through pm */
|
|
316
305
|
restartChild(id: Role): Promise<boolean> {
|
|
317
306
|
return this.post('/api/pm/child', { id, act: 'restart' })
|