@north-light/crouter 0.3.158 → 0.3.159
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/dist/builtin-memory/internal/storage-tiers.md +2 -0
- package/dist/clients/attach/chrome/bash-jobs.d.ts +0 -5
- package/dist/clients/attach/chrome/bash-jobs.js +5 -24
- package/dist/clients/attach/chrome/roster.d.ts +3 -2
- package/dist/clients/attach/chrome/roster.js +11 -35
- package/dist/clients/attach/viewer.js +479 -481
- package/dist/commands/node.js +141 -3
- package/dist/commands/surface-inspect.js +4 -3
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +5 -3
- package/dist/core/bash-jobs.d.ts +11 -0
- package/dist/core/bash-jobs.js +41 -1
- package/dist/core/feed/inbox.d.ts +3 -3
- package/dist/core/feed/inbox.js +10 -6
- package/dist/core/inspector/core.d.ts +17 -2
- package/dist/core/inspector/core.js +172 -23
- package/dist/core/inspector/model.d.ts +30 -1
- package/dist/core/inspector/model.js +39 -0
- package/dist/core/inspector/text.js +14 -1
- package/dist/core/inspector/tui.js +71 -3
- package/dist/core/keybindings/__tests__/resolve.test.js +1 -1
- package/dist/core/keybindings/catalog.d.ts +2 -2
- package/dist/core/keybindings/catalog.js +3 -2
- package/dist/core/tui/host.js +21 -6
- package/dist/pi-extensions/canvas-bash-valve.js +5 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
package/dist/commands/node.js
CHANGED
|
@@ -27,7 +27,8 @@ import { profilesStateBlock } from '../core/profiles/state-block.js';
|
|
|
27
27
|
import { stateBlock } from '../core/help.js';
|
|
28
28
|
import { fuzzyMatch } from '../core/canvas/browse/model.js';
|
|
29
29
|
import { loadPins, savePins } from '../core/canvas/browse/pins.js';
|
|
30
|
-
import {
|
|
30
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
31
|
+
import { activeBackgroundBashJobs, backgroundRunningBashJobs, bashJobPaths, formatBashElapsed, jobStartedAtMs, readJobPgid, tailJobLog } from '../core/bash-jobs.js';
|
|
31
32
|
// The API seam — every canvas mutation/read routes through the daemon (B-1).
|
|
32
33
|
import { cliClient, cliCanvasSource, ApiCanvasSource, rethrowAsCliError, getNodeOrNull } from './api-client.js';
|
|
33
34
|
// Past this much context, an ORCHESTRATOR that spawns a managed child is better
|
|
@@ -842,16 +843,153 @@ const nodeBashBackground = defineLeaf({
|
|
|
842
843
|
return `Handed ${result['backgrounded']} bash command(s) to the background job system for ${result['node_id']} · ran ${formatBashElapsed(oldestElapsedMs)} in the foreground.`;
|
|
843
844
|
},
|
|
844
845
|
});
|
|
846
|
+
const nodeBashList = defineLeaf({
|
|
847
|
+
name: 'list',
|
|
848
|
+
description: 'list a node’s live background bash jobs',
|
|
849
|
+
whenToUse: 'the Inspector’s jobs section reads this to show what a node handed off to the background',
|
|
850
|
+
tier: 'hidden',
|
|
851
|
+
help: {
|
|
852
|
+
name: 'node bash list',
|
|
853
|
+
summary: 'read every still-running background bash job owned by one node',
|
|
854
|
+
params: [
|
|
855
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node whose background jobs to list. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
|
|
856
|
+
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE.' },
|
|
857
|
+
{ kind: 'flag', name: 'tail', type: 'int', required: false, default: 0, constraint: 'Include this many trailing log lines per job (0 = none).' },
|
|
858
|
+
],
|
|
859
|
+
output: [
|
|
860
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The target node.' },
|
|
861
|
+
{ name: 'jobs', type: 'object[]', required: true, constraint: 'Oldest-first {job_id, command, started_at, elapsed_ms, log_path, pgid, log_tail} for every live background job.' },
|
|
862
|
+
],
|
|
863
|
+
outputKind: 'object',
|
|
864
|
+
effects: ['Reads the node context jobs directory. Changes nothing.'],
|
|
865
|
+
},
|
|
866
|
+
run: async (input) => {
|
|
867
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
868
|
+
let id = input['node'];
|
|
869
|
+
if (id === undefined || id === '')
|
|
870
|
+
id = nodeInPane(pane);
|
|
871
|
+
if (id === undefined || id === '')
|
|
872
|
+
id = process.env['CRTR_NODE_ID'];
|
|
873
|
+
if (id === undefined || id === '') {
|
|
874
|
+
throw new InputError({ error: 'no_node', message: 'no node found for the bash job list', next: 'Run from inside a node, pass --node <id>, or --pane <pane>.' });
|
|
875
|
+
}
|
|
876
|
+
const node = await getNodeOrNull(id);
|
|
877
|
+
if (node === null) {
|
|
878
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
879
|
+
}
|
|
880
|
+
const now = Date.now();
|
|
881
|
+
const tail = Math.max(0, Math.trunc(Number(input['tail'] ?? 0)));
|
|
882
|
+
const dir = contextDir(node.node_id);
|
|
883
|
+
const jobs = activeBackgroundBashJobs(dir).map((job) => ({
|
|
884
|
+
job_id: job.jobId,
|
|
885
|
+
command: job.command,
|
|
886
|
+
started_at: new Date(job.startedAtMs).toISOString(),
|
|
887
|
+
elapsed_ms: now - job.startedAtMs,
|
|
888
|
+
log_path: job.logPath,
|
|
889
|
+
pgid: readJobPgid(dir, job.jobId) ?? null,
|
|
890
|
+
log_tail: tailJobLog(job.logPath, tail),
|
|
891
|
+
}));
|
|
892
|
+
return { node_id: node.node_id, jobs };
|
|
893
|
+
},
|
|
894
|
+
render: (result) => {
|
|
895
|
+
const jobs = result['jobs'];
|
|
896
|
+
if (jobs.length === 0)
|
|
897
|
+
return `No background bash jobs for ${result['node_id']}.`;
|
|
898
|
+
return jobs.map((job) => `${job.job_id} · ${formatBashElapsed(job.elapsed_ms)} · ${job.command.split('\n')[0]}\n log: ${job.log_path}`).join('\n');
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
/** SIGTERM the job's process group, give it a moment, then SIGKILL. Returns
|
|
902
|
+
* false only when the group was already gone at the first signal. */
|
|
903
|
+
function stopJobGroup(pgid) {
|
|
904
|
+
try {
|
|
905
|
+
process.kill(-pgid, 'SIGTERM');
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
const nodeBashKill = defineLeaf({
|
|
913
|
+
name: 'kill',
|
|
914
|
+
description: 'stop one of a node’s background bash jobs',
|
|
915
|
+
whenToUse: 'a human watching the Inspector wants a long-running background job to stop now, or an agent is abandoning work it backgrounded',
|
|
916
|
+
tier: 'hidden',
|
|
917
|
+
help: {
|
|
918
|
+
name: 'node bash kill',
|
|
919
|
+
summary: 'terminate a background bash job’s process group and tell the owning node it was canceled',
|
|
920
|
+
params: [
|
|
921
|
+
{ kind: 'positional', name: 'job', required: true, constraint: 'Job id, as reported by node bash list.' },
|
|
922
|
+
{ kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID).' },
|
|
923
|
+
{ kind: 'flag', name: 'pane', type: 'string', required: false, constraint: 'tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE.' },
|
|
924
|
+
],
|
|
925
|
+
output: [
|
|
926
|
+
{ name: 'node_id', type: 'string', required: true, constraint: 'The owning node.' },
|
|
927
|
+
{ name: 'job_id', type: 'string', required: true, constraint: 'The job that was stopped.' },
|
|
928
|
+
{ name: 'signaled', type: 'boolean', required: true, constraint: 'True when the process group was still alive and received SIGTERM.' },
|
|
929
|
+
],
|
|
930
|
+
outputKind: 'object',
|
|
931
|
+
effects: [
|
|
932
|
+
'Sends SIGTERM (then SIGKILL) to the job’s whole process group, stopping the command and anything it forked.',
|
|
933
|
+
'Records the job’s exit so it stops showing as live, and sends the owning node an urgent inbox message saying the job was canceled — which steers that node mid-turn.',
|
|
934
|
+
],
|
|
935
|
+
},
|
|
936
|
+
run: async (input) => {
|
|
937
|
+
const pane = input['pane'] ?? process.env['TMUX_PANE'];
|
|
938
|
+
let id = input['node'];
|
|
939
|
+
if (id === undefined || id === '')
|
|
940
|
+
id = nodeInPane(pane);
|
|
941
|
+
if (id === undefined || id === '')
|
|
942
|
+
id = process.env['CRTR_NODE_ID'];
|
|
943
|
+
if (id === undefined || id === '') {
|
|
944
|
+
throw new InputError({ error: 'no_node', message: 'no node found for the bash job', next: 'Run from inside a node, pass --node <id>, or --pane <pane>.' });
|
|
945
|
+
}
|
|
946
|
+
const node = await getNodeOrNull(id);
|
|
947
|
+
if (node === null) {
|
|
948
|
+
throw new InputError({ error: 'not_found', message: `no node: ${id}`, next: 'List nodes with `crtr node inspect list`.' });
|
|
949
|
+
}
|
|
950
|
+
const jobId = input['job'];
|
|
951
|
+
const dir = contextDir(node.node_id);
|
|
952
|
+
const paths = bashJobPaths(dir, jobId);
|
|
953
|
+
if (!existsSync(paths.dir)) {
|
|
954
|
+
throw new InputError({ error: 'not_found', message: `no background job ${jobId} for ${node.node_id}`, next: 'List live jobs with `crtr node bash list`.' });
|
|
955
|
+
}
|
|
956
|
+
const pgid = readJobPgid(dir, jobId);
|
|
957
|
+
if (pgid === undefined) {
|
|
958
|
+
throw new InputError({
|
|
959
|
+
error: 'not_stoppable',
|
|
960
|
+
message: `job ${jobId} recorded no process group`,
|
|
961
|
+
next: `It started before crtr persisted one. Stop it by hand, then it will disappear from the list. Log: ${paths.jobLog}`,
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
const signaled = stopJobGroup(pgid);
|
|
965
|
+
// The supervisor shares the group it leads, so it usually dies with the
|
|
966
|
+
// command and never writes job.exit. Record the cancellation ourselves —
|
|
967
|
+
// that sentinel is what retires the job from every live-jobs read.
|
|
968
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
969
|
+
try {
|
|
970
|
+
process.kill(-pgid, 'SIGKILL');
|
|
971
|
+
}
|
|
972
|
+
catch { /* already gone */ }
|
|
973
|
+
if (!existsSync(paths.jobExit))
|
|
974
|
+
writeFileSync(paths.jobExit, '143\n');
|
|
975
|
+
await cliClient().sendMessage(node.node_id, {
|
|
976
|
+
body: `Background bash job ${jobId} was canceled from the Inspector before it finished. Log: ${paths.jobLog} Command: ${paths.cmdSh}`,
|
|
977
|
+
tier: 'urgent',
|
|
978
|
+
});
|
|
979
|
+
return { node_id: node.node_id, job_id: jobId, signaled };
|
|
980
|
+
},
|
|
981
|
+
render: (result) => `Canceled background bash job ${result['job_id']} for ${result['node_id']}${result['signaled'] === true ? '' : ' (its process group was already gone)'}.`,
|
|
982
|
+
});
|
|
845
983
|
const nodeBash = defineBranch({
|
|
846
984
|
name: 'bash',
|
|
847
985
|
description: 'control file-backed bash jobs owned by a node',
|
|
848
|
-
whenToUse: '
|
|
986
|
+
whenToUse: 'a running bash command must be handed off, listed, or stopped — the tmux prefix menu backgrounds, the Inspector’s jobs section lists and cancels',
|
|
849
987
|
tier: 'hidden',
|
|
850
988
|
help: {
|
|
851
989
|
name: 'node bash',
|
|
852
990
|
summary: 'control file-backed bash jobs owned by a node',
|
|
853
991
|
},
|
|
854
|
-
children: [nodeBashBackground],
|
|
992
|
+
children: [nodeBashBackground, nodeBashList, nodeBashKill],
|
|
855
993
|
});
|
|
856
994
|
export const surfaceNodeIdLeaf = defineLeaf({
|
|
857
995
|
name: 'id',
|
|
@@ -8,17 +8,18 @@ import { runCoreView } from '../core/tui/host.js';
|
|
|
8
8
|
export const surfaceInspectLeaf = defineLeaf({
|
|
9
9
|
name: 'inspect',
|
|
10
10
|
description: 'host the native Inspector for one node',
|
|
11
|
-
whenToUse: 'you want a live native screen for a node’s overview, raw record,
|
|
11
|
+
whenToUse: 'you want a live native screen for a node’s overview, raw record, owner-scoped crons, and live background bash jobs. Use node inspect show for the underlying one-shot data read instead.',
|
|
12
12
|
help: {
|
|
13
13
|
name: 'surface inspect',
|
|
14
14
|
summary: 'host the native Inspector in this pane (the attach viewer opens it as a tmux popup)',
|
|
15
|
+
// sections: overview · schedule (armed crons) · jobs (live background bash) · raw
|
|
15
16
|
params: [
|
|
16
17
|
{ kind: 'positional', name: 'node', required: true, constraint: 'Node id or resolvable reference to inspect.' },
|
|
17
|
-
{ kind: 'flag', name: 'section', type: 'enum', choices: ['overview', 'schedule', 'raw'], required: false, default: 'overview', constraint: 'Section focused when the Inspector opens.' },
|
|
18
|
+
{ kind: 'flag', name: 'section', type: 'enum', choices: ['overview', 'schedule', 'jobs', 'raw'], required: false, default: 'overview', constraint: 'Section focused when the Inspector opens.' },
|
|
18
19
|
],
|
|
19
20
|
output: [],
|
|
20
21
|
outputKind: 'object',
|
|
21
|
-
effects: ['Reads node and
|
|
22
|
+
effects: ['Reads node, cron, and background bash job data through crtr subprocesses and crtrd. Takes over the hosting pane (or popup) until it quits.'],
|
|
22
23
|
},
|
|
23
24
|
run: async (input) => {
|
|
24
25
|
const node = input['node'];
|
|
@@ -75,7 +75,7 @@ async function waitFor(predicate, timeoutMs = 3000, stepMs = 10) {
|
|
|
75
75
|
await wait(stepMs);
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
|
-
test('coalesce inlines short
|
|
78
|
+
test('coalesce inlines short report bodies with a trailing artifact pointer and only hints for remaining canonical refs', () => {
|
|
79
79
|
freshNode('child');
|
|
80
80
|
const child = createNode({
|
|
81
81
|
node_id: 'child',
|
|
@@ -93,8 +93,10 @@ test('coalesce inlines short reports with their absolute path and only hints for
|
|
|
93
93
|
const digest = coalesce([
|
|
94
94
|
finalizeInboxEntry({ from: 'child', tier: 'normal', kind: 'update', label: 'short report', ref: 'child:reports/short-update.md' }),
|
|
95
95
|
]);
|
|
96
|
-
|
|
97
|
-
assert.
|
|
96
|
+
// The label is NOT reprinted above the body it duplicates; the path trails it.
|
|
97
|
+
assert.doesNotMatch(digest, /\[update\] short report/);
|
|
98
|
+
assert.match(digest, /\[update\]\n {4}short report body\n/);
|
|
99
|
+
assert.match(digest, new RegExp(`\\(report: ${reportPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`));
|
|
98
100
|
assert.doesNotMatch(digest, /Dereference a ref with/);
|
|
99
101
|
const guidance = 'Dereference a ref with `crtr canvas history read <ref>`.';
|
|
100
102
|
const canonical = coalesce([
|
package/dist/core/bash-jobs.d.ts
CHANGED
|
@@ -5,9 +5,20 @@ export interface BashJobPaths {
|
|
|
5
5
|
jobLog: string;
|
|
6
6
|
jobExit: string;
|
|
7
7
|
jobBg: string;
|
|
8
|
+
/** Process-group id of the job's detached supervisor, written at spawn. It is
|
|
9
|
+
* what makes a job stoppable from outside the agent that started it (the
|
|
10
|
+
* Inspector's cancel) — without it on disk, only the agent's own handoff
|
|
11
|
+
* notice knows the group to signal. */
|
|
12
|
+
jobPgid: string;
|
|
8
13
|
}
|
|
9
14
|
export declare function bashJobsDir(contextDir: string): string;
|
|
10
15
|
export declare function bashJobPaths(contextDir: string, jobId: string): BashJobPaths;
|
|
16
|
+
/** The job's supervisor process group, or undefined for a job started before
|
|
17
|
+
* pgid was persisted (or a partially-written dir). */
|
|
18
|
+
export declare function readJobPgid(contextDir: string, jobId: string): number | undefined;
|
|
19
|
+
/** Last `count` lines of a job log, cheaply: read at most the trailing 64 KiB
|
|
20
|
+
* rather than the whole file, which for a long-running job can be enormous. */
|
|
21
|
+
export declare function tailJobLog(logPath: string, count: number): string[];
|
|
11
22
|
/** Mint a fresh job id — `${Date.now()}-${hex}`. The sole minting site; the
|
|
12
23
|
* valve calls this instead of building the literal inline so the epoch-ms
|
|
13
24
|
* prefix is a real contract shared with its parser (jobStartedAtMs), not an
|
package/dist/core/bash-jobs.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// the prefix menu requests backgrounding by creating job.bg. Keeping that signal
|
|
4
4
|
// on disk means the command can outlive its pi process and still report its exit.
|
|
5
5
|
import { randomBytes } from 'node:crypto';
|
|
6
|
-
import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { closeSync, existsSync, openSync, readdirSync, readFileSync, readSync, statSync, writeFileSync } from 'node:fs';
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
export function bashJobsDir(contextDir) {
|
|
9
9
|
return join(contextDir, 'jobs');
|
|
@@ -17,8 +17,48 @@ export function bashJobPaths(contextDir, jobId) {
|
|
|
17
17
|
jobLog: join(dir, 'job.log'),
|
|
18
18
|
jobExit: join(dir, 'job.exit'),
|
|
19
19
|
jobBg: join(dir, 'job.bg'),
|
|
20
|
+
jobPgid: join(dir, 'job.pgid'),
|
|
20
21
|
};
|
|
21
22
|
}
|
|
23
|
+
/** The job's supervisor process group, or undefined for a job started before
|
|
24
|
+
* pgid was persisted (or a partially-written dir). */
|
|
25
|
+
export function readJobPgid(contextDir, jobId) {
|
|
26
|
+
try {
|
|
27
|
+
const n = Number(readFileSync(bashJobPaths(contextDir, jobId).jobPgid, 'utf8').trim());
|
|
28
|
+
return Number.isInteger(n) && n > 1 ? n : undefined;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Last `count` lines of a job log, cheaply: read at most the trailing 64 KiB
|
|
35
|
+
* rather than the whole file, which for a long-running job can be enormous. */
|
|
36
|
+
export function tailJobLog(logPath, count) {
|
|
37
|
+
if (count <= 0)
|
|
38
|
+
return [];
|
|
39
|
+
const WINDOW = 64 * 1024;
|
|
40
|
+
let fd;
|
|
41
|
+
try {
|
|
42
|
+
const size = statSync(logPath).size;
|
|
43
|
+
const start = Math.max(0, size - WINDOW);
|
|
44
|
+
const buf = Buffer.alloc(Math.min(size, WINDOW));
|
|
45
|
+
fd = openSync(logPath, 'r');
|
|
46
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
47
|
+
const text = buf.toString('utf8');
|
|
48
|
+
// A partial first line (we cut mid-line) is dropped, not shown truncated.
|
|
49
|
+
const lines = (start > 0 ? text.slice(text.indexOf('\n') + 1) : text).split('\n');
|
|
50
|
+
if (lines[lines.length - 1] === '')
|
|
51
|
+
lines.pop();
|
|
52
|
+
return lines.slice(-count);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
finally {
|
|
58
|
+
if (fd !== undefined)
|
|
59
|
+
closeSync(fd);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
22
62
|
/** Mint a fresh job id — `${Date.now()}-${hex}`. The sole minting site; the
|
|
23
63
|
* valve calls this instead of building the literal inline so the epoch-ms
|
|
24
64
|
* prefix is a real contract shared with its parser (jobStartedAtMs), not an
|
|
@@ -75,9 +75,9 @@ export declare function clipBody(body: string): {
|
|
|
75
75
|
*
|
|
76
76
|
* Format (per sender group):
|
|
77
77
|
* From <sender> — <N> update(s):
|
|
78
|
-
* [<kind>]
|
|
79
|
-
*
|
|
80
|
-
*
|
|
78
|
+
* [<kind>]
|
|
79
|
+
* <short report body>
|
|
80
|
+
* (report: <absolute report path>)
|
|
81
81
|
*
|
|
82
82
|
* Large reports remain canonical pointers and add one shared dereference hint.
|
|
83
83
|
*/
|
package/dist/core/feed/inbox.js
CHANGED
|
@@ -330,15 +330,19 @@ function inlineReport(e) {
|
|
|
330
330
|
/**
|
|
331
331
|
* Render one entry's digest line(s).
|
|
332
332
|
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
333
|
+
* A short pushed report is inlined as its own body, followed by a pointer to
|
|
334
|
+
* the durable artifact. The label is NOT repeated above it: a report's label is
|
|
335
|
+
* just its first line (feed.ts `firstLine`), so printing both reads as a
|
|
336
|
+
* truncated preview stacked on the full text it previews. Larger reports stay
|
|
337
|
+
* canonical history pointers. Direct messages retain their bounded
|
|
335
338
|
* inline-body/spill behavior.
|
|
336
339
|
*/
|
|
337
340
|
function renderEntry(e) {
|
|
338
341
|
const report = inlineReport(e);
|
|
339
342
|
if (report !== null) {
|
|
343
|
+
const indented = report.body.trimEnd().split('\n').map((l) => ` ${l}`).join('\n');
|
|
340
344
|
return {
|
|
341
|
-
text: ` [${e.kind}]
|
|
345
|
+
text: ` [${e.kind}]\n${indented}\n (report: ${report.path})`,
|
|
342
346
|
hasCanonicalRef: false,
|
|
343
347
|
};
|
|
344
348
|
}
|
|
@@ -364,9 +368,9 @@ function renderEntry(e) {
|
|
|
364
368
|
*
|
|
365
369
|
* Format (per sender group):
|
|
366
370
|
* From <sender> — <N> update(s):
|
|
367
|
-
* [<kind>]
|
|
368
|
-
*
|
|
369
|
-
*
|
|
371
|
+
* [<kind>]
|
|
372
|
+
* <short report body>
|
|
373
|
+
* (report: <absolute report path>)
|
|
370
374
|
*
|
|
371
375
|
* Large reports remain canonical pointers and add one shared dereference hint.
|
|
372
376
|
*/
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { CronDTO } from '../../api/dto/crons.js';
|
|
2
2
|
import type { SourceError, ViewCore } from '../view/contract.js';
|
|
3
|
-
import { scheduledItems, type InspectorSection } from './model.js';
|
|
3
|
+
import { scheduledItems, type BashJobRow, type InspectorSection } from './model.js';
|
|
4
|
+
/** What a `y` confirmation will act on. Both sections arm cancellation the same
|
|
5
|
+
* way, so the pending action carries its own kind rather than the view guessing
|
|
6
|
+
* from whichever section happens to be open when the key lands. */
|
|
7
|
+
export type PendingAction = {
|
|
8
|
+
kind: 'cron' | 'job';
|
|
9
|
+
id: string;
|
|
10
|
+
label: string;
|
|
11
|
+
};
|
|
4
12
|
export interface InspectorState {
|
|
5
13
|
target: string;
|
|
6
14
|
section: InspectorSection;
|
|
@@ -14,10 +22,17 @@ export interface InspectorState {
|
|
|
14
22
|
error: SourceError | null;
|
|
15
23
|
fetchedAt: number;
|
|
16
24
|
};
|
|
25
|
+
jobs: {
|
|
26
|
+
data: BashJobRow[] | null;
|
|
27
|
+
error: SourceError | null;
|
|
28
|
+
fetchedAt: number;
|
|
29
|
+
};
|
|
17
30
|
selectedCronId: string | null;
|
|
31
|
+
selectedJobId: string | null;
|
|
18
32
|
expanded: boolean;
|
|
19
|
-
pendingCancel:
|
|
33
|
+
pendingCancel: PendingAction | null;
|
|
20
34
|
scroll: Record<InspectorSection, number>;
|
|
21
35
|
}
|
|
22
36
|
export declare const inspectorCore: ViewCore<InspectorState>;
|
|
37
|
+
export declare function selectedJob(state: InspectorState): BashJobRow | undefined;
|
|
23
38
|
export { scheduledItems };
|