@detergent-software/conductor 1.0.0-dev.6 → 1.0.0-dev.7
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 +16 -0
- package/README.md +53 -7
- package/build/cli.js +65 -0
- package/build/cli.js.map +1 -1
- package/build/config.js +68 -1
- package/build/config.js.map +1 -1
- package/build/credential.js +52 -0
- package/build/credential.js.map +1 -0
- package/build/daemon.js +14 -0
- package/build/daemon.js.map +1 -1
- package/build/inbox.js +2 -8
- package/build/inbox.js.map +1 -1
- package/build/notify.js +398 -0
- package/build/notify.js.map +1 -0
- package/build/pushover.js +67 -0
- package/build/pushover.js.map +1 -0
- package/build/server.js +47 -0
- package/build/server.js.map +1 -1
- package/build/store.js +84 -0
- package/build/store.js.map +1 -1
- package/build/watchers/linear-key.js +6 -45
- package/build/watchers/linear-key.js.map +1 -1
- package/mod/README.md +1 -1
- package/mod/hooks/model.ts +100 -6
- package/mod/hooks/register.tsx +86 -8
- package/mod/types/index.d.ts +12 -0
- package/package.json +1 -1
package/mod/hooks/model.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the status line, band and pane show, and what each event asks the mod to do. register.tsx does
|
|
3
3
|
// the engine's part (only its top-level functions may take `$`); vitest tests this file in CI
|
|
4
4
|
// (apps/conductor/mod/tests/model.spec.ts).
|
|
5
|
-
import type { ConductorInboxItem, ConductorSnapshot, ConductorWorker } from '../types';
|
|
5
|
+
import type { ConductorInboxItem, ConductorNotify, ConductorSnapshot, ConductorWorker } from '../types';
|
|
6
6
|
|
|
7
7
|
// A daemon API answer: its status and parsed JSON body.
|
|
8
8
|
export type ApiAnswer = { body: unknown; status: number };
|
|
@@ -19,6 +19,17 @@ export type Reaction =
|
|
|
19
19
|
| { kind: 'refresh'; toast: string | null }
|
|
20
20
|
| { kind: 'send'; text: string; to: string };
|
|
21
21
|
|
|
22
|
+
// What a Remote Control push came to: reached the phone (ok); not, as Remote Control is off
|
|
23
|
+
// (inactive) or pushes are off in Claude Code's config (disabled); or not needed (skipped: Jason
|
|
24
|
+
// is at the terminal, or the answer says nothing about the phone).
|
|
25
|
+
export type PushOutcome = { detail: string; state: 'disabled' | 'inactive' | 'ok' | 'skipped' };
|
|
26
|
+
|
|
27
|
+
// Said once in the console when pushes stop reaching the phone (ATK-255), by why.
|
|
28
|
+
export const PUSH_WARNINGS = {
|
|
29
|
+
disabled: "conductor: mobile pushes are off in Claude Code's config, so pushes reach this terminal only. Turn them on, or /notify away for Pushover.",
|
|
30
|
+
inactive: 'conductor: Remote Control is off, so pushes reach this terminal only. Connect Remote Control, or /notify away for Pushover.',
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
22
33
|
// A band entry and the act its press does.
|
|
23
34
|
export type BandEntry = { act: 'approve' | 'copy' | 'pane'; hotkey: string; id: number; label: string; text: string };
|
|
24
35
|
|
|
@@ -121,6 +132,15 @@ export function age(iso: string, now: number): string {
|
|
|
121
132
|
return `${Math.floor(minutes / (24 * 60))}d`;
|
|
122
133
|
}
|
|
123
134
|
|
|
135
|
+
// The time left until `iso`: "12m", "3h", "2d"; "<1m" when it is (almost) up.
|
|
136
|
+
export function remaining(iso: string, now: number): string {
|
|
137
|
+
const minutes = Math.floor((Date.parse(iso) - now) / 60_000);
|
|
138
|
+
if (!(minutes >= 1)) return '<1m';
|
|
139
|
+
if (minutes < 60) return `${minutes}m`;
|
|
140
|
+
if (minutes < 24 * 60) return `${Math.floor(minutes / 60)}h`;
|
|
141
|
+
return `${Math.floor(minutes / (24 * 60))}d`;
|
|
142
|
+
}
|
|
143
|
+
|
|
124
144
|
// 950, 42k, 6.1M.
|
|
125
145
|
export function tokens(n: number): string {
|
|
126
146
|
if (n < 1000) return String(n);
|
|
@@ -169,6 +189,11 @@ export function reaction(line: StreamLine): Reaction {
|
|
|
169
189
|
const ask = text('text');
|
|
170
190
|
return { kind: 'refresh', toast: ask === null ? null : `conductor: ${line.subject ? `${line.subject} · ` : ''}${ask}` };
|
|
171
191
|
}
|
|
192
|
+
case 'notify.failed':
|
|
193
|
+
case 'notify.routed':
|
|
194
|
+
case 'notify.sent':
|
|
195
|
+
// Nothing the mod draws: only notify.profile and notify.channel change the pane.
|
|
196
|
+
return { kind: 'ignore' };
|
|
172
197
|
case 'notify.requested': {
|
|
173
198
|
const message = text('message');
|
|
174
199
|
return line.data.channel === 'remote-control' && message !== null ? { kind: 'push', message } : { kind: 'ignore' };
|
|
@@ -264,13 +289,82 @@ export function endpointOf(portText: string, tokenText: string): { port: number;
|
|
|
264
289
|
return Number.isInteger(port) && port > 0 && /^[0-9a-f]{64}$/.test(token) ? { port, token } : null;
|
|
265
290
|
}
|
|
266
291
|
|
|
267
|
-
// GET /fleet and GET /
|
|
268
|
-
|
|
292
|
+
// GET /fleet, GET /inbox and GET /notify as one snapshot; `error` set when the daemon didn't answer
|
|
293
|
+
// the first two. A daemon older than the notifier answers /notify 404, which leaves `notify` null.
|
|
294
|
+
export function snapshotOf(fleet: ApiAnswer, inbox: ApiAnswer, readAt: number, notify?: ApiAnswer): ConductorSnapshot {
|
|
269
295
|
const workers = fleet.status === 200 ? parseFleet(fleet.body) : null;
|
|
270
|
-
if (workers === null) return { error: errorOf(fleet), inbox: [], readAt, workers: [] };
|
|
296
|
+
if (workers === null) return { error: errorOf(fleet), inbox: [], notify: null, readAt, workers: [] };
|
|
271
297
|
const items = inbox.status === 200 ? parseInbox(inbox.body) : null;
|
|
272
|
-
if (items === null) return { error: errorOf(inbox), inbox: [], readAt, workers: [] };
|
|
273
|
-
return { error: null, inbox: items, readAt, workers };
|
|
298
|
+
if (items === null) return { error: errorOf(inbox), inbox: [], notify: null, readAt, workers: [] };
|
|
299
|
+
return { error: null, inbox: items, notify: notify?.status === 200 ? parseNotify(notify.body) : null, readAt, workers };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// GET /notify's body, kept to what the mod draws; null when it isn't one.
|
|
303
|
+
export function parseNotify(body: unknown): ConductorNotify | null {
|
|
304
|
+
if (!isRecord(body) || typeof body.profile !== 'string') return null;
|
|
305
|
+
const channels = isRecord(body.channels) ? body.channels : {};
|
|
306
|
+
const remote = isRecord(channels['remote-control']) ? channels['remote-control'] : {};
|
|
307
|
+
return {
|
|
308
|
+
held: typeof body.held === 'number' ? body.held : 0,
|
|
309
|
+
profile: body.profile,
|
|
310
|
+
profiles: strings(body.profiles),
|
|
311
|
+
profileUntil: typeof body.profileUntil === 'string' ? body.profileUntil : null,
|
|
312
|
+
quietNow: body.quietNow === true,
|
|
313
|
+
remoteControl: typeof remote.state === 'string' ? remote.state : 'unknown',
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// What `$.tool.call` of PushNotification answered: `{ result: { pushSent, disabledReason }, text }`.
|
|
318
|
+
// Only a push that reached the phone is ok; Remote Control off (no_transport, or the text saying
|
|
319
|
+
// so) is inactive; pushes turned off in the config are disabled; the rest say nothing of the phone.
|
|
320
|
+
export function pushOutcome(answer: unknown): PushOutcome {
|
|
321
|
+
const call = isRecord(answer) ? answer : {};
|
|
322
|
+
const result = isRecord(call.result) ? call.result : {};
|
|
323
|
+
const detail = typeof call.text === 'string' && call.text.trim() !== '' ? call.text.trim() : typeof result.message === 'string' ? result.message : '';
|
|
324
|
+
if (result.pushSent === true) return { detail, state: 'ok' };
|
|
325
|
+
if (result.disabledReason === 'config_off') return { detail: "Mobile push not sent (off in Claude Code's config)", state: 'disabled' };
|
|
326
|
+
if (result.disabledReason === 'no_transport' || /remote control inactive/i.test(detail)) {
|
|
327
|
+
return { detail: detail || 'Mobile push not sent (Remote Control inactive)', state: 'inactive' };
|
|
328
|
+
}
|
|
329
|
+
return { detail, state: 'skipped' };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// `/notify <profile> [--until <when>]`: --until is a local time (the next one), a date (local
|
|
333
|
+
// midnight) or a date and time, as `conduct notify profile` reads it.
|
|
334
|
+
export function notifyArgs(args: string, now: number): { error: string } | { name: string; until: null | string } {
|
|
335
|
+
const typed = /^(\S+)(?:\s+--until\s+(\S+))?$/.exec(args.trim());
|
|
336
|
+
if (!typed?.[1]) return { error: 'usage: /notify [<profile> [--until <when>]]' };
|
|
337
|
+
const when = typed[2];
|
|
338
|
+
if (when === undefined) return { name: typed[1], until: null };
|
|
339
|
+
const clock = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(when);
|
|
340
|
+
let at: Date;
|
|
341
|
+
if (clock) {
|
|
342
|
+
at = new Date(now);
|
|
343
|
+
at.setHours(Number(clock[1]), Number(clock[2]), 0, 0);
|
|
344
|
+
if (at.getTime() <= now) at.setDate(at.getDate() + 1);
|
|
345
|
+
} else {
|
|
346
|
+
at = new Date(/^\d{4}-\d{2}-\d{2}$/.test(when) ? `${when}T00:00` : when);
|
|
347
|
+
}
|
|
348
|
+
if (Number.isNaN(at.getTime())) return { error: `--until "${when}" is not a time: a local time such as 17:00, or a date and time such as 2026-10-12T09:00` };
|
|
349
|
+
return { name: typed[1], until: at.toISOString() };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// `/notify` with no profile: the profile, quiet hours, Remote Control.
|
|
353
|
+
export function notifyText(notify: ConductorNotify | null, now: number): string {
|
|
354
|
+
if (notify === null) return 'the running conductor has no notifier: restart it on this build (conduct upgrade)';
|
|
355
|
+
const until = notify.profileUntil === null ? '' : ` for ${remaining(notify.profileUntil, now)} more`;
|
|
356
|
+
return [
|
|
357
|
+
`notify profile: ${notify.profile}${until} (profiles: ${notify.profiles.join(', ')}; /notify <profile> switches)`,
|
|
358
|
+
`Remote Control: ${notify.remoteControl}${notify.quietNow ? ` · quiet hours now, ${notify.held} held for the summary` : ''}`,
|
|
359
|
+
].join('\n');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// POST /notify/profile's answer as the line to show.
|
|
363
|
+
export function profileText(name: string, answer: ApiAnswer, now: number): string {
|
|
364
|
+
if (answer.status !== 200) return `notify profile not switched: ${errorOf(answer)}`;
|
|
365
|
+
const notify = parseNotify(answer.body);
|
|
366
|
+
const until = notify?.profileUntil ? ` for ${remaining(notify.profileUntil, now)}` : '';
|
|
367
|
+
return `notify profile: ${notify?.profile ?? name}${until}`;
|
|
274
368
|
}
|
|
275
369
|
|
|
276
370
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
package/mod/hooks/register.tsx
CHANGED
|
@@ -15,8 +15,13 @@ import {
|
|
|
15
15
|
glyph,
|
|
16
16
|
homeOf,
|
|
17
17
|
inboxText,
|
|
18
|
+
notifyArgs,
|
|
19
|
+
notifyText,
|
|
18
20
|
parseLine,
|
|
21
|
+
profileText,
|
|
19
22
|
projectGroups,
|
|
23
|
+
PUSH_WARNINGS,
|
|
24
|
+
pushOutcome,
|
|
20
25
|
reaction,
|
|
21
26
|
snapshotOf,
|
|
22
27
|
spendToday,
|
|
@@ -47,6 +52,8 @@ let isConsole = false;
|
|
|
47
52
|
let isRefreshing = false;
|
|
48
53
|
// The newest event id the stream showed, so a restarted `conduct events` picks up after it.
|
|
49
54
|
let lastEventId: null | number = null;
|
|
55
|
+
// Set once the console has said that pushes no longer reach the phone; a push that does clears it.
|
|
56
|
+
let warnedInactive = false;
|
|
50
57
|
|
|
51
58
|
type Endpoint = { port: number; token: string };
|
|
52
59
|
|
|
@@ -85,16 +92,20 @@ async function callApi($: EngineInterface, method: 'GET' | 'POST', path: string,
|
|
|
85
92
|
return { body: parsed, status: answer.status };
|
|
86
93
|
}
|
|
87
94
|
|
|
88
|
-
// Reads /fleet and /
|
|
95
|
+
// Reads /fleet, /inbox and /notify into the state the drawings read, and the status line.
|
|
89
96
|
async function refresh($: EngineInterface): Promise<ConductorSnapshot> {
|
|
90
97
|
const readAt = await $.clock.now();
|
|
91
98
|
let next: ConductorSnapshot;
|
|
92
99
|
try {
|
|
93
100
|
const at = await endpoint($);
|
|
94
|
-
const [fleet, inbox] = await Promise.all([
|
|
95
|
-
|
|
101
|
+
const [fleet, inbox, notify] = await Promise.all([
|
|
102
|
+
callApi($, 'GET', '/fleet', undefined, at),
|
|
103
|
+
callApi($, 'GET', '/inbox', undefined, at),
|
|
104
|
+
callApi($, 'GET', '/notify', undefined, at),
|
|
105
|
+
]);
|
|
106
|
+
next = snapshotOf(fleet, inbox, readAt, notify);
|
|
96
107
|
} catch (error) {
|
|
97
|
-
next = { error: error instanceof Error ? error.message : String(error), inbox: [], readAt, workers: [] };
|
|
108
|
+
next = { error: error instanceof Error ? error.message : String(error), inbox: [], notify: null, readAt, workers: [] };
|
|
98
109
|
}
|
|
99
110
|
await update($, snapshot, () => next);
|
|
100
111
|
$.ui.status(statusText(next));
|
|
@@ -121,7 +132,7 @@ async function onLine($: EngineInterface, text: string): Promise<void> {
|
|
|
121
132
|
switch (act.kind) {
|
|
122
133
|
case 'disconnected': {
|
|
123
134
|
const readAt = await $.clock.now();
|
|
124
|
-
await update($, snapshot, () => ({ error: NOT_RUNNING, inbox: [], readAt, workers: [] }));
|
|
135
|
+
await update($, snapshot, () => ({ error: NOT_RUNNING, inbox: [], notify: null, readAt, workers: [] }));
|
|
125
136
|
$.ui.status(undefined);
|
|
126
137
|
return;
|
|
127
138
|
}
|
|
@@ -132,7 +143,7 @@ async function onLine($: EngineInterface, text: string): Promise<void> {
|
|
|
132
143
|
case 'ignore':
|
|
133
144
|
return;
|
|
134
145
|
case 'push':
|
|
135
|
-
await $.tool.call({ message: act.message, status: 'proactive', tool: 'PushNotification' });
|
|
146
|
+
await reportPush($, await $.tool.call({ message: act.message, status: 'proactive', tool: 'PushNotification' }));
|
|
136
147
|
return;
|
|
137
148
|
case 'refresh':
|
|
138
149
|
if (act.toast !== null) $.ui.toast(act.toast);
|
|
@@ -147,6 +158,36 @@ async function onLine($: EngineInterface, text: string): Promise<void> {
|
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
160
|
|
|
161
|
+
// Tells the daemon whether the push reached the phone, and says once in the console when Remote
|
|
162
|
+
// Control is off (ATK-255): the pushes then reach this terminal only.
|
|
163
|
+
async function reportPush($: EngineInterface, answer: unknown): Promise<void> {
|
|
164
|
+
const outcome = pushOutcome(answer);
|
|
165
|
+
if (outcome.state === 'skipped') return;
|
|
166
|
+
if (outcome.state === 'ok') {
|
|
167
|
+
warnedInactive = false;
|
|
168
|
+
} else if (!warnedInactive) {
|
|
169
|
+
warnedInactive = true;
|
|
170
|
+
$.ui.toast(PUSH_WARNINGS[outcome.state]);
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const state = outcome.state === 'ok' ? 'ok' : 'inactive';
|
|
174
|
+
await callApi($, 'POST', '/notify/report', { channel: 'remote-control', result: outcome.detail, state });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
$.ui.log(`conductor: notify report failed: ${error instanceof Error ? error.message : String(error)}`, { to: 'debug' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// `/notify <profile> [--until <when>]` and the pane's picker: switch the notification profile.
|
|
181
|
+
async function switchProfile($: EngineInterface, name: string, until: null | string = null): Promise<string> {
|
|
182
|
+
try {
|
|
183
|
+
const result = await callApi($, 'POST', '/notify/profile', until === null ? { name } : { name, until });
|
|
184
|
+
refreshSoon($);
|
|
185
|
+
return profileText(name, result, await $.clock.now());
|
|
186
|
+
} catch (error) {
|
|
187
|
+
return error instanceof Error ? error.message : String(error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
150
191
|
// `conduct events --follow` from this package's own build, so the mod and the CLI are one version
|
|
151
192
|
// and no .cmd shim is in the way. It reconnects by itself; a child that ends is started again after
|
|
152
193
|
// the last event it showed, so nothing in between is lost. One that keeps exiting at once (no
|
|
@@ -222,6 +263,7 @@ async function registerCommands($: EngineInterface): Promise<void> {
|
|
|
222
263
|
await $.command.register({ argumentHint: '<item | #id>', description: "Approve an item's plan (posts approve as you)", name: 'approve' });
|
|
223
264
|
await $.command.register({ argumentHint: '<item | #id> <what to change>', description: 'Ask for a revised plan (posts revise: … as you)', name: 'revise' });
|
|
224
265
|
await $.command.register({ argumentHint: '<item | worker>', description: 'Wake a parked worker and open it in a tab', name: 'attach' });
|
|
266
|
+
await $.command.register({ argumentHint: '[<profile> [--until <when>]]', description: 'Show or switch the notification profile (default, away, focus …)', name: 'notify' });
|
|
225
267
|
}
|
|
226
268
|
|
|
227
269
|
export const register: Register = (on) => {
|
|
@@ -279,6 +321,15 @@ export const register: Register = (on) => {
|
|
|
279
321
|
return { text: worker.found ? await attach($, worker.found.id, worker.found.name) : worker.error };
|
|
280
322
|
});
|
|
281
323
|
|
|
324
|
+
on('command.run', { command: 'notify' }, async ($, e) => {
|
|
325
|
+
if (e.args.trim() !== '') {
|
|
326
|
+
const typed = notifyArgs(e.args, await $.clock.now());
|
|
327
|
+
return { text: 'error' in typed ? typed.error : await switchProfile($, typed.name, typed.until) };
|
|
328
|
+
}
|
|
329
|
+
const snap = await refresh($);
|
|
330
|
+
return { text: snap.error ?? notifyText(snap.notify, await $.clock.now()) };
|
|
331
|
+
});
|
|
332
|
+
|
|
282
333
|
// The band above the prompt, in the console, while the inbox has items. A bare digit typed into
|
|
283
334
|
// an empty prompt presses a band Button, so a plan's approve takes a second press (`y`).
|
|
284
335
|
on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
|
|
@@ -331,9 +382,10 @@ export const register: Register = (on) => {
|
|
|
331
382
|
);
|
|
332
383
|
});
|
|
333
384
|
|
|
334
|
-
// The pane: the
|
|
385
|
+
// The pane: the notification profile, the inbox, then the fleet by project, then today's spend.
|
|
335
386
|
on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
|
|
336
|
-
const
|
|
387
|
+
const ui = $.ui.resolve(e);
|
|
388
|
+
const { Box, Button, Link, Text } = ui;
|
|
337
389
|
const snap = await read($, snapshot);
|
|
338
390
|
const open = await read($, expanded);
|
|
339
391
|
const now = await $.clock.now();
|
|
@@ -352,6 +404,32 @@ export const register: Register = (on) => {
|
|
|
352
404
|
<Text bold>
|
|
353
405
|
{TITLE} <Text dimColor>{statusText(snap)}</Text>
|
|
354
406
|
</Text>
|
|
407
|
+
{snap.notify !== null && (
|
|
408
|
+
<Box columnGap={2} flexWrap="wrap">
|
|
409
|
+
{e.surface !== 'mobile' && 'Select' in ui ? (
|
|
410
|
+
<ui.Select
|
|
411
|
+
key="notify-profile"
|
|
412
|
+
label="notify "
|
|
413
|
+
onSelect={async (name) => $.ui.toast(await switchProfile($, name))}
|
|
414
|
+
options={snap.notify.profiles.map((p) => ({ value: p }))}
|
|
415
|
+
value={snap.notify.profile}
|
|
416
|
+
/>
|
|
417
|
+
) : (
|
|
418
|
+
snap.notify.profiles.map((p) => (
|
|
419
|
+
<Button
|
|
420
|
+
key={`profile-${p}`}
|
|
421
|
+
label={p === snap.notify?.profile ? `notify: ${p}` : p}
|
|
422
|
+
onPress={async () => $.ui.toast(await switchProfile($, p))}
|
|
423
|
+
variant={p === snap.notify?.profile ? 'primary' : undefined}
|
|
424
|
+
/>
|
|
425
|
+
))
|
|
426
|
+
)}
|
|
427
|
+
<Text dimColor>
|
|
428
|
+
Remote Control: {snap.notify.remoteControl}
|
|
429
|
+
{snap.notify.quietNow ? ` · quiet hours, ${snap.notify.held} held` : ''}
|
|
430
|
+
</Text>
|
|
431
|
+
</Box>
|
|
432
|
+
)}
|
|
355
433
|
<Text dimColor wrap="truncate">
|
|
356
434
|
{rule(`Inbox (${snap.inbox.length})`)}
|
|
357
435
|
</Text>
|
package/mod/types/index.d.ts
CHANGED
|
@@ -24,10 +24,22 @@ export type ConductorInboxItem = {
|
|
|
24
24
|
text: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
// GET /notify as the mod draws it: the profile, the others to switch to, Remote Control's state.
|
|
28
|
+
export type ConductorNotify = {
|
|
29
|
+
held: number;
|
|
30
|
+
profile: string;
|
|
31
|
+
profiles: string[];
|
|
32
|
+
profileUntil: string | null;
|
|
33
|
+
quietNow: boolean;
|
|
34
|
+
remoteControl: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
27
37
|
// What the mod last read from the daemon; `error` set (and the lists empty) when none answered.
|
|
38
|
+
// `notify` is null from a daemon older than the notifier.
|
|
28
39
|
export type ConductorSnapshot = {
|
|
29
40
|
error: string | null;
|
|
30
41
|
inbox: ConductorInboxItem[];
|
|
42
|
+
notify: ConductorNotify | null;
|
|
31
43
|
readAt: number;
|
|
32
44
|
workers: ConductorWorker[];
|
|
33
45
|
};
|
package/package.json
CHANGED