@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
|
@@ -35,6 +35,25 @@ const scheduleSource = {
|
|
|
35
35
|
return { ok: true, data: crons };
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
|
+
/** Trailing log lines carried with every job row: enough to fill the expanded
|
|
39
|
+
* card, so watching a job needs no second surface. */
|
|
40
|
+
const LOG_TAIL_LINES = 14;
|
|
41
|
+
/** Live background bash jobs for the inspected node. Non-blocking, exactly like
|
|
42
|
+
* the schedule: a node with no jobs dir (or a remote target) degrades this one
|
|
43
|
+
* section, never the whole view. */
|
|
44
|
+
const jobsSource = {
|
|
45
|
+
id: 'jobs',
|
|
46
|
+
request: (target) => ({ kind: 'exec', bin: 'crtr', args: ['--json', 'node', 'bash', 'list', '--node', target, '--tail', String(LOG_TAIL_LINES)] }),
|
|
47
|
+
parse: (raw) => {
|
|
48
|
+
const result = parseJson(raw, 'node bash list', false);
|
|
49
|
+
if (!result.ok)
|
|
50
|
+
return result;
|
|
51
|
+
const jobs = result.data['jobs'];
|
|
52
|
+
if (!Array.isArray(jobs))
|
|
53
|
+
return { ok: false, error: error('parse', 'Jobs unavailable', 'The job list did not contain job rows.', false) };
|
|
54
|
+
return { ok: true, data: jobs };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
38
57
|
const cancelCommand = {
|
|
39
58
|
id: 'cancel',
|
|
40
59
|
request: (id) => ({ kind: 'exec', bin: 'crtr', args: ['--json', 'cron', 'cancel', id] }),
|
|
@@ -45,8 +64,50 @@ const cancelCommand = {
|
|
|
45
64
|
return { ok: true, data: { canceled: String(result.data['canceled'] ?? '') } };
|
|
46
65
|
},
|
|
47
66
|
};
|
|
67
|
+
const killJobCommand = {
|
|
68
|
+
id: 'kill-job',
|
|
69
|
+
request: ({ target, job }) => ({ kind: 'exec', bin: 'crtr', args: ['--json', 'node', 'bash', 'kill', job, '--node', target] }),
|
|
70
|
+
parse: (raw) => {
|
|
71
|
+
const result = parseJson(raw, 'node bash kill', false);
|
|
72
|
+
if (!result.ok)
|
|
73
|
+
return result;
|
|
74
|
+
return { ok: true, data: { job: String(result.data['job_id'] ?? '') } };
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
/** Open the selected job's log in its own tmux window, following like `tail -f`.
|
|
78
|
+
* A new window rather than a nested popup: tmux allows one popup per client and
|
|
79
|
+
* the Inspector is usually already in one — and a window is where you actually
|
|
80
|
+
* want a log you intend to keep watching after the Inspector closes. */
|
|
81
|
+
const openLogCommand = {
|
|
82
|
+
id: 'open-log',
|
|
83
|
+
request: ({ path, title }) => ({
|
|
84
|
+
kind: 'exec',
|
|
85
|
+
bin: 'tmux',
|
|
86
|
+
args: ['new-window', '-n', title, `less +F -- ${quote(path)} 2>/dev/null || tail -f -- ${quote(path)}`],
|
|
87
|
+
}),
|
|
88
|
+
parse: (raw) => {
|
|
89
|
+
if (!raw.ok || (raw.exitCode ?? 0) !== 0) {
|
|
90
|
+
return { ok: false, error: error('no_tmux', 'Could not open the log', raw.stderr || 'tmux new-window failed — the Inspector must be running inside tmux.', false) };
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, data: {} };
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
function quote(text) {
|
|
96
|
+
return `'${text.split("'").join(`'\\''`)}'`;
|
|
97
|
+
}
|
|
98
|
+
const SECTION_ORDER = ['overview', 'schedule', 'jobs', 'raw'];
|
|
48
99
|
function section(value) {
|
|
49
|
-
return value === 'schedule' || value === 'raw' ? value : 'overview';
|
|
100
|
+
return value === 'schedule' || value === 'jobs' || value === 'raw' ? value : 'overview';
|
|
101
|
+
}
|
|
102
|
+
/** The section-aware status line: what is pending, counted for what you see. */
|
|
103
|
+
function countsFor(state) {
|
|
104
|
+
const crons = `${scheduledItems(state.schedule.data ?? []).length} cron(s) armed`;
|
|
105
|
+
const jobs = `${(state.jobs.data ?? []).length} background job(s) running`;
|
|
106
|
+
if (state.section === 'schedule')
|
|
107
|
+
return crons;
|
|
108
|
+
if (state.section === 'jobs')
|
|
109
|
+
return jobs;
|
|
110
|
+
return `${crons} · ${jobs}`;
|
|
50
111
|
}
|
|
51
112
|
function reconcile(state, next) {
|
|
52
113
|
const items = scheduledItems(next);
|
|
@@ -58,21 +119,30 @@ function reconcile(state, next) {
|
|
|
58
119
|
return { ...state, schedule: { data: next, error: null, fetchedAt: Date.now() }, selectedCronId: replacement?.cron.cron_id ?? null, expanded: false, pendingCancel: null };
|
|
59
120
|
}
|
|
60
121
|
export const inspectorCore = {
|
|
61
|
-
manifest: { id: 'inspector', title: 'Inspector', description: 'Node overview and
|
|
122
|
+
manifest: { id: 'inspector', title: 'Inspector', description: 'Node overview, scheduled crons, and live background bash jobs', refreshMs: 5000 },
|
|
62
123
|
init(options) {
|
|
63
124
|
return {
|
|
64
125
|
target: options['target'] ?? '', section: section(options['section']),
|
|
65
126
|
dossier: { data: null, error: null, fetchedAt: 0 }, schedule: { data: null, error: null, fetchedAt: 0 },
|
|
66
|
-
|
|
67
|
-
|
|
127
|
+
jobs: { data: null, error: null, fetchedAt: 0 },
|
|
128
|
+
selectedCronId: null, selectedJobId: null, expanded: false, pendingCancel: null,
|
|
129
|
+
scroll: { overview: 0, schedule: 0, jobs: 0, raw: 0 },
|
|
68
130
|
};
|
|
69
131
|
},
|
|
70
|
-
sources: { dossier: dossierSource, schedule: scheduleSource },
|
|
71
|
-
commands: { cancel: cancelCommand },
|
|
132
|
+
sources: { dossier: dossierSource, schedule: scheduleSource, jobs: jobsSource },
|
|
133
|
+
commands: { cancel: cancelCommand, killJob: killJobCommand, openLog: openLogCommand },
|
|
72
134
|
intents: {
|
|
73
135
|
async refresh(ctx) {
|
|
74
136
|
ctx.signal.setStatus('Refreshing…');
|
|
75
|
-
|
|
137
|
+
let note;
|
|
138
|
+
// All three reads are independent subprocesses, so they run TOGETHER: the
|
|
139
|
+
// host drops keystrokes while an intent is in flight, and the auto-refresh
|
|
140
|
+
// fires every 5s — resolving them in series would make the view feel deaf.
|
|
141
|
+
const [dossier, schedule, jobs] = await Promise.all([
|
|
142
|
+
ctx.resolve(dossierSource, ctx.state.target),
|
|
143
|
+
ctx.resolve(scheduleSource, ctx.state.target),
|
|
144
|
+
ctx.resolve(jobsSource, ctx.state.target),
|
|
145
|
+
]);
|
|
76
146
|
if (!dossier.ok) {
|
|
77
147
|
ctx.set((state) => ({ ...state, dossier: { ...state.dossier, error: dossier.error } }));
|
|
78
148
|
ctx.signal.setBanner(`${dossier.error.display.headline}: ${dossier.error.display.explanation}`, 'error');
|
|
@@ -80,7 +150,6 @@ export const inspectorCore = {
|
|
|
80
150
|
return;
|
|
81
151
|
}
|
|
82
152
|
ctx.set((state) => ({ ...state, dossier: { data: dossier.data, error: null, fetchedAt: Date.now() } }));
|
|
83
|
-
const schedule = await ctx.resolve(scheduleSource, ctx.state.target);
|
|
84
153
|
if (!schedule.ok) {
|
|
85
154
|
ctx.set((state) => ({ ...state, schedule: { ...state.schedule, error: schedule.error } }));
|
|
86
155
|
ctx.signal.setBanner(schedule.error.display.explanation, 'error');
|
|
@@ -89,37 +158,90 @@ export const inspectorCore = {
|
|
|
89
158
|
const before = ctx.state.selectedCronId;
|
|
90
159
|
ctx.set((state) => reconcile(state, schedule.data));
|
|
91
160
|
if (before !== null && !schedule.data.some((cron) => cron.cron_id === before))
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
ctx.
|
|
161
|
+
note = 'Selected cron disappeared; moved to the next item.';
|
|
162
|
+
// Clear ONLY a banner this source raised. A background refresh must not
|
|
163
|
+
// wipe the user's own "press y to confirm" prompt or an action result.
|
|
164
|
+
if (ctx.state.schedule.error !== null)
|
|
165
|
+
ctx.signal.clearBanner();
|
|
166
|
+
}
|
|
167
|
+
if (!jobs.ok)
|
|
168
|
+
ctx.set((state) => ({ ...state, jobs: { ...state.jobs, error: jobs.error } }));
|
|
169
|
+
else {
|
|
170
|
+
const gone = ctx.state.selectedJobId !== null && !jobs.data.some((job) => job.job_id === ctx.state.selectedJobId);
|
|
171
|
+
ctx.set((state) => reconcileJobs(state, jobs.data));
|
|
172
|
+
if (gone)
|
|
173
|
+
note = 'The selected job finished; moved to the next one.';
|
|
96
174
|
}
|
|
175
|
+
// One status line, always about the section you are looking at: a count of
|
|
176
|
+
// crons in schedule, of jobs in jobs, of both in the whole-node sections.
|
|
177
|
+
ctx.signal.setStatus(note ?? countsFor(ctx.state));
|
|
97
178
|
const node = record(ctx.state.dossier.data?.['node']);
|
|
98
179
|
ctx.signal.setSubtitle(`${node['name'] ?? ctx.state.target} · ${ctx.state.section}`);
|
|
99
180
|
},
|
|
100
|
-
sectionNext(ctx) { const order =
|
|
101
|
-
sectionPrev(ctx) { const order =
|
|
181
|
+
sectionNext(ctx) { const order = SECTION_ORDER; const next = order[(order.indexOf(ctx.state.section) + 1) % order.length]; ctx.set((state) => ({ ...state, section: next, pendingCancel: null })); ctx.signal.clearBanner(); ctx.signal.setStatus(countsFor(ctx.state)); ctx.signal.setSubtitle(`${ctx.state.target} · ${next}`); },
|
|
182
|
+
sectionPrev(ctx) { const order = SECTION_ORDER; const next = order[(order.indexOf(ctx.state.section) + order.length - 1) % order.length]; ctx.set((state) => ({ ...state, section: next, pendingCancel: null })); ctx.signal.clearBanner(); ctx.signal.setStatus(countsFor(ctx.state)); ctx.signal.setSubtitle(`${ctx.state.target} · ${next}`); },
|
|
102
183
|
down(ctx) { ctx.set((state) => move(state, 1)); },
|
|
103
184
|
up(ctx) { ctx.set((state) => move(state, -1)); },
|
|
104
185
|
pageDown(ctx) { ctx.set((state) => ({ ...state, scroll: { ...state.scroll, [state.section]: state.scroll[state.section] + 8 } })); },
|
|
105
186
|
pageUp(ctx) { ctx.set((state) => ({ ...state, scroll: { ...state.scroll, [state.section]: Math.max(0, state.scroll[state.section] - 8) } })); },
|
|
106
|
-
toggleDetail(ctx) {
|
|
107
|
-
ctx.
|
|
108
|
-
|
|
109
|
-
|
|
187
|
+
toggleDetail(ctx) {
|
|
188
|
+
if (ctx.state.section === 'jobs' ? ctx.state.selectedJobId !== null : ctx.state.selectedCronId !== null) {
|
|
189
|
+
ctx.set((state) => ({ ...state, expanded: !state.expanded }));
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
cancelItem(ctx) {
|
|
193
|
+
const pending = ctx.state.section === 'jobs'
|
|
194
|
+
? selectedJob(ctx.state) && { kind: 'job', id: selectedJob(ctx.state).job_id, label: commandHead(selectedJob(ctx.state).command) }
|
|
195
|
+
: ctx.state.selectedCronId !== null && { kind: 'cron', id: ctx.state.selectedCronId, label: ctx.state.selectedCronId };
|
|
196
|
+
if (!pending)
|
|
197
|
+
return;
|
|
198
|
+
if (pending.kind === 'job' && selectedJob(ctx.state)?.pgid === null) {
|
|
199
|
+
ctx.signal.setBanner('This job predates recorded process groups — crtr cannot stop it. Open its log and stop it by hand.', 'error');
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
ctx.set((state) => ({ ...state, pendingCancel: pending }));
|
|
203
|
+
ctx.signal.setBanner(pending.kind === 'job'
|
|
204
|
+
? `Stop job “${pending.label}”? It dies now and the node is told. Press y to confirm.`
|
|
205
|
+
: `Cancel cron ${pending.label}? Press y to confirm.`, 'action');
|
|
206
|
+
},
|
|
207
|
+
async openLog(ctx) {
|
|
208
|
+
const job = selectedJob(ctx.state);
|
|
209
|
+
if (job === undefined)
|
|
210
|
+
return;
|
|
211
|
+
ctx.signal.setStatus('Opening the job log…');
|
|
212
|
+
const result = await ctx.execute(openLogCommand, { path: job.log_path, title: `job · ${commandHead(job.command, 18)}` });
|
|
213
|
+
if (!result.ok) {
|
|
214
|
+
ctx.signal.setBanner(result.error.display.explanation, 'error');
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
ctx.signal.setBanner('Log opened in a new tmux window, following live. Quit the Inspector to read it.', 'info');
|
|
218
|
+
},
|
|
110
219
|
async confirm(ctx) {
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
220
|
+
const pending = ctx.state.pendingCancel;
|
|
221
|
+
if (pending === null)
|
|
113
222
|
return;
|
|
223
|
+
if (pending.kind === 'job') {
|
|
224
|
+
ctx.signal.setStatus('Stopping the job…');
|
|
225
|
+
const killed = await ctx.execute(killJobCommand, { target: ctx.state.target, job: pending.id });
|
|
226
|
+
ctx.set((state) => ({ ...state, pendingCancel: null }));
|
|
227
|
+
if (!killed.ok) {
|
|
228
|
+
ctx.signal.setBanner(killed.error.display.explanation, 'error');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
await ctx.dispatch('refresh');
|
|
232
|
+
const stillRunning = (ctx.state.jobs.data ?? []).some((job) => job.job_id === pending.id);
|
|
233
|
+
ctx.signal.setBanner(stillRunning ? 'The job is still running; refresh and retry.' : 'Job stopped — the node was told it was canceled.', stillRunning ? 'error' : 'info');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
114
236
|
ctx.signal.setStatus('Canceling cron…');
|
|
115
|
-
const result = await ctx.execute(cancelCommand, id);
|
|
237
|
+
const result = await ctx.execute(cancelCommand, pending.id);
|
|
116
238
|
ctx.set((state) => ({ ...state, pendingCancel: null }));
|
|
117
239
|
if (!result.ok) {
|
|
118
240
|
ctx.signal.setBanner(result.error.display.explanation, 'error');
|
|
119
241
|
return;
|
|
120
242
|
}
|
|
121
243
|
await ctx.dispatch('refresh');
|
|
122
|
-
const stillArmed = (ctx.state.schedule.data ?? []).some((cron) => cron.cron_id === id);
|
|
244
|
+
const stillArmed = (ctx.state.schedule.data ?? []).some((cron) => cron.cron_id === pending.id);
|
|
123
245
|
ctx.signal.setBanner(stillArmed ? 'Cron remains armed; refresh and retry.' : 'Cron canceled.', stillArmed ? 'error' : 'info');
|
|
124
246
|
},
|
|
125
247
|
dismiss(ctx) { ctx.set((state) => ({ ...state, pendingCancel: null })); ctx.signal.clearBanner(); },
|
|
@@ -128,14 +250,41 @@ export const inspectorCore = {
|
|
|
128
250
|
const next = cron?.anchor_node ?? cron?.created_by;
|
|
129
251
|
if (!next)
|
|
130
252
|
return;
|
|
131
|
-
ctx.set((state) => ({ ...state, target: next, selectedCronId: null, expanded: false, pendingCancel: null, dossier: { data: null, error: null, fetchedAt: 0 }, schedule: { data: null, error: null, fetchedAt: 0 } }));
|
|
253
|
+
ctx.set((state) => ({ ...state, target: next, selectedCronId: null, expanded: false, pendingCancel: null, dossier: { data: null, error: null, fetchedAt: 0 }, schedule: { data: null, error: null, fetchedAt: 0 }, jobs: { data: null, error: null, fetchedAt: 0 } }));
|
|
132
254
|
void ctx.dispatch('refresh');
|
|
133
255
|
},
|
|
134
256
|
quit(ctx) { ctx.signal.quit(); },
|
|
135
257
|
},
|
|
136
258
|
};
|
|
137
259
|
function selectedItem(state) { return scheduledItems(state.schedule.data ?? []).find((item) => item.cron.cron_id === state.selectedCronId); }
|
|
260
|
+
export function selectedJob(state) {
|
|
261
|
+
return (state.jobs.data ?? []).find((job) => job.job_id === state.selectedJobId);
|
|
262
|
+
}
|
|
263
|
+
/** First line of a command, for banners and window titles. */
|
|
264
|
+
function commandHead(command, cap = 40) {
|
|
265
|
+
const line = (command.split('\n')[0] ?? '').replace(/\s+/g, ' ').trim();
|
|
266
|
+
return line.length > cap ? `${line.slice(0, cap - 1)}…` : line;
|
|
267
|
+
}
|
|
268
|
+
/** Keep the job cursor on the same job across refreshes; when it finished, land
|
|
269
|
+
* on whatever took its place rather than losing the cursor entirely. */
|
|
270
|
+
function reconcileJobs(state, next) {
|
|
271
|
+
const fetched = { data: next, error: null, fetchedAt: Date.now() };
|
|
272
|
+
if (state.selectedJobId !== null && next.some((job) => job.job_id === state.selectedJobId))
|
|
273
|
+
return { ...state, jobs: fetched };
|
|
274
|
+
const previousIndex = (state.jobs.data ?? []).findIndex((job) => job.job_id === state.selectedJobId);
|
|
275
|
+
const replacement = next[Math.min(Math.max(0, previousIndex), Math.max(0, next.length - 1))];
|
|
276
|
+
const pendingCancel = state.pendingCancel?.kind === 'job' ? null : state.pendingCancel;
|
|
277
|
+
return { ...state, jobs: fetched, selectedJobId: replacement?.job_id ?? null, expanded: false, pendingCancel };
|
|
278
|
+
}
|
|
138
279
|
function move(state, delta) {
|
|
280
|
+
if (state.section === 'jobs') {
|
|
281
|
+
const jobs = state.jobs.data ?? [];
|
|
282
|
+
if (jobs.length === 0)
|
|
283
|
+
return state;
|
|
284
|
+
const current = Math.max(0, jobs.findIndex((job) => job.job_id === state.selectedJobId));
|
|
285
|
+
const next = Math.max(0, Math.min(jobs.length - 1, current + delta));
|
|
286
|
+
return { ...state, selectedJobId: jobs[next].job_id, expanded: false, pendingCancel: null };
|
|
287
|
+
}
|
|
139
288
|
if (state.section !== 'schedule')
|
|
140
289
|
return { ...state, scroll: { ...state.scroll, [state.section]: Math.max(0, state.scroll[state.section] + delta) } };
|
|
141
290
|
const items = scheduledItems(state.schedule.data ?? []);
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { CronDTO } from '../../api/dto/crons.js';
|
|
2
|
-
export type InspectorSection = 'overview' | 'schedule' | 'raw';
|
|
2
|
+
export type InspectorSection = 'overview' | 'schedule' | 'jobs' | 'raw';
|
|
3
|
+
/** One live background bash job, as `crtr --json node bash list` reports it. */
|
|
4
|
+
export interface BashJobRow {
|
|
5
|
+
job_id: string;
|
|
6
|
+
command: string;
|
|
7
|
+
started_at: string;
|
|
8
|
+
elapsed_ms: number;
|
|
9
|
+
log_path: string;
|
|
10
|
+
/** Supervisor process group — null for a job started before crtr persisted
|
|
11
|
+
* one, which is exactly the set of jobs the Inspector cannot stop. */
|
|
12
|
+
pgid: number | null;
|
|
13
|
+
log_tail: string[];
|
|
14
|
+
}
|
|
3
15
|
export interface ScheduledItem {
|
|
4
16
|
cron: CronDTO;
|
|
5
17
|
nextAt: string;
|
|
@@ -14,6 +26,8 @@ export declare const ICON: {
|
|
|
14
26
|
readonly artifacts: string;
|
|
15
27
|
readonly paths: string;
|
|
16
28
|
readonly cron: string;
|
|
29
|
+
readonly job: string;
|
|
30
|
+
readonly log: string;
|
|
17
31
|
readonly section: string;
|
|
18
32
|
readonly raw: string;
|
|
19
33
|
readonly empty: string;
|
|
@@ -38,6 +52,21 @@ export declare function statusVisual(status: string): {
|
|
|
38
52
|
tone: Tone;
|
|
39
53
|
};
|
|
40
54
|
export declare function scheduledItems(crons: readonly CronDTO[]): ScheduledItem[];
|
|
55
|
+
/** Elapsed duration in the shape every bash-handoff surface uses: `12s`,
|
|
56
|
+
* `3m 40s`, `1h 04m`. Duplicated from core/bash-jobs.ts on purpose — a view
|
|
57
|
+
* module stays a pure read and never pulls the node:fs job scanner in. */
|
|
58
|
+
export declare function elapsed(ms: number): string;
|
|
59
|
+
/** How a running job's age reads at a glance: an ordered ramp, not decoration.
|
|
60
|
+
* Fresh work is green, a job past 15m is amber, past an hour is red — the same
|
|
61
|
+
* good/warn/bad progression the node status glyphs use. */
|
|
62
|
+
export declare function jobTone(elapsedMs: number): Tone;
|
|
63
|
+
/** Milliseconds a job has been running, live from `started_at`. */
|
|
64
|
+
export declare function jobElapsedMs(job: BashJobRow, now?: number): number;
|
|
65
|
+
/** Live elapsed time for a job row — recomputed from `started_at` so the number
|
|
66
|
+
* keeps ticking between the view's 5s refreshes; `elapsed_ms` is the fallback. */
|
|
67
|
+
export declare function jobElapsed(job: BashJobRow, now?: number): string;
|
|
68
|
+
/** First line of a command, whitespace-collapsed and capped for a one-row cell. */
|
|
69
|
+
export declare function commandCell(command: string, cap?: number): string;
|
|
41
70
|
export declare function relativeTime(iso: string | null, now?: number): string;
|
|
42
71
|
/** How long ago an ISO timestamp was, e.g. `3d ago`. `—` when absent. */
|
|
43
72
|
export declare function ago(iso: unknown, now?: number): string;
|
|
@@ -13,6 +13,8 @@ export const ICON = {
|
|
|
13
13
|
artifacts: cp(0xf1da), // history/archive — what it left behind
|
|
14
14
|
paths: cp(0xf0f6), // file-text — on-disk locations
|
|
15
15
|
cron: cp(0xf017), // clock — one armed cron
|
|
16
|
+
job: cp(0xf013), // cog — one live background bash job
|
|
17
|
+
log: cp(0xf120), // terminal — the job's log stream
|
|
16
18
|
section: cp(0xf192), // dot-circle — the active tab marker
|
|
17
19
|
raw: cp(0xf121), // code — the raw record
|
|
18
20
|
empty: cp(0xf1b3), // cubes — nothing armed
|
|
@@ -42,6 +44,43 @@ export function scheduledItems(crons) {
|
|
|
42
44
|
return a.cron.cron_id.localeCompare(b.cron.cron_id);
|
|
43
45
|
});
|
|
44
46
|
}
|
|
47
|
+
/** Elapsed duration in the shape every bash-handoff surface uses: `12s`,
|
|
48
|
+
* `3m 40s`, `1h 04m`. Duplicated from core/bash-jobs.ts on purpose — a view
|
|
49
|
+
* module stays a pure read and never pulls the node:fs job scanner in. */
|
|
50
|
+
export function elapsed(ms) {
|
|
51
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
52
|
+
if (totalSeconds < 60)
|
|
53
|
+
return `${totalSeconds}s`;
|
|
54
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
55
|
+
if (totalMinutes < 60)
|
|
56
|
+
return `${totalMinutes}m ${String(totalSeconds % 60).padStart(2, '0')}s`;
|
|
57
|
+
return `${Math.floor(totalMinutes / 60)}h ${String(totalMinutes % 60).padStart(2, '0')}m`;
|
|
58
|
+
}
|
|
59
|
+
/** How a running job's age reads at a glance: an ordered ramp, not decoration.
|
|
60
|
+
* Fresh work is green, a job past 15m is amber, past an hour is red — the same
|
|
61
|
+
* good/warn/bad progression the node status glyphs use. */
|
|
62
|
+
export function jobTone(elapsedMs) {
|
|
63
|
+
if (elapsedMs >= 60 * 60_000)
|
|
64
|
+
return 'bad';
|
|
65
|
+
if (elapsedMs >= 15 * 60_000)
|
|
66
|
+
return 'warn';
|
|
67
|
+
return 'good';
|
|
68
|
+
}
|
|
69
|
+
/** Milliseconds a job has been running, live from `started_at`. */
|
|
70
|
+
export function jobElapsedMs(job, now = Date.now()) {
|
|
71
|
+
const at = Date.parse(job.started_at);
|
|
72
|
+
return Number.isFinite(at) ? now - at : job.elapsed_ms;
|
|
73
|
+
}
|
|
74
|
+
/** Live elapsed time for a job row — recomputed from `started_at` so the number
|
|
75
|
+
* keeps ticking between the view's 5s refreshes; `elapsed_ms` is the fallback. */
|
|
76
|
+
export function jobElapsed(job, now = Date.now()) {
|
|
77
|
+
return elapsed(jobElapsedMs(job, now));
|
|
78
|
+
}
|
|
79
|
+
/** First line of a command, whitespace-collapsed and capped for a one-row cell. */
|
|
80
|
+
export function commandCell(command, cap = 60) {
|
|
81
|
+
const oneLine = (command.split('\n')[0] ?? '').replace(/\s+/g, ' ').trim();
|
|
82
|
+
return oneLine.length > cap ? `${oneLine.slice(0, cap - 1)}…` : oneLine;
|
|
83
|
+
}
|
|
45
84
|
export function relativeTime(iso, now = Date.now()) {
|
|
46
85
|
if (iso === null)
|
|
47
86
|
return '—';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { overviewGroups, relativeTime, scheduledItems, value } from './model.js';
|
|
1
|
+
import { jobElapsed, overviewGroups, relativeTime, scheduledItems, value } from './model.js';
|
|
2
2
|
/** The piped path: same information and grouping as the TUI, minus the glyphs
|
|
3
3
|
* and hue a pipe cannot carry. */
|
|
4
4
|
export const inspectorText = {
|
|
@@ -27,6 +27,19 @@ export const inspectorText = {
|
|
|
27
27
|
const cron = item.cron;
|
|
28
28
|
lines.push('', ` ${cron.name} · ${item.cadence} · ${relativeTime(item.nextAt)}`, ` id ${cron.cron_id}`, ` next ${item.nextAt}`, ` state ${value(cron.state)}`, ` output ${cron.on_output}${cron.sink ? ` → ${cron.sink}` : ''}`, ` expires ${value(cron.expires_at)}`);
|
|
29
29
|
}
|
|
30
|
+
lines.push('', 'BACKGROUND BASH JOBS');
|
|
31
|
+
if (state.jobs.error) {
|
|
32
|
+
lines.push(` ${state.jobs.error.display.explanation}`, ` ${state.jobs.error.display.nextStep}`);
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
const jobs = state.jobs.data ?? [];
|
|
36
|
+
if (jobs.length === 0)
|
|
37
|
+
lines.push(' nothing running');
|
|
38
|
+
for (const job of jobs) {
|
|
39
|
+
lines.push('', ` ${job.command.split('\n')[0]} · ${jobElapsed(job)}`, ` id ${job.job_id}`, ` started ${job.started_at}`, ` log ${job.log_path}`);
|
|
40
|
+
for (const line of job.log_tail.slice(-5))
|
|
41
|
+
lines.push(` | ${line}`);
|
|
42
|
+
}
|
|
30
43
|
return lines.join('\n');
|
|
31
44
|
},
|
|
32
45
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ICON, overviewGroups, relativeTime, scheduledItems, shortPath, value, } from './model.js';
|
|
1
|
+
import { ICON, commandCell, jobElapsed, jobElapsedMs, jobTone, overviewGroups, relativeTime, scheduledItems, shortPath, value, } from './model.js';
|
|
2
2
|
// ── Visual grammar ───────────────────────────────────────────────────────────
|
|
3
3
|
//
|
|
4
4
|
// Three primitives carry the whole screen: a TAB STRIP (where am I / where can
|
|
@@ -21,6 +21,7 @@ const HEAD_STYLE = { fg: '36', bold: true };
|
|
|
21
21
|
const SECTIONS = [
|
|
22
22
|
{ id: 'overview', label: 'overview' },
|
|
23
23
|
{ id: 'schedule', label: 'schedule' },
|
|
24
|
+
{ id: 'jobs', label: 'jobs' },
|
|
24
25
|
{ id: 'raw', label: 'raw' },
|
|
25
26
|
];
|
|
26
27
|
/** `label` padded into the dim label column, then the value. */
|
|
@@ -50,7 +51,9 @@ function tabStrip(state, width) {
|
|
|
50
51
|
const active = section.id === state.section;
|
|
51
52
|
const badge = section.id === 'schedule'
|
|
52
53
|
? ` ${scheduledItems(state.schedule.data ?? []).length}`
|
|
53
|
-
: ''
|
|
54
|
+
: section.id === 'jobs'
|
|
55
|
+
? ` ${(state.jobs.data ?? []).length}`
|
|
56
|
+
: '';
|
|
54
57
|
spans.push({
|
|
55
58
|
text: active ? `${ICON.section} ${section.label}${badge}` : `${section.label}${badge}`,
|
|
56
59
|
style: active ? { fg: '36', bold: true } : { dim: true },
|
|
@@ -147,6 +150,52 @@ function cronItem(item, expanded) {
|
|
|
147
150
|
lines,
|
|
148
151
|
};
|
|
149
152
|
}
|
|
153
|
+
/** One live background job as a list item. Collapsed it shows the single thing
|
|
154
|
+
* a watcher wants — the newest log line — under the command and its running
|
|
155
|
+
* time; expanding adds the job's identity and a live tail of its log, so the
|
|
156
|
+
* Inspector answers "what is it doing right now" without a second surface. */
|
|
157
|
+
function jobItem(job, expanded, width) {
|
|
158
|
+
const tone = TONE[jobTone(jobElapsedMs(job))];
|
|
159
|
+
const last = job.log_tail[job.log_tail.length - 1];
|
|
160
|
+
const lines = [];
|
|
161
|
+
if (!expanded) {
|
|
162
|
+
lines.push([{ text: `${INDENT}${INDENT}${last === undefined ? 'no output yet' : logCell(last, width - 6)}`, style: { dim: true } }]);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
for (const [label, text] of [['job', job.job_id], ['started', job.started_at], ['log', shortPath(job.log_path)], ['watch', `tail -f ${shortPath(job.log_path)}`]]) {
|
|
166
|
+
lines.push([{ text: `${INDENT}${INDENT}${label.padEnd(LABEL_COL)}`, style: LABEL_STYLE }, { text, style: label === 'watch' ? { fg: '36' } : undefined }]);
|
|
167
|
+
}
|
|
168
|
+
lines.push([]);
|
|
169
|
+
lines.push(groupHeader(ICON.log, `last ${job.log_tail.length} lines`, width - 2));
|
|
170
|
+
if (job.log_tail.length === 0)
|
|
171
|
+
lines.push([{ text: `${INDENT}${INDENT}no output yet`, style: { dim: true } }]);
|
|
172
|
+
for (const line of job.log_tail)
|
|
173
|
+
lines.push([{ text: `${INDENT}${INDENT}${logCell(line, width - 6)}`, style: { dim: true } }]);
|
|
174
|
+
}
|
|
175
|
+
lines.push([]); // one blank row between jobs — each item reads as its own block
|
|
176
|
+
return {
|
|
177
|
+
spans: [
|
|
178
|
+
{ text: ` ${ICON.job} `, style: tone },
|
|
179
|
+
{ text: commandCell(job.command, Math.max(20, width - 26)), style: { bold: true } },
|
|
180
|
+
],
|
|
181
|
+
right: [{ text: `${jobElapsed(job)} `, style: tone }],
|
|
182
|
+
lines,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
/** A log line, tabs expanded and clipped to the card — never wrapped, so one
|
|
186
|
+
* runaway line cannot push the rest of the tail off the screen. */
|
|
187
|
+
function logCell(line, width) {
|
|
188
|
+
const flat = line.replace(/\t/g, ' ').replace(/[\u0000-\u0008\u000b-\u001f]/g, '');
|
|
189
|
+
const cap = Math.max(10, width);
|
|
190
|
+
return flat.length > cap ? `${flat.slice(0, cap - 1)}…` : flat;
|
|
191
|
+
}
|
|
192
|
+
function jobsEmptyRows() {
|
|
193
|
+
return [
|
|
194
|
+
[{ text: ` ${ICON.empty} nothing running`, style: { dim: true } }],
|
|
195
|
+
[],
|
|
196
|
+
[{ text: `${INDENT}This node has no bash commands running in the background.`, style: { dim: true } }],
|
|
197
|
+
];
|
|
198
|
+
}
|
|
150
199
|
function scheduleEmptyRows() {
|
|
151
200
|
return [
|
|
152
201
|
[{ text: ` ${ICON.empty} nothing armed`, style: { dim: true } }],
|
|
@@ -185,6 +234,23 @@ export const inspectorTui = {
|
|
|
185
234
|
paint(state, rawRows(state), draw, content);
|
|
186
235
|
return;
|
|
187
236
|
}
|
|
237
|
+
if (state.section === 'jobs') {
|
|
238
|
+
if (state.jobs.error) {
|
|
239
|
+
const d = state.jobs.error.display;
|
|
240
|
+
paint(state, errorRows(d.headline, d.explanation, d.nextStep), draw, content);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const jobs = state.jobs.data ?? [];
|
|
244
|
+
if (jobs.length === 0) {
|
|
245
|
+
paint(state, jobsEmptyRows(), draw, content);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
draw.spans(content.row, content.col, tabStrip(state, content.width), content.width);
|
|
249
|
+
const cursor = Math.max(0, jobs.findIndex((job) => job.job_id === state.selectedJobId));
|
|
250
|
+
const rows = jobs.map((job, index) => jobItem(job, index === cursor && state.expanded, content.width));
|
|
251
|
+
draw.list({ ...content, row: content.row + 2, height: Math.max(0, content.height - 2) }, rows, cursor, state.scroll.jobs);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
188
254
|
if (state.schedule.error) {
|
|
189
255
|
const d = state.schedule.error.display;
|
|
190
256
|
paint(state, errorRows(d.headline, d.explanation, d.nextStep), draw, content);
|
|
@@ -209,8 +275,10 @@ export const inspectorTui = {
|
|
|
209
275
|
{ bindingId: 'crtr.view.inspector.section-prev', intent: 'sectionPrev' },
|
|
210
276
|
{ bindingId: 'crtr.view.inspector.page-down', intent: 'pageDown' },
|
|
211
277
|
{ bindingId: 'crtr.view.inspector.page-up', intent: 'pageUp' },
|
|
212
|
-
{ bindingId: 'crtr.view.inspector.toggle-detail', intent: 'toggleDetail', when: (state) => state.section === 'schedule' && state.selectedCronId !== null, hint: { label: 'details' } },
|
|
278
|
+
{ bindingId: 'crtr.view.inspector.toggle-detail', intent: 'toggleDetail', when: (state) => (state.section === 'schedule' && state.selectedCronId !== null) || (state.section === 'jobs' && state.selectedJobId !== null), hint: { label: 'details' } },
|
|
213
279
|
{ bindingId: 'crtr.view.inspector.cancel-item', intent: 'cancelItem', when: (state) => state.section === 'schedule' && state.selectedCronId !== null, hint: { label: 'cancel' } },
|
|
280
|
+
{ bindingId: 'crtr.view.inspector.cancel-item', intent: 'cancelItem', when: (state) => state.section === 'jobs' && state.selectedJobId !== null, hint: { label: 'stop job' } },
|
|
281
|
+
{ bindingId: 'crtr.view.inspector.open-log', intent: 'openLog', when: (state) => state.section === 'jobs' && state.selectedJobId !== null, hint: { label: 'open log' } },
|
|
214
282
|
{ bindingId: 'crtr.view.inspector.confirm', intent: 'confirm', when: (state) => state.pendingCancel !== null, hint: { label: 'confirm' } },
|
|
215
283
|
{ bindingId: 'crtr.view.inspector.dismiss', intent: 'dismiss', hint: { label: 'dismiss' } },
|
|
216
284
|
{ bindingId: 'crtr.view.inspector.retarget', intent: 'retarget', when: (state) => state.section === 'schedule' && state.selectedCronId !== null, hint: { label: 'retarget' } },
|
|
@@ -31,7 +31,7 @@ const REQUIRED_IDS = [
|
|
|
31
31
|
...ids('crtr.view.linkedin', ['down', 'up', 'open', 'reply', 'react', 'refresh', 'quit', 'submit-reply', 'cancel', 'reaction-previous', 'reaction-next', 'submit-reaction']),
|
|
32
32
|
...ids('crtr.view.prompt-review', ['down', 'up', 'compare-previous', 'compare-next', 'toggle-mode', 'expand', 'export', 'refresh', 'quit']),
|
|
33
33
|
...ids('crtr.view.workspace-sidebar', ['down', 'up', 'open', 'refresh', 'quit']),
|
|
34
|
-
...ids('crtr.view.inspector', ['down', 'up', 'section-next', 'section-prev', 'page-down', 'page-up', 'toggle-detail', 'cancel-item', 'confirm', 'dismiss', 'retarget', 'refresh', 'quit']),
|
|
34
|
+
...ids('crtr.view.inspector', ['down', 'up', 'section-next', 'section-prev', 'page-down', 'page-up', 'toggle-detail', 'cancel-item', 'open-log', 'confirm', 'dismiss', 'retarget', 'refresh', 'quit']),
|
|
35
35
|
...ids('crtr.view-picker', ['down', 'up', 'run', 'quit']),
|
|
36
36
|
...ids('crtr.browse', ['quit', 'down', 'up', 'first', 'last', 'expand-or-child', 'collapse-or-parent', 'toggle-fold', 'tab-next', 'tab-previous', 'tab.1', 'tab.2', 'tab.3', 'tab.4', 'tab.5', 'close-subtree', 'confirm-close', 'sort', 'scope', 'profile', 'residents', 'preview', 'search', 'revive-now', 'focus', 'focus-background']),
|
|
37
37
|
...ids('crtr.browse.search', ['cancel', 'commit', 'delete', 'quit']),
|
|
@@ -8,8 +8,8 @@ type ProfileMenuAction = 'up' | 'down' | 'select' | 'accept-default' | 'abort' |
|
|
|
8
8
|
type SetupAction = 'cancel' | 'tab-next' | 'tab-previous' | 'open' | 'list-down' | 'list-up' | 'toggle' | 'details' | 'advance' | 'cell-previous' | 'cell-next';
|
|
9
9
|
type SetupKeybindingAction = 'edit' | 'commit-edit' | 'cancel-edit' | 'unbind' | 'reset-row' | 'reset-all' | 'save' | 'discard' | 'page-up' | 'page-down' | 'filter' | 'filter-delete' | 'filter-clear';
|
|
10
10
|
type SetupModelsAction = 'open' | 'commit' | 'cancel' | 'scope-next';
|
|
11
|
-
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next' | 'model-ladder.previous' | 'command.inspect' | 'file-review'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view.inspector.${'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
|
|
12
|
-
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.fork">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.background-bash">, BindingDefinition<"crtr.tmux.menu.file-review">, BindingDefinition<"crtr.tmux.menu.pin">, BindingDefinition<"crtr.tmux.menu.show">, BindingDefinition<"crtr.tmux.menu.metadata">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.copy-id">, BindingDefinition<"crtr.tmux.menu.reload-viewer">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.inbox.toggle">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.attach.model-ladder.previous">, BindingDefinition<"crtr.attach.command.inspect">, BindingDefinition<"crtr.attach.file-review">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.revive">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view.inspector.down"> | BindingDefinition<"crtr.view.inspector.up"> | BindingDefinition<"crtr.view.inspector.section-next"> | BindingDefinition<"crtr.view.inspector.section-prev"> | BindingDefinition<"crtr.view.inspector.page-down"> | BindingDefinition<"crtr.view.inspector.page-up"> | BindingDefinition<"crtr.view.inspector.toggle-detail"> | BindingDefinition<"crtr.view.inspector.cancel-item"> | BindingDefinition<"crtr.view.inspector.confirm"> | BindingDefinition<"crtr.view.inspector.dismiss"> | BindingDefinition<"crtr.view.inspector.retarget"> | BindingDefinition<"crtr.view.inspector.refresh"> | BindingDefinition<"crtr.view.inspector.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.9" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.6">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
11
|
+
export type BindingId = `crtr.tmux.menu.${TmuxMenuAction}` | `crtr.tmux.menu.issue.${'idea' | 'bug'}` | `crtr.tmux.node.${'next' | 'previous'}` | `crtr.tmux.view.${'next' | 'previous'}` | 'crtr.tmux.inbox.toggle' | `crtr.attach.${'detach' | 'clear-or-detach' | 'graph.toggle' | 'model-ladder.next' | 'model-ladder.previous' | 'command.inspect' | 'file-review'}` | 'crtr.mode.cycle' | `crtr.graph.${GraphAction}` | `crtr.graph.confirm.${'accept' | 'cancel'}` | `crtr.view.host.${'force-quit' | 'editor.backspace'}` | `crtr.view.canvas.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.chat.${'submit' | 'contextual-cancel' | 'reconnect' | 'quit'}` | `crtr.view.git-pr.${'down' | 'up' | 'refresh' | 'quit'}` | `crtr.view.inbox.${InboxAction}` | `crtr.view.linkedin.${LinkedinAction}` | `crtr.view.prompt-review.${'down' | 'up' | 'compare-previous' | 'compare-next' | 'toggle-mode' | 'expand' | 'export' | 'refresh' | 'quit'}` | `crtr.view.workspace-sidebar.${'down' | 'up' | 'open' | 'refresh' | 'quit'}` | `crtr.view.inspector.${'down' | 'up' | 'section-next' | 'section-prev' | 'page-down' | 'page-up' | 'toggle-detail' | 'cancel-item' | 'open-log' | 'confirm' | 'dismiss' | 'retarget' | 'refresh' | 'quit'}` | `crtr.view-picker.${'down' | 'up' | 'run' | 'quit'}` | `crtr.browse.${BrowseAction}` | `crtr.browse.search.${'cancel' | 'commit' | 'delete' | 'quit'}` | `crtr.profile.menu.${ProfileMenuAction}` | `crtr.profile.search.${'up' | 'down' | 'pick' | 'back' | 'delete' | 'abort'}` | `crtr.setup.${SetupAction}` | `crtr.setup.confirm.${'accept' | 'back'}` | `crtr.setup.keybindings.${SetupKeybindingAction}` | `crtr.setup.models.${SetupModelsAction}`;
|
|
12
|
+
export declare const BINDING_CATALOG: readonly [BindingDefinition<"crtr.tmux.menu.open">, BindingDefinition<"crtr.tmux.menu.promote">, BindingDefinition<"crtr.tmux.menu.resume">, BindingDefinition<"crtr.tmux.menu.fork">, BindingDefinition<"crtr.tmux.menu.demote">, BindingDefinition<"crtr.tmux.menu.detach">, BindingDefinition<"crtr.tmux.menu.close-subtree">, BindingDefinition<"crtr.tmux.menu.background-bash">, BindingDefinition<"crtr.tmux.menu.file-review">, BindingDefinition<"crtr.tmux.menu.pin">, BindingDefinition<"crtr.tmux.menu.show">, BindingDefinition<"crtr.tmux.menu.metadata">, BindingDefinition<"crtr.tmux.menu.context">, BindingDefinition<"crtr.tmux.menu.providers">, BindingDefinition<"crtr.tmux.menu.graph">, BindingDefinition<"crtr.tmux.menu.focus-manager">, BindingDefinition<"crtr.tmux.menu.copy-id">, BindingDefinition<"crtr.tmux.menu.reload-viewer">, BindingDefinition<"crtr.tmux.menu.issues">, BindingDefinition<"crtr.tmux.menu.issue.idea">, BindingDefinition<"crtr.tmux.menu.issue.bug">, BindingDefinition<"crtr.tmux.node.next">, BindingDefinition<"crtr.tmux.node.previous">, BindingDefinition<"crtr.tmux.inbox.toggle">, BindingDefinition<"crtr.tmux.view.next">, BindingDefinition<"crtr.tmux.view.previous">, BindingDefinition<"crtr.attach.detach">, BindingDefinition<"crtr.attach.clear-or-detach">, BindingDefinition<"crtr.attach.graph.toggle">, BindingDefinition<"crtr.attach.model-ladder.next">, BindingDefinition<"crtr.attach.model-ladder.previous">, BindingDefinition<"crtr.attach.command.inspect">, BindingDefinition<"crtr.attach.file-review">, BindingDefinition<"crtr.mode.cycle">, BindingDefinition<"crtr.graph.down">, BindingDefinition<"crtr.graph.up">, BindingDefinition<"crtr.graph.first">, BindingDefinition<"crtr.graph.last">, BindingDefinition<"crtr.graph.collapse-or-manager">, BindingDefinition<"crtr.graph.expand-or-child">, BindingDefinition<"crtr.graph.focus">, BindingDefinition<"crtr.graph.revive">, BindingDefinition<"crtr.graph.focus-manager">, BindingDefinition<"crtr.graph.close-subtree">, BindingDefinition<"crtr.graph.dismiss">, BindingDefinition<"crtr.graph.confirm.accept">, BindingDefinition<"crtr.graph.confirm.cancel">, BindingDefinition<"crtr.view.host.force-quit">, BindingDefinition<"crtr.view.host.editor.backspace">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<"crtr.view.chat.submit">, BindingDefinition<"crtr.view.chat.contextual-cancel">, BindingDefinition<"crtr.view.chat.reconnect">, BindingDefinition<"crtr.view.chat.quit">, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, BindingDefinition<BindingId>, ...(BindingDefinition<BindingId> | BindingDefinition<"crtr.view.prompt-review.down"> | BindingDefinition<"crtr.view.prompt-review.up"> | BindingDefinition<"crtr.view.prompt-review.compare-previous"> | BindingDefinition<"crtr.view.prompt-review.compare-next"> | BindingDefinition<"crtr.view.prompt-review.toggle-mode"> | BindingDefinition<"crtr.view.prompt-review.expand"> | BindingDefinition<"crtr.view.prompt-review.export"> | BindingDefinition<"crtr.view.prompt-review.refresh"> | BindingDefinition<"crtr.view.prompt-review.quit"> | BindingDefinition<"crtr.view.workspace-sidebar.down"> | BindingDefinition<"crtr.view.workspace-sidebar.up"> | BindingDefinition<"crtr.view.workspace-sidebar.open"> | BindingDefinition<"crtr.view.workspace-sidebar.refresh"> | BindingDefinition<"crtr.view.workspace-sidebar.quit"> | BindingDefinition<"crtr.view.inspector.down"> | BindingDefinition<"crtr.view.inspector.up"> | BindingDefinition<"crtr.view.inspector.section-next"> | BindingDefinition<"crtr.view.inspector.section-prev"> | BindingDefinition<"crtr.view.inspector.page-down"> | BindingDefinition<"crtr.view.inspector.page-up"> | BindingDefinition<"crtr.view.inspector.toggle-detail"> | BindingDefinition<"crtr.view.inspector.cancel-item"> | BindingDefinition<"crtr.view.inspector.open-log"> | BindingDefinition<"crtr.view.inspector.confirm"> | BindingDefinition<"crtr.view.inspector.dismiss"> | BindingDefinition<"crtr.view.inspector.retarget"> | BindingDefinition<"crtr.view.inspector.refresh"> | BindingDefinition<"crtr.view.inspector.quit"> | BindingDefinition<"crtr.view-picker.down"> | BindingDefinition<"crtr.view-picker.up"> | BindingDefinition<"crtr.view-picker.run"> | BindingDefinition<"crtr.view-picker.quit"> | BindingDefinition<"crtr.browse.quit"> | BindingDefinition<"crtr.browse.down"> | BindingDefinition<"crtr.browse.up"> | BindingDefinition<"crtr.browse.first"> | BindingDefinition<"crtr.browse.last"> | BindingDefinition<"crtr.browse.expand-or-child"> | BindingDefinition<"crtr.browse.collapse-or-parent"> | BindingDefinition<"crtr.browse.toggle-fold"> | BindingDefinition<"crtr.browse.tab-next"> | BindingDefinition<"crtr.browse.tab-previous"> | BindingDefinition<"crtr.browse.tab.2" | "crtr.browse.tab.1" | "crtr.browse.tab.3" | "crtr.browse.tab.4" | "crtr.browse.tab.5"> | BindingDefinition<"crtr.browse.close-subtree"> | BindingDefinition<"crtr.browse.confirm-close"> | BindingDefinition<"crtr.browse.sort"> | BindingDefinition<"crtr.browse.scope"> | BindingDefinition<"crtr.browse.profile"> | BindingDefinition<"crtr.browse.residents"> | BindingDefinition<"crtr.browse.preview"> | BindingDefinition<"crtr.browse.search"> | BindingDefinition<"crtr.browse.revive-now"> | BindingDefinition<"crtr.browse.focus"> | BindingDefinition<"crtr.browse.focus-background"> | BindingDefinition<"crtr.browse.search.cancel"> | BindingDefinition<"crtr.browse.search.commit"> | BindingDefinition<"crtr.browse.search.delete"> | BindingDefinition<"crtr.browse.search.quit"> | BindingDefinition<"crtr.profile.menu.up"> | BindingDefinition<"crtr.profile.menu.down"> | BindingDefinition<"crtr.profile.menu.select"> | BindingDefinition<"crtr.profile.menu.accept-default"> | BindingDefinition<"crtr.profile.menu.abort"> | BindingDefinition<"crtr.profile.menu.slot.2" | "crtr.profile.menu.slot.1" | "crtr.profile.menu.slot.3" | "crtr.profile.menu.slot.4" | "crtr.profile.menu.slot.5" | "crtr.profile.menu.slot.9" | "crtr.profile.menu.slot.8" | "crtr.profile.menu.slot.7" | "crtr.profile.menu.slot.6">)[], BindingDefinition<"crtr.profile.menu.create">, BindingDefinition<"crtr.profile.menu.search-existing">, BindingDefinition<"crtr.profile.menu.use-root">, BindingDefinition<"crtr.profile.menu.set-default">, BindingDefinition<"crtr.profile.menu.add-directory">, BindingDefinition<"crtr.profile.menu.unpin">, BindingDefinition<"crtr.profile.search.up">, BindingDefinition<"crtr.profile.search.down">, BindingDefinition<"crtr.profile.search.pick">, BindingDefinition<"crtr.profile.search.back">, BindingDefinition<"crtr.profile.search.delete">, BindingDefinition<"crtr.profile.search.abort">, BindingDefinition<"crtr.setup.cancel">, BindingDefinition<"crtr.setup.tab-next">, BindingDefinition<"crtr.setup.tab-previous">, BindingDefinition<"crtr.setup.open">, BindingDefinition<"crtr.setup.list-down">, BindingDefinition<"crtr.setup.list-up">, BindingDefinition<"crtr.setup.toggle">, BindingDefinition<"crtr.setup.details">, BindingDefinition<"crtr.setup.advance">, BindingDefinition<"crtr.setup.confirm.accept">, BindingDefinition<"crtr.setup.confirm.back">, BindingDefinition<"crtr.setup.keybindings.edit">, BindingDefinition<"crtr.setup.keybindings.commit-edit">, BindingDefinition<"crtr.setup.keybindings.cancel-edit">, BindingDefinition<"crtr.setup.keybindings.unbind">, BindingDefinition<"crtr.setup.keybindings.reset-row">, BindingDefinition<"crtr.setup.keybindings.reset-all">, BindingDefinition<"crtr.setup.keybindings.save">, BindingDefinition<"crtr.setup.keybindings.discard">, BindingDefinition<"crtr.setup.keybindings.page-up">, BindingDefinition<"crtr.setup.keybindings.page-down">, BindingDefinition<"crtr.setup.keybindings.filter">, BindingDefinition<"crtr.setup.keybindings.filter-delete">, BindingDefinition<"crtr.setup.keybindings.filter-clear">, BindingDefinition<"crtr.setup.cell-previous">, BindingDefinition<"crtr.setup.cell-next">, BindingDefinition<"crtr.setup.models.open">, BindingDefinition<"crtr.setup.models.scope-next">, BindingDefinition<"crtr.setup.models.commit">, BindingDefinition<"crtr.setup.models.cancel">];
|
|
13
13
|
export declare const BINDING_IDS: ReadonlySet<string>;
|
|
14
14
|
export declare const SAFETY_REQUIREMENTS: readonly SafetyRequirement[];
|
|
15
15
|
export {};
|
|
@@ -118,8 +118,9 @@ export const BINDING_CATALOG = Object.freeze([
|
|
|
118
118
|
b('crtr.view.inspector.section-prev', 'Views', 'Previous Inspector section', 'Move to the previous Inspector section.', ['shift+tab'], 'terminal', ['view.inspector']),
|
|
119
119
|
b('crtr.view.inspector.page-down', 'Views', 'Page down Inspector', 'Scroll the Inspector detail body down.', ['ctrl+d', 'pagedown'], 'terminal', ['view.inspector']),
|
|
120
120
|
b('crtr.view.inspector.page-up', 'Views', 'Page up Inspector', 'Scroll the Inspector detail body up.', ['ctrl+u', 'pageup'], 'terminal', ['view.inspector']),
|
|
121
|
-
b('crtr.view.inspector.toggle-detail', 'Views', 'Toggle
|
|
122
|
-
b('crtr.view.inspector.cancel-item', 'Views', 'Cancel
|
|
121
|
+
b('crtr.view.inspector.toggle-detail', 'Views', 'Toggle item detail', 'Expand or collapse the selected cron or background job.', ['enter'], 'terminal', ['view.inspector']),
|
|
122
|
+
b('crtr.view.inspector.cancel-item', 'Views', 'Cancel selected item', 'Arm cancellation for the selected cron, or stopping for the selected background job.', ['x'], 'terminal', ['view.inspector']),
|
|
123
|
+
b('crtr.view.inspector.open-log', 'Views', 'Open job log', 'Follow the selected background job’s log in a new tmux window.', ['o'], 'terminal', ['view.inspector']),
|
|
123
124
|
b('crtr.view.inspector.confirm', 'Views', 'Confirm Inspector action', 'Confirm the armed Inspector action.', ['y'], 'terminal', ['view.inspector']),
|
|
124
125
|
b('crtr.view.inspector.dismiss', 'Views', 'Dismiss Inspector action', 'Dismiss an Inspector confirmation or banner.', ['escape'], 'terminal', ['view.inspector'], 'dismiss-modal'),
|
|
125
126
|
b('crtr.view.inspector.retarget', 'Views', 'Retarget Inspector', 'Inspect the selected cron anchor or creator node.', ['t'], 'terminal', ['view.inspector']),
|