@detergent-software/conductor 2.1.1-dev.1 → 2.2.0-dev.1
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 +10 -0
- package/README.md +1 -1
- package/mod/hooks/model.ts +162 -14
- package/mod/hooks/register.tsx +41 -35
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Releases are tagged `conductor-v<version>` and staged on npm by `atk-conductor-release` (`pipelines/release-conductor.yml`), which writes each section below. `scripts/check-changelog.mjs` fails when a tag has no section here.
|
|
4
4
|
|
|
5
|
+
## [2.2.0-dev.1](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v2.1.1-dev.1&targetVersion=GTconductor-v2.2.0-dev.1) (2026-10-16)
|
|
6
|
+
|
|
7
|
+
### Features
|
|
8
|
+
|
|
9
|
+
* **conductor:** fleet pane rows in columns with waits in words ([3d79d73](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/3d79d7398e45db3e2a05ea569c594b95dbcfcdc7))
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **conductor:** keep the fleet row's wait at narrow widths ([9ccf333](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/commit/9ccf33371347ef43bdd0e6d08f43b6f193eb3200))
|
|
14
|
+
|
|
5
15
|
## [2.1.1-dev.1](https://emergentcode.visualstudio.com/Emergent%20Software/_git/Emergent.AgenticToolkit/branchCompare?baseVersion=GTconductor-v2.1.0&targetVersion=GTconductor-v2.1.1-dev.1) (2026-10-16)
|
|
6
16
|
|
|
7
17
|
### Refactoring
|
package/README.md
CHANGED
|
@@ -729,7 +729,7 @@ It runs `claude -n conductor --permission-mode auto -- /conductor-console` with
|
|
|
729
729
|
- **Other sessions** get nothing from it: no status line, and no calls to the daemon.
|
|
730
730
|
- **The console**, the session started with `CONDUCTOR_CONSOLE=1` (`conduct console` starts it; see Console), reads `GET /fleet`, `GET /inbox` and `GET /notify` every 30 s and gets:
|
|
731
731
|
- **the band** above the prompt while the inbox has items: one hotkey per item (a plan approves after a second press, `y`, since a digit typed into an empty prompt presses a band button; an npm OTP copies its `npm stage approve` line; the rest open the pane), and `o` for the pane;
|
|
732
|
-
- **the pane** (`/conductor`, `o`, or opened at the start, docked as a sidebar on a fullscreen terminal of 144 columns or more): the notification profile (a picker; buttons on mobile) and Remote Control's state, the inbox with each item's actions (approve, revise, copy a command, open a link, done; on a Windows terminal, open is a button that opens a web link in the default browser once, since a ctrl+click on a link in a pane opened it twice, ATK-494), the workers by project (each project's rule has [pause] and [focus]
|
|
732
|
+
- **the pane** (`/conductor`, `o`, or opened at the start, docked as a sidebar on a fullscreen terminal of 144 columns or more): the notification profile (a picker; buttons on mobile) and Remote Control's state, the inbox with each item's actions (approve, revise, copy a command, open a link, done; on a Windows terminal, open is a button that opens a web link in the default browser once, since a ctrl+click on a link in a pane opened it twice, ATK-494), the workers by project (each project's rule has [pause] and [focus]), and today's spend. The header counts the workers running and parked and the inbox in words. Each worker is one line in columns that line up across projects, cut to the pane's width so it never wraps: a glyph, the item, its phase, its PR, what it waits on in words (`PR #557 to merge (agentic-toolkit-registry)`, `npm 8.9.0-dev.1 on dev (atk)`, `login (ado)`) or else its activity, and the age. As the pane narrows, the age goes first, then the wait, then the PR and the phase (ATK-521). The glyphs are one cell wide: `●` in a turn, `◌` up between turns, `!` stuck on a prompt, `○` parked, `×` failed or gone, `·` starting. A row expands to attach and why, its tokens today, its whole PR ref and its raw wait keys;
|
|
733
733
|
- **commands answered in code**, never by the model: `/conductor`, `/inbox`, `/approve <item | #id>`, `/revise <item | #id> <text>`, `/attach <item | worker>`, `/notify [<profile> [--until <when>]]`, `/pause [<project>] [--until <when>]`, `/unpause [<project>]`, `/focus-project <project> [--until <when>]` (puts its items first, until midnight without `--until`; not `/focus`, which is Claude Code's own);
|
|
734
734
|
- **a check that `/conductor-console` ran**: a console started without it (by hand, as `claude -n conductor`) shows `console without its skill: run /conductor-console` in the band, and each prompt, Jason's or an escalation's, carries a line telling the model to load the skill before it answers. Running the skill (`/conductor-console`, or the model's Skill tool) clears both, until a `/clear` or a resume starts a conversation without it;
|
|
735
735
|
- **a warm prompt cache while it idles** (ATK-386). The console's cache lasts an hour after its last use, and a wake after that writes the whole context again. So 55 minutes after the console's last turn, the mod sends one ping (`$.model.fork`, the console's own last request plus a one-word ask, no tools, no turn in the transcript). The API serves the ping from the cache for a tenth of a write's price, and the hour starts over. It pings again every 55 minutes until a turn comes, and each turn starts the timer over. At an idle point it runs `/clear` instead when the context is 150k or more, or after 12 pings in a row (the 13th idle point, about 12 hours after the last turn), and the band says `fresh console (cleared while idle: …)` until the next prompt loads the skill. A ping that read less than half the context from the cache (the cache was gone anyway, as under a 5-minute TTL) stops the pings until the next turn. Each ping and clear goes to `POST /console/cache`, which logs a `console.cache` event with the context size and what the ping cost. A fork isn't in the transcript, so that event is the only place its cost shows;
|
package/mod/hooks/model.ts
CHANGED
|
@@ -121,12 +121,12 @@ export type Found<T> = { error: string; found?: undefined } | { error?: undefine
|
|
|
121
121
|
|
|
122
122
|
const BAND_LIMIT = 5;
|
|
123
123
|
|
|
124
|
-
// `
|
|
124
|
+
// `3 running · 2 parked · inbox 2` beside the pane's title, or undefined when no daemon answers.
|
|
125
125
|
export function statusText(snapshot: ConductorSnapshot | null): string | undefined {
|
|
126
126
|
if (snapshot === null || snapshot.error !== null) return undefined;
|
|
127
127
|
const running = snapshot.workers.filter((w) => w.status === 'running').length;
|
|
128
128
|
const parked = snapshot.workers.filter((w) => w.status === 'parked').length;
|
|
129
|
-
return
|
|
129
|
+
return `${running} running · ${parked} parked · inbox ${snapshot.inbox.length}`;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// The band's buttons: a plan approves (behind a confirm), an npm OTP copies its command, the rest
|
|
@@ -159,20 +159,172 @@ export function projectGroups(workers: readonly ConductorWorker[]): ProjectGroup
|
|
|
159
159
|
}));
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
// What a worker is up to, by its status and its session's activity (manager.ts's activityOf):
|
|
163
|
+
// in a turn, up between turns, stuck on a prompt (`waiting on permission`), parked on a wait,
|
|
164
|
+
// failed or gone (exited too), or not up yet (launching, starting).
|
|
165
|
+
export type WorkerState = 'failed' | 'idle' | 'parked' | 'prompt' | 'starting' | 'working';
|
|
166
|
+
|
|
167
|
+
export function workerState(w: Pick<ConductorWorker, 'activity' | 'status'>): WorkerState {
|
|
168
|
+
if (w.status === 'failed' || w.status === 'stopped-unexpectedly' || w.status === 'exited') return 'failed';
|
|
169
|
+
if (w.status === 'parked') return 'parked';
|
|
170
|
+
if (w.status !== 'running') return 'starting';
|
|
171
|
+
if (w.activity === 'working') return 'working';
|
|
172
|
+
if (w.activity.startsWith('idle')) return 'idle';
|
|
173
|
+
if (w.activity.startsWith('waiting on ')) return 'prompt';
|
|
174
|
+
if (w.activity === 'stopped' || w.activity === 'no session') return 'failed';
|
|
175
|
+
return 'starting';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// A row's glyph and its theme color. None has an emoji form, which Windows Terminal draws two cells
|
|
179
|
+
// wide (⏸ ✖ ✉ did, ATK-521); the shapes are East Asian ambiguous width, one cell outside CJK setups.
|
|
180
|
+
export const GLYPHS: Readonly<Record<WorkerState, { char: string; color: string }>> = {
|
|
181
|
+
failed: { char: '×', color: 'error' },
|
|
182
|
+
idle: { char: '◌', color: 'suggestion' },
|
|
183
|
+
parked: { char: '○', color: 'subtle' },
|
|
184
|
+
prompt: { char: '!', color: 'warning' },
|
|
185
|
+
starting: { char: '·', color: 'subtle' },
|
|
186
|
+
working: { char: '●', color: 'success' },
|
|
187
|
+
};
|
|
169
188
|
|
|
170
189
|
// A worker's PR as its row shows it: `#1850`; the expanded row shows the whole ref.
|
|
171
190
|
export function prLabel(pr: null | string): string {
|
|
172
191
|
return pr === null ? '' : `#${pr.slice(pr.lastIndexOf('#') + 1)}`;
|
|
173
192
|
}
|
|
174
193
|
|
|
175
|
-
|
|
194
|
+
const PR_EVENTS: Readonly<Record<string, string>> = { merged: 'to merge' };
|
|
195
|
+
|
|
196
|
+
// A wait's condition key (conditions.ts's formatCondition) in words, the telling part first so a
|
|
197
|
+
// truncated row keeps it: `PR #557 to merge (agentic-toolkit-registry)`, `npm 1.2.3 on dev (atk)`.
|
|
198
|
+
// A kind this doesn't know, or a key it can't read, stays as it is.
|
|
199
|
+
export function waitLabel(condition: string, now: number): string {
|
|
200
|
+
const colon = condition.indexOf(':');
|
|
201
|
+
if (colon === -1) return condition;
|
|
202
|
+
const rest = condition.slice(colon + 1);
|
|
203
|
+
const read = (pattern: RegExp): string[] | null => pattern.exec(rest)?.slice(1) ?? null;
|
|
204
|
+
const short = (sha: string): string => sha.slice(0, 7);
|
|
205
|
+
const last = (path: string): string => path.slice(path.lastIndexOf('/') + 1);
|
|
206
|
+
switch (condition.slice(0, colon)) {
|
|
207
|
+
case 'approval':
|
|
208
|
+
return 'plan approval';
|
|
209
|
+
case 'health': {
|
|
210
|
+
const [env = '', field = '', value = ''] = read(/^([^:]+):(commit|version)=(.+)$/) ?? [];
|
|
211
|
+
if (env) return `${env} health at ${field === 'commit' ? short(value) : value}`;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
case 'item': {
|
|
215
|
+
const [item = '', field = '', value = ''] = read(/^[^:]+:([^:]+):(state|label|note)=(.*)$/) ?? [];
|
|
216
|
+
if (item) return `${item} ${field} ${value}`.trimEnd();
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case 'login': {
|
|
220
|
+
const [provider = ''] = read(/^([^:]+):/) ?? [];
|
|
221
|
+
if (provider) return `login (${provider})`;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
case 'merge': {
|
|
225
|
+
const [sha = '', only] = read(/^(?:ado|github):[^@]+@([0-9a-f]+)(?::for=(.+))?$/i) ?? [];
|
|
226
|
+
if (sha) return `runs of ${short(sha)}${only ? ` (${only})` : ''}`;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'npm': {
|
|
230
|
+
const [pkg = '', version = '', tag = ''] = read(/^(.+)@([^@:]+):([^:]+)$/) ?? [];
|
|
231
|
+
if (pkg) return `npm ${version} on ${tag} (${last(pkg)})`;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
case 'pr': {
|
|
235
|
+
const [host = '', repo = '', number = '', on = ''] = read(/^(ado|github):([^#]+)#(\d+):(\w+)$/) ?? [];
|
|
236
|
+
// An ADO PR is in the project's own repo; a GitHub one names its repo.
|
|
237
|
+
if (host) return `PR #${number} ${PR_EVENTS[on] ?? on}${host === 'github' ? ` (${last(repo)})` : ''}`;
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case 'run': {
|
|
241
|
+
const [pipeline = '', id = ''] = read(/^(?:ado|github):(.+)#(\d+):done$/) ?? [];
|
|
242
|
+
if (pipeline) return `run #${id} to finish (${last(pipeline).replace(/\.ya?ml$/, '')})`;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'time':
|
|
246
|
+
if (!Number.isNaN(Date.parse(rest))) return `timer, ${remaining(rest, now)} left`;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
return condition;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// What a worker waits on, in words (and how many more waits), else the session's activity.
|
|
253
|
+
export function workerDoing(w: ConductorWorker, now: number): string {
|
|
254
|
+
const [first, ...more] = w.waits;
|
|
255
|
+
if (first === undefined) return w.activity;
|
|
256
|
+
return `${waitLabel(first, now)}${more.length > 0 ? ` +${more.length}` : ''}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The fleet's column widths: the widest name, phase (at most PHASE_MAX) and PR across every project,
|
|
260
|
+
// so the rows line up under each other wherever they are.
|
|
261
|
+
export type FleetColumns = { key: number; phase: number; pr: number };
|
|
262
|
+
|
|
263
|
+
// One worker's row as the pane draws it, in segments, each with its leading space: together at
|
|
264
|
+
// most the pane's width, so it never wraps. `isWait` says `doing` is a wait (drawn full strength).
|
|
265
|
+
export type FleetRow = {
|
|
266
|
+
age: string;
|
|
267
|
+
color: string;
|
|
268
|
+
doing: string;
|
|
269
|
+
glyph: string;
|
|
270
|
+
isWait: boolean;
|
|
271
|
+
key: string;
|
|
272
|
+
meta: string;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const PHASE_MAX = 13;
|
|
276
|
+
const DOING_MIN = 12;
|
|
277
|
+
const DOING_SHORT = 6;
|
|
278
|
+
const AGE_WIDTH = 4;
|
|
279
|
+
|
|
280
|
+
export function fleetColumns(workers: readonly ConductorWorker[]): FleetColumns {
|
|
281
|
+
const widest = (texts: string[]): number => Math.max(0, ...texts.map((t) => t.length));
|
|
282
|
+
return {
|
|
283
|
+
key: widest(workers.map((w) => w.name)),
|
|
284
|
+
phase: Math.min(PHASE_MAX, widest(workers.map((w) => w.phase ?? ''))),
|
|
285
|
+
pr: widest(workers.map((w) => prLabel(w.pr))),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// The row in `width` cells: glyph, name, phase, PR, what it does or waits on, age. Where they don't
|
|
290
|
+
// all fit, the age goes first, then the doing text, then the PR, then the phase. Which columns show
|
|
291
|
+
// depends only on the fleet's widths, so every row keeps the same ones.
|
|
292
|
+
export function fleetRow(w: ConductorWorker, columns: FleetColumns, width: number, now: number): FleetRow {
|
|
293
|
+
const key = Math.max(1, Math.min(columns.key, width - 2));
|
|
294
|
+
const shown = new Map<'age' | 'doing' | 'phase' | 'pr', number>([
|
|
295
|
+
['phase', columns.phase],
|
|
296
|
+
['pr', columns.pr],
|
|
297
|
+
['doing', DOING_MIN],
|
|
298
|
+
['age', AGE_WIDTH],
|
|
299
|
+
]);
|
|
300
|
+
for (const [column, cells] of shown) if (cells === 0 && column !== 'doing') shown.delete(column);
|
|
301
|
+
const need = (): number => 2 + key + [...shown.values()].reduce((sum, cells) => sum + cells + 1, 0);
|
|
302
|
+
for (const column of ['age', 'doing', 'pr', 'phase'] as const) if (need() > width) shown.delete(column);
|
|
303
|
+
// The doing text comes back in what room is left, while that still shows a few words.
|
|
304
|
+
if (!shown.has('doing') && width - need() - 1 >= DOING_SHORT) shown.set('doing', width - need() - 1);
|
|
305
|
+
const cell = (column: 'age' | 'doing' | 'phase' | 'pr', text: string): string => {
|
|
306
|
+
const cells = shown.get(column);
|
|
307
|
+
if (cells === undefined) return '';
|
|
308
|
+
return ` ${column === 'doing' ? fit(text, cells + Math.max(0, width - need())) : fit(text, cells)}`;
|
|
309
|
+
};
|
|
310
|
+
const { char, color } = GLYPHS[workerState(w)];
|
|
311
|
+
return {
|
|
312
|
+
age: shown.has('age') ? ` ${age(w.updatedAt, now).padStart(AGE_WIDTH)}` : '',
|
|
313
|
+
color,
|
|
314
|
+
doing: cell('doing', workerDoing(w, now)),
|
|
315
|
+
glyph: char,
|
|
316
|
+
isWait: w.waits.length > 0,
|
|
317
|
+
key: ` ${fit(w.name, key)}`,
|
|
318
|
+
meta: `${cell('phase', w.phase ?? '')}${cell('pr', prLabel(w.pr))}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// `text` in exactly `cells` cells: padded, or cut with an ellipsis.
|
|
323
|
+
function fit(text: string, cells: number): string {
|
|
324
|
+
if (text.length <= cells) return text.padEnd(cells);
|
|
325
|
+
return cells <= 1 ? '…'.slice(0, cells) : `${text.slice(0, cells - 1).trimEnd()}…`.padEnd(cells);
|
|
326
|
+
}
|
|
327
|
+
|
|
176
328
|
// A link the terminal's [open] hands to the OS opener (ATK-494): one a Link would make a link of,
|
|
177
329
|
// an https: URL or http://localhost. Anything else, such as a path, stays a Link, drawn as text.
|
|
178
330
|
export function isWebLink(href: string): boolean {
|
|
@@ -184,10 +336,6 @@ export function isWebLink(href: string): boolean {
|
|
|
184
336
|
}
|
|
185
337
|
}
|
|
186
338
|
|
|
187
|
-
export function workerDoing(w: ConductorWorker): string {
|
|
188
|
-
return w.waits[0] ?? w.activity;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
339
|
// The open plan of `ref` (as `conduct approve` finds it): an item key, or an inbox id as #12.
|
|
192
340
|
export function findPlan(inbox: readonly ConductorInboxItem[], ref: string): Found<ConductorInboxItem> {
|
|
193
341
|
const byId = /^#(\d+)$/.exec(ref.trim());
|
package/mod/hooks/register.tsx
CHANGED
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
errorOf,
|
|
15
15
|
findPlan,
|
|
16
16
|
findWorker,
|
|
17
|
-
|
|
17
|
+
fleetColumns,
|
|
18
|
+
fleetRow,
|
|
18
19
|
homeOf,
|
|
19
20
|
idleAction,
|
|
20
21
|
inboxText,
|
|
@@ -29,7 +30,6 @@ import {
|
|
|
29
30
|
parseLine,
|
|
30
31
|
type PingOutcome,
|
|
31
32
|
pingOutcome,
|
|
32
|
-
prLabel,
|
|
33
33
|
profileText,
|
|
34
34
|
projectGroups,
|
|
35
35
|
type PushOutcome,
|
|
@@ -45,7 +45,6 @@ import {
|
|
|
45
45
|
WARM_AFTER_MS,
|
|
46
46
|
WARM_PROMPT,
|
|
47
47
|
whyText,
|
|
48
|
-
workerDoing,
|
|
49
48
|
} from './model.ts';
|
|
50
49
|
|
|
51
50
|
const PANE = 'conductor';
|
|
@@ -744,10 +743,13 @@ export const register: Register = (on) => {
|
|
|
744
743
|
</Box>
|
|
745
744
|
);
|
|
746
745
|
}
|
|
746
|
+
const columns = fleetColumns(snap.workers);
|
|
747
747
|
return (
|
|
748
748
|
<Box flexDirection='column'>
|
|
749
|
-
<Text bold>
|
|
750
|
-
{TITLE}
|
|
749
|
+
<Text bold wrap='truncate'>
|
|
750
|
+
{TITLE}
|
|
751
|
+
{' '}
|
|
752
|
+
<Text dimColor>{statusText(snap)}</Text>
|
|
751
753
|
</Text>
|
|
752
754
|
{snap.notify !== null && (
|
|
753
755
|
<Box columnGap={2} flexWrap='wrap'>
|
|
@@ -846,36 +848,40 @@ export const register: Register = (on) => {
|
|
|
846
848
|
onPress={async () => $.ui.toast(await override($, 'focus', group.project))}
|
|
847
849
|
/>
|
|
848
850
|
</Box>
|
|
849
|
-
{group.rows.map((w) =>
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
{w.
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
851
|
+
{group.rows.map((w) => {
|
|
852
|
+
// One line in the fleet's columns, cut to the pane's width so it never wraps.
|
|
853
|
+
const row = fleetRow(w, columns, e.props.bodyColumns, now);
|
|
854
|
+
return (
|
|
855
|
+
<Box flexDirection='column' key={`worker-${w.id}`}>
|
|
856
|
+
<Button
|
|
857
|
+
key={`row-${w.id}`}
|
|
858
|
+
onPress={() => update($, expanded, (name) => (name === w.name ? null : w.name))}
|
|
859
|
+
plain
|
|
860
|
+
>
|
|
861
|
+
<Text color={row.color}>{row.glyph}</Text>
|
|
862
|
+
<Text bold>{row.key}</Text>
|
|
863
|
+
<Text dimColor>{row.meta}</Text>
|
|
864
|
+
<Text dimColor={!row.isWait}>{row.doing}</Text>
|
|
865
|
+
<Text dimColor>{row.age}</Text>
|
|
866
|
+
</Button>
|
|
867
|
+
{open === w.name && (
|
|
868
|
+
<Box columnGap={2} flexWrap='wrap' marginLeft={2}>
|
|
869
|
+
<Button
|
|
870
|
+
key={`attach-${w.id}`}
|
|
871
|
+
label='attach'
|
|
872
|
+
onPress={async () => $.ui.toast(await attach($, w.id, w.name))}
|
|
873
|
+
/>
|
|
874
|
+
<Button key={`why-${w.id}`} label='why' onPress={async () => $.ui.toast(await why($, w.item))} />
|
|
875
|
+
<Text dimColor>
|
|
876
|
+
{w.status} · {tokens(w.spendToday)} tok today
|
|
877
|
+
{w.pr === null ? '' : ` · PR ${w.pr}`}
|
|
878
|
+
{w.waits.length > 0 ? ` · waits on ${w.waits.join(', ')}` : ''}
|
|
879
|
+
</Text>
|
|
880
|
+
</Box>
|
|
881
|
+
)}
|
|
882
|
+
</Box>
|
|
883
|
+
);
|
|
884
|
+
})}
|
|
879
885
|
</Box>
|
|
880
886
|
))}
|
|
881
887
|
<Text dimColor wrap='truncate'>
|
package/package.json
CHANGED