@detergent-software/conductor 2.2.0-dev.1 → 2.2.0-dev.3
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 +23 -0
- package/README.md +9 -5
- package/build/code-host.js +11 -0
- package/build/code-host.js.map +1 -1
- package/build/daemon.js +5 -3
- package/build/daemon.js.map +1 -1
- package/build/follow-ups.js +0 -3
- package/build/follow-ups.js.map +1 -1
- package/build/notes.js +1 -0
- package/build/notes.js.map +1 -1
- package/build/profile.js +11 -3
- package/build/profile.js.map +1 -1
- package/build/queue.js +25 -2
- package/build/queue.js.map +1 -1
- package/build/scheduler.js +85 -27
- package/build/scheduler.js.map +1 -1
- package/build/store.js +7 -3
- package/build/store.js.map +1 -1
- package/build/watchers/boards-queue.js +43 -2
- package/build/watchers/boards-queue.js.map +1 -1
- package/build/watchers/boards-writes.js +6 -2
- package/build/watchers/boards-writes.js.map +1 -1
- package/build/watchers/boards.js +15 -6
- package/build/watchers/boards.js.map +1 -1
- package/build/watchers/issues-queue.js +47 -5
- package/build/watchers/issues-queue.js.map +1 -1
- package/build/watchers/issues-writes.js +21 -2
- package/build/watchers/issues-writes.js.map +1 -1
- package/build/watchers/issues.js +6 -5
- package/build/watchers/issues.js.map +1 -1
- package/build/watchers/linear-queue.js +6 -20
- package/build/watchers/linear-queue.js.map +1 -1
- package/build/watchers/release-wait.js +30 -0
- package/build/watchers/release-wait.js.map +1 -0
- package/mod/README.md +2 -0
- package/mod/hooks/model.ts +284 -281
- package/mod/hooks/register.tsx +266 -265
- package/mod/types/index.d.ts +26 -26
- package/package.json +4 -4
package/mod/hooks/register.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { atom, read, update } from 'claude-code';
|
|
2
1
|
import type { EngineInterface, Register, RenderElement, Timer } from 'claude-code';
|
|
3
2
|
|
|
3
|
+
import { atom, read, update } from 'claude-code';
|
|
4
|
+
|
|
4
5
|
import type { ConductorSnapshot } from '../types';
|
|
5
6
|
|
|
6
7
|
import {
|
|
7
8
|
age,
|
|
8
|
-
type ApiAnswer,
|
|
9
9
|
answerText,
|
|
10
|
+
type ApiAnswer,
|
|
10
11
|
bandEntries,
|
|
11
12
|
clearedText,
|
|
12
13
|
CONSOLE_SKILL,
|
|
@@ -65,11 +66,11 @@ const ATTACH_RUN_MS = 10 * 60_000;
|
|
|
65
66
|
const ANSWER_RUN_MS = 2 * 60_000;
|
|
66
67
|
const NOT_RUNNING = 'the conductor is not running (conduct serve)';
|
|
67
68
|
|
|
68
|
-
const snapshot = atom({
|
|
69
|
-
const confirming = atom({
|
|
70
|
-
const expanded = atom({
|
|
71
|
-
const hasSkill = atom({
|
|
72
|
-
const cleared = atom({
|
|
69
|
+
const snapshot = atom({ key: 'snapshot', plugin: 'conductor' } as const, null);
|
|
70
|
+
const confirming = atom({ key: 'confirming', plugin: 'conductor' } as const, null);
|
|
71
|
+
const expanded = atom({ key: 'expanded', plugin: 'conductor' } as const, null);
|
|
72
|
+
const hasSkill = atom({ key: 'hasSkill', plugin: 'conductor' } as const, false);
|
|
73
|
+
const cleared = atom({ key: 'cleared', plugin: 'conductor' } as const, null);
|
|
73
74
|
|
|
74
75
|
// The console is the session started with CONDUCTOR_CONSOLE=1 (`conduct console` sets it): only it
|
|
75
76
|
// draws the band and the pane, answers the commands, follows the stream and acts on events. Both
|
|
@@ -94,22 +95,70 @@ let isWindows = false;
|
|
|
94
95
|
|
|
95
96
|
type Endpoint = { port: number; token: string };
|
|
96
97
|
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let found: Endpoint | null;
|
|
98
|
+
// Jason's answer to an inbox item, as `conduct approve|revise|done` posts it; the line to show.
|
|
99
|
+
// Approve and revise run `conduct` (ANSWER_RUN_MS), by inbox id. Without the CLI, the API, which
|
|
100
|
+
// $.http.fetch cuts off after 30 s, possibly after the comment was posted. Done doesn't ask Linear.
|
|
101
|
+
async function answer(
|
|
102
|
+
$: EngineInterface,
|
|
103
|
+
id: number,
|
|
104
|
+
body: { action: 'approve' | 'done' } | { action: 'revise'; text: string },
|
|
105
|
+
): Promise<string> {
|
|
106
106
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
const cli = cliOf($);
|
|
108
|
+
if (body.action !== 'done' && (await $.fs.exists(cli))) {
|
|
109
|
+
const failed = `not ${body.action}d`;
|
|
110
|
+
const argv = body.action === 'revise' ? ['revise', `#${id}`, body.text] : ['approve', `#${id}`];
|
|
111
|
+
let run;
|
|
112
|
+
try {
|
|
113
|
+
run = await $.process.run(['node', cli, ...argv], { timeoutMs: ANSWER_RUN_MS });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return `${failed}: ${error instanceof Error ? error.message : String(error)}`;
|
|
116
|
+
}
|
|
117
|
+
refreshSoon($);
|
|
118
|
+
return runText(run, {
|
|
119
|
+
failed,
|
|
120
|
+
ok: `${body.action === 'approve' ? 'approved' : 'asked for a revision of'} #${id}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const at = await endpoint($);
|
|
124
|
+
let result: ApiAnswer;
|
|
125
|
+
try {
|
|
126
|
+
result = await callApi($, 'POST', `/inbox/${id}/resolve`, body, at);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (body.action === 'done') throw error;
|
|
129
|
+
return `not ${body.action}d: ${error instanceof Error ? error.message : String(error)}; ${MAYBE_POSTED}`;
|
|
130
|
+
}
|
|
131
|
+
refreshSoon($);
|
|
132
|
+
return answerText(body.action, id, result);
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return error instanceof Error ? error.message : String(error);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Starts the console's idle point over: WARM_AFTER_MS from now, unless a turn comes first.
|
|
139
|
+
function armIdle($: EngineInterface): void {
|
|
140
|
+
idleTimer?.cancel();
|
|
141
|
+
idleTimer = $.clock.after(WARM_AFTER_MS, () => void onIdle($));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Through `conduct attach` (ATTACH_RUN_MS), by id: a name is unique only within its project.
|
|
145
|
+
// Without the CLI, the API, which answers within 30 s only when the worker needs no settle time
|
|
146
|
+
// and resumes quickly. The daemon refuses a second attach of a worker while one runs.
|
|
147
|
+
async function attach($: EngineInterface, id: number, name: string): Promise<string> {
|
|
148
|
+
try {
|
|
149
|
+
const cli = cliOf($);
|
|
150
|
+
if (await $.fs.exists(cli)) {
|
|
151
|
+
$.ui.toast(`attaching to ${name}…`);
|
|
152
|
+
const run = await $.process.run(['node', cli, 'attach', `#${id}`], { timeoutMs: ATTACH_RUN_MS });
|
|
153
|
+
refreshSoon($);
|
|
154
|
+
return runText(run, { failed: `could not attach to ${name}`, ok: `attached to ${name}` });
|
|
155
|
+
}
|
|
156
|
+
const result = await callApi($, 'POST', `/workers/${id}/attach`);
|
|
157
|
+
refreshSoon($);
|
|
158
|
+
return result.status === 200 ? `attached to ${name}` : `could not attach to ${name}: ${errorOf(result)}`;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return error instanceof Error ? error.message : String(error);
|
|
110
161
|
}
|
|
111
|
-
if (found === null) throw new Error(NOT_RUNNING);
|
|
112
|
-
return found;
|
|
113
162
|
}
|
|
114
163
|
|
|
115
164
|
// One call on the daemon's API (127.0.0.1, with its token). Throws when none runs.
|
|
@@ -129,7 +178,7 @@ async function callApi(
|
|
|
129
178
|
},
|
|
130
179
|
method,
|
|
131
180
|
});
|
|
132
|
-
let parsed: unknown
|
|
181
|
+
let parsed: unknown;
|
|
133
182
|
try {
|
|
134
183
|
parsed = answer.text ? JSON.parse(answer.text) : null;
|
|
135
184
|
} catch {
|
|
@@ -138,116 +187,90 @@ async function callApi(
|
|
|
138
187
|
return { body: parsed, status: answer.status };
|
|
139
188
|
}
|
|
140
189
|
|
|
141
|
-
//
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
let next: ConductorSnapshot;
|
|
145
|
-
try {
|
|
146
|
-
const at = await endpoint($);
|
|
147
|
-
const [fleet, inbox, notify] = await Promise.all([
|
|
148
|
-
callApi($, 'GET', '/fleet', undefined, at),
|
|
149
|
-
callApi($, 'GET', '/inbox', undefined, at),
|
|
150
|
-
callApi($, 'GET', '/notify', undefined, at),
|
|
151
|
-
]);
|
|
152
|
-
next = snapshotOf(fleet, inbox, readAt, notify);
|
|
153
|
-
} catch (error) {
|
|
154
|
-
next = {
|
|
155
|
-
error: error instanceof Error ? error.message : String(error),
|
|
156
|
-
inbox: [],
|
|
157
|
-
notify: null,
|
|
158
|
-
readAt,
|
|
159
|
-
workers: [],
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
await update($, snapshot, () => next);
|
|
163
|
-
return next;
|
|
190
|
+
// The conductor's own CLI, beside the mod in its package; a mod checkout without a build has none.
|
|
191
|
+
function cliOf($: EngineInterface): string {
|
|
192
|
+
return `${$.plugin.root}/../build/cli.js`;
|
|
164
193
|
}
|
|
165
194
|
|
|
166
|
-
//
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
$.clock.after(DEBOUNCE_MS, () => {
|
|
171
|
-
isRefreshing = false;
|
|
172
|
-
void refresh($);
|
|
173
|
-
});
|
|
195
|
+
// Stages a band approve for its `y`; it lapses unless pressed within CONFIRM_MS.
|
|
196
|
+
async function confirm($: EngineInterface, id: number): Promise<void> {
|
|
197
|
+
await update($, confirming, () => id);
|
|
198
|
+
$.clock.after(CONFIRM_MS, () => void update($, confirming, (staged) => (staged === id ? null : staged)));
|
|
174
199
|
}
|
|
175
200
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
switch (act.kind) {
|
|
184
|
-
case 'disconnected': {
|
|
185
|
-
const readAt = await $.clock.now();
|
|
186
|
-
await update($, snapshot, () => ({ error: NOT_RUNNING, inbox: [], notify: null, readAt, workers: [] }));
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
case 'escalate':
|
|
190
|
-
// The one place the mod wakes the console's model: facts only, as the rules engine wrote them.
|
|
191
|
-
await $.prompt.submit({ text: act.text });
|
|
192
|
-
return;
|
|
193
|
-
case 'ignore':
|
|
194
|
-
return;
|
|
195
|
-
case 'push':
|
|
196
|
-
await push($, act.message);
|
|
197
|
-
return;
|
|
198
|
-
case 'refresh':
|
|
199
|
-
if (act.toast !== null) $.ui.toast(act.toast);
|
|
200
|
-
refreshSoon($);
|
|
201
|
-
return;
|
|
202
|
-
case 'send':
|
|
203
|
-
await $.session.send({ text: act.text, to: act.to });
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
} catch (error) {
|
|
207
|
-
$.ui.log(`conductor: ${line.kind} failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
208
|
-
to: 'debug',
|
|
209
|
-
});
|
|
210
|
-
}
|
|
201
|
+
async function copy(
|
|
202
|
+
$: EngineInterface,
|
|
203
|
+
text: string,
|
|
204
|
+
surface: Parameters<EngineInterface['ui']['copy']>[0]['surface'],
|
|
205
|
+
): Promise<void> {
|
|
206
|
+
const copied = await $.ui.copy({ surface, text });
|
|
207
|
+
$.ui.toast(copied.isCopied ? `copied: ${text}` : `not copied: ${text}`);
|
|
211
208
|
}
|
|
212
209
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
async function push($: EngineInterface, message: string): Promise<void> {
|
|
217
|
-
let outcome: PushOutcome;
|
|
218
|
-
try {
|
|
219
|
-
outcome = pushOutcome(await $.tool.call({ message, status: 'proactive', tool: 'PushNotification' }));
|
|
220
|
-
} catch (error) {
|
|
221
|
-
outcome = { detail: error instanceof Error ? error.message : String(error), state: 'failed' };
|
|
222
|
-
}
|
|
223
|
-
const warning = pushWarning(outcome);
|
|
224
|
-
if (outcome.state === 'ok') warnedInactive = false;
|
|
225
|
-
if (warning !== null && !warnedInactive) {
|
|
226
|
-
warnedInactive = true;
|
|
227
|
-
$.ui.toast(warning);
|
|
228
|
-
}
|
|
229
|
-
try {
|
|
230
|
-
await callApi($, 'POST', '/notify/report', {
|
|
231
|
-
channel: 'remote-control',
|
|
232
|
-
result: outcome.detail,
|
|
233
|
-
state: reportedState(outcome),
|
|
234
|
-
});
|
|
235
|
-
} catch (error) {
|
|
236
|
-
$.ui.log(`conductor: notify report failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
237
|
-
to: 'debug',
|
|
238
|
-
});
|
|
239
|
-
}
|
|
210
|
+
function disarmIdle(): void {
|
|
211
|
+
idleTimer?.cancel();
|
|
212
|
+
idleTimer = null;
|
|
240
213
|
}
|
|
241
214
|
|
|
242
|
-
//
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
215
|
+
// The running daemon's port and token, from its home's files. Throws when none runs.
|
|
216
|
+
async function endpoint($: EngineInterface): Promise<Endpoint> {
|
|
217
|
+
const home = homeOf({
|
|
218
|
+
conductorHome: await $.env.get('CONDUCTOR_HOME'),
|
|
219
|
+
home: await $.env.get('HOME'),
|
|
220
|
+
userProfile: await $.env.get('USERPROFILE'),
|
|
221
|
+
});
|
|
222
|
+
if (home === null) throw new Error(NOT_RUNNING);
|
|
223
|
+
let found: Endpoint | null;
|
|
224
|
+
try {
|
|
225
|
+
found = endpointOf(await $.fs.read(`${home}/port`), await $.fs.read(`${home}/token`));
|
|
226
|
+
} catch {
|
|
227
|
+
found = null;
|
|
228
|
+
}
|
|
229
|
+
if (found === null) throw new Error(NOT_RUNNING);
|
|
230
|
+
return found;
|
|
246
231
|
}
|
|
247
232
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
233
|
+
// `conduct events --follow` from this package's own build, so the mod and the CLI are one version
|
|
234
|
+
// and no .cmd shim is in the way. It reconnects by itself; a child that ends is started again after
|
|
235
|
+
// the last event it showed, so nothing in between is lost. One that keeps exiting at once (no
|
|
236
|
+
// build, no node) is given up on, and the poll carries on alone.
|
|
237
|
+
async function follow($: EngineInterface): Promise<void> {
|
|
238
|
+
const cli = cliOf($);
|
|
239
|
+
if (!(await $.fs.exists(cli))) {
|
|
240
|
+
$.ui.log(`conductor: no ${cli}; the console polls instead of following events`, { to: 'debug' });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
for (let quickExits = 0; quickExits < 3;) {
|
|
244
|
+
const startedAt = await $.clock.now();
|
|
245
|
+
let buffer = '';
|
|
246
|
+
try {
|
|
247
|
+
// --console: while this stream is open, the daemon knows escalations have a console to wake.
|
|
248
|
+
const argv = [
|
|
249
|
+
'node',
|
|
250
|
+
cli,
|
|
251
|
+
'events',
|
|
252
|
+
'--follow',
|
|
253
|
+
'--console',
|
|
254
|
+
...(lastEventId === null ? [] : ['--since', String(lastEventId)]),
|
|
255
|
+
];
|
|
256
|
+
for await (const chunk of $.process.spawn({ argv })) {
|
|
257
|
+
if (chunk.stream !== 'stdout') continue;
|
|
258
|
+
buffer += chunk.text;
|
|
259
|
+
for (let end = buffer.indexOf('\n'); end >= 0; end = buffer.indexOf('\n')) {
|
|
260
|
+
const text = buffer.slice(0, end).trim();
|
|
261
|
+
buffer = buffer.slice(end + 1);
|
|
262
|
+
if (text !== '') await onLine($, text);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch (error) {
|
|
266
|
+
$.ui.log(`conductor: conduct events failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
267
|
+
to: 'debug',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
quickExits = (await $.clock.now()) - startedAt < 10_000 ? quickExits + 1 : 0;
|
|
271
|
+
await new Promise<void>((done) => $.clock.after(RESTART_MS, done));
|
|
272
|
+
}
|
|
273
|
+
$.ui.log('conductor: conduct events keeps exiting; the console polls instead', { to: 'debug' });
|
|
251
274
|
}
|
|
252
275
|
|
|
253
276
|
// The console has been idle WARM_AFTER_MS: keep its cache warm with a ping, or clear it (ATK-386).
|
|
@@ -301,108 +324,57 @@ async function onIdle($: EngineInterface): Promise<void> {
|
|
|
301
324
|
await reportCache($, { action: outcome.kind, contextTokens: contextTokens ?? null, usage: outcome.usage, warms });
|
|
302
325
|
}
|
|
303
326
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
327
|
+
// One line of the follow stream, carried out.
|
|
328
|
+
async function onLine($: EngineInterface, text: string): Promise<void> {
|
|
329
|
+
const line = parseLine(text);
|
|
330
|
+
if (line === null) return;
|
|
331
|
+
if (line.id !== null) lastEventId = line.id;
|
|
332
|
+
const act = reaction(line);
|
|
333
|
+
try {
|
|
334
|
+
switch (act.kind) {
|
|
335
|
+
case 'disconnected': {
|
|
336
|
+
const readAt = await $.clock.now();
|
|
337
|
+
await update($, snapshot, () => ({ error: NOT_RUNNING, inbox: [], notify: null, readAt, workers: [] }));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
case 'escalate':
|
|
341
|
+
// The one place the mod wakes the console's model: facts only, as the rules engine wrote them.
|
|
342
|
+
await $.prompt.submit({ text: act.text });
|
|
343
|
+
return;
|
|
344
|
+
case 'ignore':
|
|
345
|
+
return;
|
|
346
|
+
case 'push':
|
|
347
|
+
await push($, act.message);
|
|
348
|
+
return;
|
|
349
|
+
case 'refresh':
|
|
350
|
+
if (act.toast !== null) $.ui.toast(act.toast);
|
|
351
|
+
refreshSoon($);
|
|
352
|
+
return;
|
|
353
|
+
case 'send':
|
|
354
|
+
await $.session.send({ text: act.text, to: act.to });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
307
357
|
} catch (error) {
|
|
308
|
-
$.ui.log(`conductor:
|
|
358
|
+
$.ui.log(`conductor: ${line.kind} failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
309
359
|
to: 'debug',
|
|
310
360
|
});
|
|
311
361
|
}
|
|
312
362
|
}
|
|
313
363
|
|
|
314
|
-
//
|
|
315
|
-
|
|
364
|
+
// The Windows terminal's [open]: the default browser, run once (ATK-494). A Link there is an OSC 8
|
|
365
|
+
// span, which a ctrl+click in a pane opened twice: the pane and Windows Terminal each opened it.
|
|
366
|
+
// rundll32 hands the URL over whole (explorer.exe would split it at a comma) and exits 0.
|
|
367
|
+
async function openLink($: EngineInterface, href: string): Promise<void> {
|
|
316
368
|
try {
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
return error instanceof Error ? error.message : String(error);
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// `conduct events --follow` from this package's own build, so the mod and the CLI are one version
|
|
326
|
-
// and no .cmd shim is in the way. It reconnects by itself; a child that ends is started again after
|
|
327
|
-
// the last event it showed, so nothing in between is lost. One that keeps exiting at once (no
|
|
328
|
-
// build, no node) is given up on, and the poll carries on alone.
|
|
329
|
-
async function follow($: EngineInterface): Promise<void> {
|
|
330
|
-
const cli = cliOf($);
|
|
331
|
-
if (!(await $.fs.exists(cli))) {
|
|
332
|
-
$.ui.log(`conductor: no ${cli}; the console polls instead of following events`, { to: 'debug' });
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
for (let quickExits = 0; quickExits < 3;) {
|
|
336
|
-
const startedAt = await $.clock.now();
|
|
337
|
-
let buffer = '';
|
|
338
|
-
try {
|
|
339
|
-
// --console: while this stream is open, the daemon knows escalations have a console to wake.
|
|
340
|
-
const argv = [
|
|
341
|
-
'node',
|
|
342
|
-
cli,
|
|
343
|
-
'events',
|
|
344
|
-
'--follow',
|
|
345
|
-
'--console',
|
|
346
|
-
...(lastEventId === null ? [] : ['--since', String(lastEventId)]),
|
|
347
|
-
];
|
|
348
|
-
for await (const chunk of $.process.spawn({ argv })) {
|
|
349
|
-
if (chunk.stream !== 'stdout') continue;
|
|
350
|
-
buffer += chunk.text;
|
|
351
|
-
for (let end = buffer.indexOf('\n'); end >= 0; end = buffer.indexOf('\n')) {
|
|
352
|
-
const text = buffer.slice(0, end).trim();
|
|
353
|
-
buffer = buffer.slice(end + 1);
|
|
354
|
-
if (text !== '') await onLine($, text);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
} catch (error) {
|
|
358
|
-
$.ui.log(`conductor: conduct events failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
359
|
-
to: 'debug',
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
|
-
quickExits = (await $.clock.now()) - startedAt < 10_000 ? quickExits + 1 : 0;
|
|
363
|
-
await new Promise<void>((done) => $.clock.after(RESTART_MS, done));
|
|
369
|
+
const run = await $.process.run(['rundll32.exe', 'url.dll,FileProtocolHandler', href]);
|
|
370
|
+
if (run.exitCode !== 0) $.ui.toast(`not opened: ${href}`);
|
|
371
|
+
} catch {
|
|
372
|
+
$.ui.toast(`not opened: ${href}`);
|
|
364
373
|
}
|
|
365
|
-
$.ui.log('conductor: conduct events keeps exiting; the console polls instead', { to: 'debug' });
|
|
366
374
|
}
|
|
367
375
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
// $.http.fetch cuts off after 30 s, possibly after the comment was posted. Done doesn't ask Linear.
|
|
371
|
-
async function answer(
|
|
372
|
-
$: EngineInterface,
|
|
373
|
-
id: number,
|
|
374
|
-
body: { action: 'approve' | 'done' } | { action: 'revise'; text: string },
|
|
375
|
-
): Promise<string> {
|
|
376
|
-
try {
|
|
377
|
-
const cli = cliOf($);
|
|
378
|
-
if (body.action !== 'done' && (await $.fs.exists(cli))) {
|
|
379
|
-
const failed = `not ${body.action}d`;
|
|
380
|
-
const argv = body.action === 'approve' ? ['approve', `#${id}`] : ['revise', `#${id}`, body.text];
|
|
381
|
-
let run;
|
|
382
|
-
try {
|
|
383
|
-
run = await $.process.run(['node', cli, ...argv], { timeoutMs: ANSWER_RUN_MS });
|
|
384
|
-
} catch (error) {
|
|
385
|
-
return `${failed}: ${error instanceof Error ? error.message : String(error)}`;
|
|
386
|
-
}
|
|
387
|
-
refreshSoon($);
|
|
388
|
-
return runText(run, {
|
|
389
|
-
failed,
|
|
390
|
-
ok: `${body.action === 'approve' ? 'approved' : 'asked for a revision of'} #${id}`,
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
const at = await endpoint($);
|
|
394
|
-
let result: ApiAnswer;
|
|
395
|
-
try {
|
|
396
|
-
result = await callApi($, 'POST', `/inbox/${id}/resolve`, body, at);
|
|
397
|
-
} catch (error) {
|
|
398
|
-
if (body.action === 'done') throw error;
|
|
399
|
-
return `not ${body.action}d: ${error instanceof Error ? error.message : String(error)}; ${MAYBE_POSTED}`;
|
|
400
|
-
}
|
|
401
|
-
refreshSoon($);
|
|
402
|
-
return answerText(body.action, id, result);
|
|
403
|
-
} catch (error) {
|
|
404
|
-
return error instanceof Error ? error.message : String(error);
|
|
405
|
-
}
|
|
376
|
+
async function openPane($: EngineInterface): Promise<void> {
|
|
377
|
+
await $.ui.open({ focus: true, id: PANE, title: TITLE });
|
|
406
378
|
}
|
|
407
379
|
|
|
408
380
|
// `/pause`, `/unpause`, `/focus-project` and the pane's buttons: an override that ends on its own, as
|
|
@@ -419,69 +391,68 @@ async function override($: EngineInterface, command: 'focus' | 'pause' | 'unpaus
|
|
|
419
391
|
}
|
|
420
392
|
}
|
|
421
393
|
|
|
422
|
-
//
|
|
423
|
-
|
|
394
|
+
// Pushes to the phone through PushNotification, tells the daemon what came of it whatever it was
|
|
395
|
+
// (`conduct notify` shows it), and says once in the console when pushes stop reaching the phone
|
|
396
|
+
// (ATK-255): Remote Control off, pushes off in the config, or the call failing.
|
|
397
|
+
async function push($: EngineInterface, message: string): Promise<void> {
|
|
398
|
+
let outcome: PushOutcome;
|
|
424
399
|
try {
|
|
425
|
-
|
|
400
|
+
outcome = pushOutcome(await $.tool.call({ message, status: 'proactive', tool: 'PushNotification' }));
|
|
426
401
|
} catch (error) {
|
|
427
|
-
|
|
402
|
+
outcome = { detail: error instanceof Error ? error.message : String(error), state: 'failed' };
|
|
403
|
+
}
|
|
404
|
+
const warning = pushWarning(outcome);
|
|
405
|
+
if (outcome.state === 'ok') warnedInactive = false;
|
|
406
|
+
if (warning !== null && !warnedInactive) {
|
|
407
|
+
warnedInactive = true;
|
|
408
|
+
$.ui.toast(warning);
|
|
428
409
|
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// The conductor's own CLI, beside the mod in its package; a mod checkout without a build has none.
|
|
432
|
-
function cliOf($: EngineInterface): string {
|
|
433
|
-
return `${$.plugin.root}/../build/cli.js`;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// Through `conduct attach` (ATTACH_RUN_MS), by id: a name is unique only within its project.
|
|
437
|
-
// Without the CLI, the API, which answers within 30 s only when the worker needs no settle time
|
|
438
|
-
// and resumes quickly. The daemon refuses a second attach of a worker while one runs.
|
|
439
|
-
async function attach($: EngineInterface, id: number, name: string): Promise<string> {
|
|
440
410
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
return runText(run, { failed: `could not attach to ${name}`, ok: `attached to ${name}` });
|
|
447
|
-
}
|
|
448
|
-
const result = await callApi($, 'POST', `/workers/${id}/attach`);
|
|
449
|
-
refreshSoon($);
|
|
450
|
-
return result.status === 200 ? `attached to ${name}` : `could not attach to ${name}: ${errorOf(result)}`;
|
|
411
|
+
await callApi($, 'POST', '/notify/report', {
|
|
412
|
+
channel: 'remote-control',
|
|
413
|
+
result: outcome.detail,
|
|
414
|
+
state: reportedState(outcome),
|
|
415
|
+
});
|
|
451
416
|
} catch (error) {
|
|
452
|
-
|
|
417
|
+
$.ui.log(`conductor: notify report failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
418
|
+
to: 'debug',
|
|
419
|
+
});
|
|
453
420
|
}
|
|
454
421
|
}
|
|
455
422
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
): Promise<void> {
|
|
461
|
-
const copied = await $.ui.copy({ surface, text });
|
|
462
|
-
$.ui.toast(copied.isCopied ? `copied: ${text}` : `not copied: ${text}`);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
// The Windows terminal's [open]: the default browser, run once (ATK-494). A Link there is an OSC 8
|
|
466
|
-
// span, which a ctrl+click in a pane opened twice: the pane and Windows Terminal each opened it.
|
|
467
|
-
// rundll32 hands the URL over whole (explorer.exe would split it at a comma) and exits 0.
|
|
468
|
-
async function openLink($: EngineInterface, href: string): Promise<void> {
|
|
423
|
+
// Reads /fleet, /inbox and /notify into the state the drawings read.
|
|
424
|
+
async function refresh($: EngineInterface): Promise<ConductorSnapshot> {
|
|
425
|
+
const readAt = await $.clock.now();
|
|
426
|
+
let next: ConductorSnapshot;
|
|
469
427
|
try {
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
428
|
+
const at = await endpoint($);
|
|
429
|
+
const [fleet, inbox, notify] = await Promise.all([
|
|
430
|
+
callApi($, 'GET', '/fleet', undefined, at),
|
|
431
|
+
callApi($, 'GET', '/inbox', undefined, at),
|
|
432
|
+
callApi($, 'GET', '/notify', undefined, at),
|
|
433
|
+
]);
|
|
434
|
+
next = snapshotOf(fleet, inbox, readAt, notify);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
next = {
|
|
437
|
+
error: error instanceof Error ? error.message : String(error),
|
|
438
|
+
inbox: [],
|
|
439
|
+
notify: null,
|
|
440
|
+
readAt,
|
|
441
|
+
workers: [],
|
|
442
|
+
};
|
|
474
443
|
}
|
|
444
|
+
await update($, snapshot, () => next);
|
|
445
|
+
return next;
|
|
475
446
|
}
|
|
476
447
|
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
448
|
+
// Many events land together (a note raises an item and a wait); one read covers them.
|
|
449
|
+
function refreshSoon($: EngineInterface): void {
|
|
450
|
+
if (isRefreshing) return;
|
|
451
|
+
isRefreshing = true;
|
|
452
|
+
$.clock.after(DEBOUNCE_MS, () => {
|
|
453
|
+
isRefreshing = false;
|
|
454
|
+
void refresh($);
|
|
455
|
+
});
|
|
485
456
|
}
|
|
486
457
|
|
|
487
458
|
async function registerCommands($: EngineInterface): Promise<void> {
|
|
@@ -526,6 +497,36 @@ async function registerCommands($: EngineInterface): Promise<void> {
|
|
|
526
497
|
});
|
|
527
498
|
}
|
|
528
499
|
|
|
500
|
+
async function reportCache($: EngineInterface, body: Record<string, unknown>): Promise<void> {
|
|
501
|
+
try {
|
|
502
|
+
await callApi($, 'POST', '/console/cache', body);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
$.ui.log(`conductor: console cache report failed: ${error instanceof Error ? error.message : String(error)}`, {
|
|
505
|
+
to: 'debug',
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// `/notify <profile> [--until <when>]` and the pane's picker: switch the notification profile.
|
|
511
|
+
async function switchProfile($: EngineInterface, name: string, until: null | string = null): Promise<string> {
|
|
512
|
+
try {
|
|
513
|
+
const result = await callApi($, 'POST', '/notify/profile', until === null ? { name } : { name, until });
|
|
514
|
+
refreshSoon($);
|
|
515
|
+
return profileText(name, result, await $.clock.now());
|
|
516
|
+
} catch (error) {
|
|
517
|
+
return error instanceof Error ? error.message : String(error);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// The rule behind an item's newest action (`conduct why <item>`), in one line.
|
|
522
|
+
async function why($: EngineInterface, item: string): Promise<string> {
|
|
523
|
+
try {
|
|
524
|
+
return whyText(item, await callApi($, 'GET', `/why?item=${encodeURIComponent(item)}`));
|
|
525
|
+
} catch (error) {
|
|
526
|
+
return error instanceof Error ? error.message : String(error);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
529
530
|
export const register: Register = (on) => {
|
|
530
531
|
on('session.start', async ($, e, next) => {
|
|
531
532
|
const started = await next(e);
|