@oisin-ee/oisinpi 0.5.0 → 0.5.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 +4 -2
- package/node_modules/@oisin-ee/pi-guardrails/package.json +1 -1
- package/node_modules/@oisin-ee/pi-prewalk/package.json +1 -1
- package/node_modules/@oisin-ee/pi-todo/package.json +1 -1
- package/node_modules/@oisin-ee/pi-wrangle/README.md +2 -2
- package/node_modules/@oisin-ee/pi-wrangle/package.json +1 -1
- package/node_modules/@oisin-ee/pi-wrangle/pi/index.ts +87 -42
- package/node_modules/@oisin-ee/pi-wrangle/pi/work-panel.ts +35 -34
- package/node_modules/@oisin-ee/pi-wrangle/pi/work.ts +34 -22
- package/node_modules/@oisin-ee/pi-wrangle/plugins/wrangle/skills/wrangle/SKILL.md +1 -1
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @oisin-ee/oisinpi
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`0170caa`](https://github.com/oisin-ee/oisinpi/commit/0170caa9c59c4e3d71409627b14925cefcc68679) Thanks [@momo-momokaya[bot]](https://github.com/momo-momokaya%5Bbot%5D)! - Bundle pi-wrangle 0.12.2. Show the current task and named agents and jobs above the editor. Alt+W and `/work` expand the same widget in place, without a popup or editor replacement.
|
|
8
|
+
- Updated dependencies []:
|
|
9
|
+
- @oisin-ee/pi-guardrails@0.5.1
|
|
10
|
+
- @oisin-ee/pi-prewalk@0.5.1
|
|
11
|
+
- @oisin-ee/pi-todo@0.5.1
|
|
12
|
+
|
|
3
13
|
## 0.5.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Oisin's [pi](https://pi.dev) packages in one install, at versions tested togethe
|
|
|
6
6
|
| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
7
7
|
| [`@oisin-ee/pi-prewalk`](https://www.npmjs.com/package/@oisin-ee/pi-prewalk) | A frontier model plans and makes the first edit; a cheaper model finishes. |
|
|
8
8
|
| [`@oisin-ee/pi-todo`](https://www.npmjs.com/package/@oisin-ee/pi-todo) | The `todo` tool, which also opens the prewalk gate. |
|
|
9
|
-
| [`@oisin-ee/pi-wrangle`](https://www.npmjs.com/package/@oisin-ee/pi-wrangle) | The
|
|
9
|
+
| [`@oisin-ee/pi-wrangle`](https://www.npmjs.com/package/@oisin-ee/pi-wrangle) | The inline `WORK` widget, `/work`, `/wrangle`, and the agent/job inbox. |
|
|
10
10
|
| [`@oisin-ee/pi-guardrails`](https://www.npmjs.com/package/@oisin-ee/pi-guardrails) | Git safety, the wait gate, and the agent-typing gate on every tool call. |
|
|
11
11
|
|
|
12
12
|
```bash
|
|
@@ -17,6 +17,8 @@ The collection bundles the four packages in its tarball and adds no code. No mem
|
|
|
17
17
|
installs by itself. pi-wrangle needs the `wrangle` binary at the same version
|
|
18
18
|
([oisin-ee/wrangle](https://github.com/oisin-ee/wrangle)).
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The compact widget shows the current task and named agents and jobs above the editor.
|
|
21
|
+
Alt+W or `/work` expands that widget in place, without a popup. Typing stays in the editor.
|
|
22
|
+
Todos follow the active Pi branch. Agents and jobs follow the Herdr owner pane.
|
|
21
23
|
Use Wrangle `run`, `read`, and `stop` for background work. The collection does not require `pi-background-tasks`.
|
|
22
24
|
Standalone pi-todo keeps its own widget when Wrangle does not claim the shared display.
|
|
@@ -466,8 +466,8 @@ The caller is the harness's `HERDR_PANE_ID`. Pi registers the server from its ex
|
|
|
466
466
|
|
|
467
467
|
The extension adds host status before each prompt, registers the tools, and draws:
|
|
468
468
|
|
|
469
|
-
- **The `WORK`
|
|
470
|
-
-
|
|
469
|
+
- **The `WORK` widget** above the editor shows the current task and named agents and jobs. Agent/job data refreshes every ten seconds. Compatible pi-todo snapshots update immediately. Empty providers hide the widget. Unavailable providers show an error.
|
|
470
|
+
- **Alt+W or `/work`** expands or collapses that widget in place, without a popup or editor replacement. Alt+J/K moves selection, Alt+] changes views, and Alt+I shows inline details. Typing stays in the editor. Alt+S stops jobs, Alt+C cancels queued work, and Alt+M sends agent messages. Stop and cancel require confirmation. Completed job history starts collapsed.
|
|
471
471
|
- **`/wrangle`**, the control panel, with four tabs: Status (hosts with their retry state, then your rows; Enter on a host reads it again now), Hosts (reconnect, rename, enable, disable, add), Profiles (Enter edits `prompt.md`), and Inbox. The widget steps aside while the panel is open.
|
|
472
472
|
- **The inbox reader** and a renderer that shows each record's kind, sender, and text, folded past twelve lines.
|
|
473
473
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oisin-ee/pi-wrangle",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "Pi extension for wrangle: the board, the /wrangle panel, the inbox reader, and the wrangle tools. Needs the wrangle binary of the same version.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Box, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Box, Text, Key } from "@earendil-works/pi-tui";
|
|
4
4
|
import { type Board, clean, parseBoard } from "./board.ts";
|
|
5
5
|
import { WorkTodo } from "./work-todo.ts";
|
|
6
6
|
import { workStrip, workText, type WorkState } from "./work.ts";
|
|
7
7
|
import { type Details, InboxReader, MESSAGE_TYPE } from "./inbox.ts";
|
|
8
8
|
import { openPanel } from "./panel.ts";
|
|
9
9
|
import { loadJobHistory, type JobHistory } from "./work-history.ts";
|
|
10
|
-
import { WorkPanel,
|
|
10
|
+
import { WorkPanel, WorkDetail, runWorkAction, type WorkAction, type WorkCursor, type WorkData, type WorkTab } from "./work-panel.ts";
|
|
11
11
|
|
|
12
12
|
const REFRESH_MS = 10_000;
|
|
13
13
|
|
|
@@ -64,7 +64,9 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
64
64
|
let workOpen = false;
|
|
65
65
|
let history: JobHistory = { jobs: [] };
|
|
66
66
|
let workPanel: WorkPanel | undefined;
|
|
67
|
-
let
|
|
67
|
+
let workDetail: WorkDetail | undefined;
|
|
68
|
+
let detail: { title: string; body: string } | undefined;
|
|
69
|
+
let cursor: WorkCursor = { tab: "all", selected: undefined, history: false };
|
|
68
70
|
let workAbort: AbortController | undefined;
|
|
69
71
|
let context: ExtensionContext | undefined;
|
|
70
72
|
const todos = new WorkTodo(pi.events, () => { if (context) draw(context); }, () => {
|
|
@@ -77,44 +79,52 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
77
79
|
|
|
78
80
|
function workData(): WorkData { return { ...state(), history }; }
|
|
79
81
|
|
|
82
|
+
function collapseWork(ctx: ExtensionContext): void {
|
|
83
|
+
workAbort?.abort();
|
|
84
|
+
workAbort = undefined;
|
|
85
|
+
workOpen = false;
|
|
86
|
+
workPanel = undefined;
|
|
87
|
+
workDetail = undefined;
|
|
88
|
+
detail = undefined;
|
|
89
|
+
history = { jobs: [] };
|
|
90
|
+
draw(ctx);
|
|
91
|
+
}
|
|
92
|
+
|
|
80
93
|
async function openWork(ctx: ExtensionContext, tab: WorkTab): Promise<void> {
|
|
81
94
|
if (ctx.mode !== "tui") { ctx.ui.notify(workText(state()), "info"); return; }
|
|
82
|
-
if (
|
|
95
|
+
if (panelOpen) return;
|
|
83
96
|
workOpen = true;
|
|
97
|
+
detail = undefined;
|
|
98
|
+
workDetail = undefined;
|
|
99
|
+
cursor = { tab, selected: undefined, history: false };
|
|
100
|
+
workPanel = undefined;
|
|
101
|
+
const current = generation;
|
|
102
|
+
draw(ctx);
|
|
103
|
+
// Draw immediately; remote history must not delay expansion or typing.
|
|
104
|
+
if (refresh) await refresh;
|
|
105
|
+
if (current === generation && workOpen) await update(ctx);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function act(ctx: ExtensionContext, action: WorkAction | undefined): Promise<void> {
|
|
109
|
+
if (!action) { collapseWork(ctx); return; }
|
|
110
|
+
if (workAbort) return;
|
|
84
111
|
const interaction = new AbortController();
|
|
85
112
|
workAbort = interaction;
|
|
86
113
|
const current = generation;
|
|
87
|
-
let cursor: WorkCursor = { tab, selected: undefined, history: false };
|
|
88
114
|
try {
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
115
|
+
const body = await runWorkAction(action, ctx, exec, interaction.signal);
|
|
116
|
+
if (current !== generation || interaction.signal.aborted) return;
|
|
117
|
+
if (body !== undefined) { detail = { title: action.entry.label, body }; workDetail = undefined; }
|
|
118
|
+
draw(ctx);
|
|
92
119
|
await update(ctx);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}, { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
|
|
99
|
-
workPanel = undefined;
|
|
100
|
-
closeWork = undefined;
|
|
101
|
-
if (!action || current !== generation) break;
|
|
102
|
-
cursor = action.cursor;
|
|
103
|
-
try {
|
|
104
|
-
const detail = await runWorkAction(action, ctx, exec, interaction.signal);
|
|
105
|
-
if (detail !== undefined && current === generation) await showWorkDetail(ctx, action.entry.label, detail, interaction.signal);
|
|
106
|
-
} catch (error) {
|
|
107
|
-
if (current === generation) await showWorkDetail(ctx, "Work action unavailable", String(error), interaction.signal);
|
|
108
|
-
}
|
|
109
|
-
if (current === generation) await update(ctx);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (current === generation && !interaction.signal.aborted) {
|
|
122
|
+
detail = { title: "Work action unavailable", body: String(error) };
|
|
123
|
+
workDetail = undefined;
|
|
124
|
+
draw(ctx);
|
|
110
125
|
}
|
|
111
126
|
} finally {
|
|
112
|
-
interaction.abort();
|
|
113
127
|
if (workAbort === interaction) workAbort = undefined;
|
|
114
|
-
workPanel = undefined;
|
|
115
|
-
closeWork = undefined;
|
|
116
|
-
workOpen = false;
|
|
117
|
-
history = { jobs: [] };
|
|
118
128
|
}
|
|
119
129
|
}
|
|
120
130
|
|
|
@@ -122,10 +132,19 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
122
132
|
if (ctx.mode !== "tui") return;
|
|
123
133
|
workPanel?.update(workData());
|
|
124
134
|
const data = state();
|
|
125
|
-
ctx.ui.setWidget("wrangle", panelOpen || (!data.board.rows.length && !data.todo?.tasks.length && !data.error) ? undefined : (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
135
|
+
ctx.ui.setWidget("wrangle", panelOpen || (!workOpen && !data.board.rows.length && !data.todo?.tasks.length && !data.error) ? undefined : (tui, theme) => {
|
|
136
|
+
if (workOpen) {
|
|
137
|
+
if (detail) {
|
|
138
|
+
workDetail ??= new WorkDetail(detail.title, detail.body, theme, () => tui.terminal.rows, () => tui.requestRender());
|
|
139
|
+
workDetail.setTheme(theme);
|
|
140
|
+
return workDetail;
|
|
141
|
+
}
|
|
142
|
+
workDetail = undefined;
|
|
143
|
+
workPanel = new WorkPanel(workData(), cursor, theme, action => { void act(ctx, action); }, () => tui.requestRender(), () => tui.terminal.rows);
|
|
144
|
+
return workPanel;
|
|
145
|
+
}
|
|
146
|
+
return { render: (width: number) => workStrip(data, width, theme), invalidate() {} };
|
|
147
|
+
}, { placement: "aboveEditor" });
|
|
129
148
|
}
|
|
130
149
|
|
|
131
150
|
function update(ctx: ExtensionContext): Promise<void> {
|
|
@@ -143,7 +162,7 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
143
162
|
if (workOpen) {
|
|
144
163
|
const loaded = await loadJobHistory(exec, owner);
|
|
145
164
|
if (current !== generation) return;
|
|
146
|
-
history = loaded;
|
|
165
|
+
if (workOpen) history = loaded;
|
|
147
166
|
}
|
|
148
167
|
draw(ctx);
|
|
149
168
|
} catch (error) {
|
|
@@ -162,7 +181,12 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
162
181
|
if (timer) clearInterval(timer);
|
|
163
182
|
generation++;
|
|
164
183
|
workAbort?.abort();
|
|
165
|
-
|
|
184
|
+
workAbort = undefined;
|
|
185
|
+
workOpen = false;
|
|
186
|
+
workPanel = undefined;
|
|
187
|
+
workDetail = undefined;
|
|
188
|
+
detail = undefined;
|
|
189
|
+
history = { jobs: [] };
|
|
166
190
|
context = ctx;
|
|
167
191
|
board = { hosts: [], rows: [] };
|
|
168
192
|
failure = owner ? undefined : "Wrangle needs a Herdr owner pane";
|
|
@@ -182,7 +206,11 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
182
206
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
183
207
|
generation++;
|
|
184
208
|
workAbort?.abort();
|
|
185
|
-
|
|
209
|
+
workAbort = undefined;
|
|
210
|
+
workOpen = false;
|
|
211
|
+
workPanel = undefined;
|
|
212
|
+
workDetail = undefined;
|
|
213
|
+
detail = undefined;
|
|
186
214
|
if (timer) clearInterval(timer);
|
|
187
215
|
timer = undefined;
|
|
188
216
|
inbox.stop();
|
|
@@ -203,12 +231,29 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
203
231
|
return;
|
|
204
232
|
}
|
|
205
233
|
});
|
|
234
|
+
async function toggleWork(ctx: ExtensionContext): Promise<void> {
|
|
235
|
+
if (workOpen) collapseWork(ctx);
|
|
236
|
+
else await openWork(ctx, "all");
|
|
237
|
+
}
|
|
206
238
|
pi.registerCommand("work", {
|
|
207
|
-
description: "
|
|
208
|
-
handler: async (_args, ctx) => {
|
|
209
|
-
await openWork(ctx, "all");
|
|
210
|
-
},
|
|
239
|
+
description: "Expand or collapse work above the editor (Alt+W)",
|
|
240
|
+
handler: async (_args, ctx) => { await toggleWork(ctx); },
|
|
211
241
|
});
|
|
242
|
+
pi.registerShortcut(Key.alt("w"), { description: "Expand or collapse work above the editor", handler: toggleWork });
|
|
243
|
+
for (const [key, input, description] of [
|
|
244
|
+
["j", "\u001b[B", "Next work item / scroll down"], ["k", "\u001b[A", "Previous work item / scroll up"],
|
|
245
|
+
["]", "\t", "Next work view"], ["i", "\r", "Inspect work / return to list"],
|
|
246
|
+
["s", "s", "Stop selected job"], ["c", "c", "Cancel selected queued work"],
|
|
247
|
+
["m", "m", "Message selected agent"], ["r", "r", "Refresh work"],
|
|
248
|
+
] as const) {
|
|
249
|
+
pi.registerShortcut(Key.alt(key), { description, handler: async (_ctx) => {
|
|
250
|
+
if (!workOpen || panelOpen || workAbort || !context) return;
|
|
251
|
+
if (detail) {
|
|
252
|
+
if (key === "i") { detail = undefined; draw(context); }
|
|
253
|
+
else if (key === "j" || key === "k") workDetail?.handleInput(input);
|
|
254
|
+
} else workPanel?.handleInput(input);
|
|
255
|
+
} });
|
|
256
|
+
}
|
|
212
257
|
pi.registerCommand("wrangle", {
|
|
213
258
|
description: "Hosts, your children and jobs, profiles, and messages",
|
|
214
259
|
handler: async (_args, ctx) => {
|
|
@@ -216,7 +261,7 @@ export default function wrangle(pi: ExtensionAPI) {
|
|
|
216
261
|
ctx.ui.notify(await exec(["board"]), "info");
|
|
217
262
|
return;
|
|
218
263
|
}
|
|
219
|
-
if (
|
|
264
|
+
if (panelOpen || workAbort) return;
|
|
220
265
|
panelOpen = true;
|
|
221
266
|
draw(ctx);
|
|
222
267
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext,
|
|
1
|
+
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Key, matchesKey, SelectList, Text, ScrollView, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { clean } from "./board.ts";
|
|
4
4
|
import type { WorkState } from "./work.ts";
|
|
@@ -19,7 +19,7 @@ export interface WorkAction { action: "inspect" | "stop" | "cancel" | "send" | "
|
|
|
19
19
|
export function workEntries(data: WorkData, tab: WorkTab, history: boolean): WorkEntry[] {
|
|
20
20
|
const todos: WorkEntry[] = (data.todo?.tasks ?? []).map(task => ({ value: `todo:${task.content}`, label: `${task.status === "in_progress" ? "→" : task.status === "completed" ? "✓" : task.status === "abandoned" ? "−" : "·"} ${clean(task.content)}`, description: task.status, kind: "todo", detail: task.content }));
|
|
21
21
|
const canonicalHost = (label: string) => data.history.hosts?.find(host => host.label === label)?.id ?? label;
|
|
22
|
-
const rows: WorkEntry[] = data.board.rows.map(row => ({ value: `${row.kind}:${canonicalHost(row.host)}:${row.id}`, id: row.id, host: canonicalHost(row.host), ...(row.pane ? { pane: row.pane } : {}), kind: row.group === "waiting" ? "queued" : row.kind === "job" ? "job" : "agent", label: `${row.group === "attention" || row.group === "to_land" ? "!" : "●"} ${clean(row.name)}
|
|
22
|
+
const rows: WorkEntry[] = data.board.rows.map(row => ({ value: `${row.kind}:${canonicalHost(row.host)}:${row.id}`, id: row.id, host: canonicalHost(row.host), ...(row.pane ? { pane: row.pane } : {}), kind: row.group === "waiting" ? "queued" : row.kind === "job" ? "job" : "agent", label: `${row.group === "attention" || row.group === "to_land" ? "!" : "●"} ${clean(row.name)} · ${clean(row.state)}`, description: clean(row.host), running: row.group === "running" }));
|
|
23
23
|
const visible = tab === "todos" ? todos : tab === "agents" ? rows.filter(row => row.value.startsWith("agent:")) : tab === "jobs" ? rows.filter(row => row.value.startsWith("job:")) : [...rows.filter(row => row.label.startsWith("!")), ...todos.filter(row => row.description === "in_progress"), ...rows.filter(row => !row.label.startsWith("!")), ...todos.filter(row => row.description === "pending")];
|
|
24
24
|
if (tab === "all" || tab === "jobs") {
|
|
25
25
|
const known = new Set(rows.map(row => row.value));
|
|
@@ -39,21 +39,21 @@ export class WorkPanel {
|
|
|
39
39
|
private data: WorkData;
|
|
40
40
|
readonly cursor: WorkCursor;
|
|
41
41
|
private readonly theme: Theme;
|
|
42
|
-
private readonly keys: KeybindingsManager;
|
|
43
42
|
private readonly done: (action: WorkAction | undefined) => void;
|
|
44
43
|
private readonly renderRequest: () => void;
|
|
45
44
|
private readonly rows: () => number;
|
|
46
45
|
private height = 0;
|
|
47
46
|
|
|
48
|
-
constructor(data: WorkData, cursor: WorkCursor, theme: Theme,
|
|
49
|
-
this.data = data; this.cursor = cursor; this.theme = theme; this.
|
|
47
|
+
constructor(data: WorkData, cursor: WorkCursor, theme: Theme, done: (action: WorkAction | undefined) => void, request: () => void, rows: () => number) {
|
|
48
|
+
this.data = data; this.cursor = cursor; this.theme = theme; this.done = done; this.renderRequest = request; this.rows = rows;
|
|
50
49
|
this.rebuild();
|
|
51
50
|
}
|
|
52
51
|
update(data: WorkData): void { this.data = data; this.rebuild(); this.renderRequest(); }
|
|
52
|
+
private listHeight(): number { return Math.max(1, Math.min(12, Math.floor(this.rows() / 2) - 4)); }
|
|
53
53
|
private rebuild(preserveSelection = true): void {
|
|
54
54
|
if (preserveSelection) this.cursor.selected = this.list?.getSelectedItem()?.value ?? this.cursor.selected;
|
|
55
55
|
this.entries = workEntries(this.data, this.cursor.tab, this.cursor.history);
|
|
56
|
-
this.height =
|
|
56
|
+
this.height = this.listHeight();
|
|
57
57
|
this.list = new SelectList(this.entries, this.height, {
|
|
58
58
|
selectedPrefix: text => this.theme.fg("accent", text), selectedText: text => this.theme.fg("accent", text),
|
|
59
59
|
description: text => this.theme.fg("muted", text), scrollInfo: text => this.theme.fg("dim", text), noMatch: text => this.theme.fg("muted", text),
|
|
@@ -82,12 +82,13 @@ export class WorkPanel {
|
|
|
82
82
|
else if (input === "c") this.act("cancel");
|
|
83
83
|
else if (input === "m") this.act("send");
|
|
84
84
|
else this.list.handleInput(input);
|
|
85
|
+
this.renderRequest();
|
|
85
86
|
}
|
|
86
87
|
invalidate(): void { this.list.invalidate(); }
|
|
87
88
|
render(width: number): string[] {
|
|
88
|
-
if (this.height !==
|
|
89
|
+
if (this.height !== this.listHeight()) this.rebuild();
|
|
89
90
|
const tabbar = tabs.map(tab => tab === this.cursor.tab ? `[${tab.toUpperCase()}]` : tab).join(" · ");
|
|
90
|
-
return [this.theme.fg("accent", `WORK ${tabbar}`),
|
|
91
|
+
return [this.theme.fg("accent", `WORK ${tabbar}`), ...this.list.render(width), this.theme.fg("dim", "Alt+W collapse · Alt+J/K select · Alt+] view · Alt+I inspect"), this.theme.fg("dim", "Alt+S stop · Alt+C cancel · Alt+M message · Alt+R refresh")].map(line => truncateToWidth(line, width));
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
@@ -105,7 +106,7 @@ export async function inspectEntry(entry: WorkEntry, exec: WorkExec): Promise<st
|
|
|
105
106
|
return hint + safeOutput(output);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
/**
|
|
109
|
+
/** Explicit actions may open confirmation/input dialogs; the work widget stays inline. */
|
|
109
110
|
export async function runWorkAction(action: WorkAction, ctx: ExtensionContext, exec: WorkExec, signal: AbortSignal): Promise<string | undefined> {
|
|
110
111
|
if (signal.aborted) return undefined;
|
|
111
112
|
const entry = action.entry;
|
|
@@ -119,29 +120,29 @@ export async function runWorkAction(action: WorkAction, ctx: ExtensionContext, e
|
|
|
119
120
|
return undefined;
|
|
120
121
|
}
|
|
121
122
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
123
|
+
/** Details occupy the same above-editor widget, never an overlay. */
|
|
124
|
+
export class WorkDetail {
|
|
125
|
+
private readonly text: Text;
|
|
126
|
+
private readonly scroll: ScrollView;
|
|
127
|
+
private readonly title: string;
|
|
128
|
+
private theme: Theme;
|
|
129
|
+
private readonly rows: () => number;
|
|
130
|
+
private readonly request: () => void;
|
|
131
|
+
constructor(title: string, body: string, theme: Theme, rows: () => number, request: () => void) {
|
|
132
|
+
this.title = title; this.theme = theme; this.rows = rows; this.request = request;
|
|
133
|
+
this.text = new Text(safeOutput(body) || "No output yet.", 0, 0);
|
|
134
|
+
this.scroll = new ScrollView(this.text, { overscroll: "contain", scrollbar: "hidden" });
|
|
135
|
+
}
|
|
136
|
+
render(width: number): string[] {
|
|
137
|
+
const height = Math.max(1, Math.min(12, Math.floor(this.rows() / 2) - 2));
|
|
138
|
+
this.scroll.updateLayout(this.text.render(width).length, height, this.request);
|
|
139
|
+
return [this.theme.fg("accent", truncateToWidth(this.title, width)), ...this.scroll.render(width).slice(this.scroll.scrollTop, this.scroll.scrollTop + height), this.theme.fg("dim", truncateToWidth("Alt+J/K scroll · Alt+I back · Alt+W collapse", width))];
|
|
140
|
+
}
|
|
141
|
+
setTheme(theme: Theme): void { this.theme = theme; this.invalidate(); }
|
|
142
|
+
invalidate(): void { this.text.invalidate(); this.scroll.invalidate(); }
|
|
143
|
+
handleInput(input: string): void {
|
|
144
|
+
if (matchesKey(input, Key.up)) this.scroll.scrollBy(-1);
|
|
145
|
+
if (matchesKey(input, Key.down)) this.scroll.scrollBy(1);
|
|
146
|
+
this.request();
|
|
147
|
+
}
|
|
147
148
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { clean, type Board, type Paint } from "./board.ts";
|
|
2
|
+
import { age, clean, type Board, type Paint, type Row } from "./board.ts";
|
|
3
3
|
import type { TodoSnapshot } from "./work-todo.ts";
|
|
4
4
|
|
|
5
5
|
export interface WorkState {
|
|
@@ -8,33 +8,45 @@ export interface WorkState {
|
|
|
8
8
|
error: string | undefined;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
/**
|
|
11
|
+
/** A small, persistent readout: names and current work, not just counts. */
|
|
12
12
|
export function workStrip(state: WorkState, width: number, paint: Paint): string[] {
|
|
13
13
|
const { board, todo, error } = state;
|
|
14
14
|
if (!board.rows.length && !todo?.tasks.length && !error) return [];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
const lines = [paint.fg("dim", "WORK · Alt+W expand")];
|
|
16
|
+
if (error) lines.push(paint.fg("warning", `! unavailable · ${clean(error)}`));
|
|
17
|
+
const tasks = todo?.tasks ?? [];
|
|
18
|
+
const active = tasks.find(task => task.status === "in_progress");
|
|
19
|
+
const next = tasks.find(task => task.status === "pending");
|
|
20
|
+
const current = active ?? next;
|
|
21
|
+
if (current) lines.push(`${paint.fg("accent", active ? "→ Task" : "· Next")} ${clean(current.content)}`);
|
|
22
|
+
else if (tasks.length) lines.push(paint.fg("muted", `Tasks · ${tasks.filter(task => task.status === "completed").length}/${tasks.length} completed`));
|
|
23
|
+
const priority = (row: Row) => row.group === "attention" || row.group === "to_land" ? 0 : row.group === "running" ? 1 : 2;
|
|
24
|
+
for (const [label, members] of [
|
|
25
|
+
["Agents", board.rows.filter(row => row.kind !== "job")],
|
|
26
|
+
["Jobs", board.rows.filter(row => row.kind === "job")],
|
|
27
|
+
] as const) {
|
|
28
|
+
const rows = [...members].sort((a, b) => priority(a) - priority(b));
|
|
29
|
+
if (!rows.length) continue;
|
|
30
|
+
let line = `${label} `;
|
|
31
|
+
let shown = 0;
|
|
32
|
+
for (const row of rows) {
|
|
33
|
+
const warning = priority(row) === 0;
|
|
34
|
+
const entry = `${warning ? "! " : ""}${clean(row.name)} · ${clean(row.state)} · ${age(row.age_ms)}`;
|
|
35
|
+
const remaining = rows.length - shown - 1;
|
|
36
|
+
const suffix = remaining ? ` +${remaining} more` : "";
|
|
37
|
+
const join = shown ? " | " : "";
|
|
38
|
+
if (shown && visibleWidth(line + join + entry + suffix) > width) break;
|
|
39
|
+
const room = Math.max(0, width - visibleWidth(line + join + suffix));
|
|
40
|
+
line += join + paint.fg(warning ? "warning" : "muted", truncateToWidth(entry, room));
|
|
41
|
+
shown++;
|
|
42
|
+
}
|
|
43
|
+
if (shown < rows.length) line += paint.fg("dim", ` +${rows.length - shown} more`);
|
|
44
|
+
lines.push(line);
|
|
31
45
|
}
|
|
32
|
-
|
|
33
|
-
if (visibleWidth(line) + 8 <= width) line += " /work";
|
|
34
|
-
return [truncateToWidth(paint.fg(urgent ? "warning" : "accent", line), width)];
|
|
46
|
+
return lines.map(line => truncateToWidth(line, width));
|
|
35
47
|
}
|
|
36
48
|
|
|
37
|
-
/** A plain readout also works in modes where
|
|
49
|
+
/** A plain readout also works in modes where terminal UI is unavailable. */
|
|
38
50
|
export function workText(state: WorkState): string {
|
|
39
51
|
const lines = ["WORK · todos: Pi branch · agents/jobs: Herdr owner pane"];
|
|
40
52
|
if (state.error) lines.push(state.error);
|
|
@@ -18,7 +18,7 @@ wrangle read NAME_OR_PANE [--host H]
|
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
The board groups what you started by what you must do: TO LAND (a finished child with commits), ATTENTION (blocked, stalled, failed), RUNNING, and WAITING (queued for a host). Each agent row shows its harness and model; each job row its command.
|
|
21
|
-
Your prompt context carries host status and a `To land:` line. In Pi, the
|
|
21
|
+
Your prompt context carries host status and a `To land:` line. In Pi, the compact `WORK` widget shows the current task and named agents and jobs above the editor. Alt+W or `/work` expands it in place. Work inspection stays inline, not in a popup. Todos follow the Pi branch; agents and jobs follow the owner pane. `/wrangle` retains host, profile, and inbox administration.
|
|
22
22
|
|
|
23
23
|
## Delegate
|
|
24
24
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oisin-ee/oisinpi",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Oisin's pi packages in one install: prewalk, todo, wrangle, and guardrails.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"guardrails",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
},
|
|
19
19
|
"type": "module",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@oisin-ee/pi-guardrails": "0.5.
|
|
22
|
-
"@oisin-ee/pi-prewalk": "0.5.
|
|
23
|
-
"@oisin-ee/pi-todo": "0.5.
|
|
24
|
-
"@oisin-ee/pi-wrangle": "0.12.
|
|
21
|
+
"@oisin-ee/pi-guardrails": "0.5.1",
|
|
22
|
+
"@oisin-ee/pi-prewalk": "0.5.1",
|
|
23
|
+
"@oisin-ee/pi-todo": "0.5.1",
|
|
24
|
+
"@oisin-ee/pi-wrangle": "0.12.2"
|
|
25
25
|
},
|
|
26
26
|
"bundleDependencies": [
|
|
27
27
|
"@oisin-ee/pi-guardrails",
|