@rmrdeveloper/sideroom-pi 8.2.0 → 8.3.0
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/README.md +15 -2
- package/extensions/todo/index.ts +8 -0
- package/extensions/todo/ui.ts +233 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -70,8 +70,10 @@ wall of tool calls, it:
|
|
|
70
70
|
- **Works in the open.** A compact board sits above the editor:
|
|
71
71
|
`pending → in_progress → completed`. Exactly one step is active at a
|
|
72
72
|
time; the agent completes the current step and starts the next one in the
|
|
73
|
-
same move, so skipped steps become structurally hard. The
|
|
74
|
-
|
|
73
|
+
same move, so skipped steps become structurally hard. The widget shows at
|
|
74
|
+
most five rows, keeps the active step visible, and `F9` opens the full
|
|
75
|
+
read-only board. The board survives reload, tree navigation, and
|
|
76
|
+
compaction.
|
|
75
77
|
- **Shows its traces.** Below the board, the last edited files appear as
|
|
76
78
|
clickable `file://` links. `F8` opens the full session history; `R` clears
|
|
77
79
|
it. Only successful Pi `write`/`edit` calls are tracked — no guesses from
|
|
@@ -114,6 +116,10 @@ chat. It never creates a repository task file or a `/todos` command.
|
|
|
114
116
|
- `update` patches `{ id, status?, content? }` items and works everywhere,
|
|
115
117
|
including headless modes.
|
|
116
118
|
- Invariant: while anything is pending, exactly one item is `in_progress`.
|
|
119
|
+
- Widget: at most five rows — open items before resolved ones, the
|
|
120
|
+
`in_progress` row always visible — plus a `…+N more · F9: view all` hint.
|
|
121
|
+
`F9` opens the full board as a read-only overlay, so the widget stays small
|
|
122
|
+
without hiding work from you; the agent still gets every item in its prompt.
|
|
117
123
|
|
|
118
124
|
### Edited files — proof, not promises
|
|
119
125
|
|
|
@@ -213,6 +219,13 @@ Items have stable string ids, short content, and one of `pending`,
|
|
|
213
219
|
start the next one in the same `update` call. Snapshots live in the active
|
|
214
220
|
session branch and are rebuilt after navigation and compaction.
|
|
215
221
|
|
|
222
|
+
- Widget: max five rows. Open items win the cap over resolved ones, the
|
|
223
|
+
`in_progress` row is always shown, and a `…+N more · F9: view all` line
|
|
224
|
+
appears when rows are hidden.
|
|
225
|
+
- Extended view: `F9` toggles a read-only overlay. `↑`/`↓` and `PgUp`/`PgDn`
|
|
226
|
+
scroll; `Esc` or `F9` closes. It never mutates the board.
|
|
227
|
+
- The cap is display-only: every item still reaches the system prompt.
|
|
228
|
+
|
|
216
229
|
A forked session inherits the board visible at its fork point. Later board
|
|
217
230
|
updates are reconstructed from each session's active branch, so the fork and
|
|
218
231
|
the original session evolve independently.
|
package/extensions/todo/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
TODO_WIDGET_REFRESH_EVENT,
|
|
19
19
|
type TodoStore,
|
|
20
20
|
} from './session.ts';
|
|
21
|
+
import { createTodoBoardOverlayController } from './ui.ts';
|
|
21
22
|
|
|
22
23
|
export const TODO_DESCRIPTION =
|
|
23
24
|
'Maintain a live, user-visible work board. propose replaces the full board; update patches existing items by id.';
|
|
@@ -32,9 +33,12 @@ export const TODO_PROMPT_GUIDELINES = [
|
|
|
32
33
|
'propose is interactive-TUI-only; update also works in print, JSON, and RPC modes.',
|
|
33
34
|
];
|
|
34
35
|
|
|
36
|
+
const BOARD_SHORTCUT = 'f9';
|
|
37
|
+
|
|
35
38
|
// Pi loads extensions/*/index.ts through export default.
|
|
36
39
|
export default function registerTodo(pi: ExtensionAPI): void {
|
|
37
40
|
const store = createTodoStore();
|
|
41
|
+
const overlay = createTodoBoardOverlayController(() => store.items);
|
|
38
42
|
const restore: BoardLifecycle = (ctx) => {
|
|
39
43
|
restoreBoard(store, ctx);
|
|
40
44
|
};
|
|
@@ -50,6 +54,10 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
50
54
|
registerGuardEvents(pi, store, restore);
|
|
51
55
|
registerBoardContext(pi, store, restoreAndRefresh);
|
|
52
56
|
registerTodoTool(pi, store, notifyWidgetRefresh);
|
|
57
|
+
pi.registerShortcut(BOARD_SHORTCUT, {
|
|
58
|
+
description: 'Toggle the work board',
|
|
59
|
+
handler: (ctx) => overlay.toggle(ctx),
|
|
60
|
+
});
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
function registerTodoTool(
|
package/extensions/todo/ui.ts
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
|
-
import type { Theme } from '@earendil-works/pi-coding-agent';
|
|
2
|
-
import {
|
|
1
|
+
import type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import {
|
|
3
|
+
type Component,
|
|
4
|
+
Key,
|
|
5
|
+
matchesKey,
|
|
6
|
+
type TUI,
|
|
7
|
+
truncateToWidth,
|
|
8
|
+
} from '@earendil-works/pi-tui';
|
|
9
|
+
import {
|
|
10
|
+
formatBoardBlock,
|
|
11
|
+
TODO_STATUS,
|
|
12
|
+
type TodoItem,
|
|
13
|
+
type TodoStatus,
|
|
14
|
+
} from './model.ts';
|
|
15
|
+
|
|
16
|
+
const COMPACT_ROW_LIMIT = 5;
|
|
17
|
+
const EXTENDED_ROW_LIMIT = 12;
|
|
18
|
+
|
|
19
|
+
const SELECTION_RANK = {
|
|
20
|
+
inProgress: 0,
|
|
21
|
+
pending: 1,
|
|
22
|
+
resolved: 2,
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
export interface VisibleBoardRows {
|
|
26
|
+
readonly indexes: readonly number[];
|
|
27
|
+
readonly hiddenCount: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function selectVisibleBoardRows(
|
|
31
|
+
items: readonly TodoItem[],
|
|
32
|
+
limit: number,
|
|
33
|
+
): VisibleBoardRows {
|
|
34
|
+
if (items.length <= limit) {
|
|
35
|
+
return { indexes: items.map((_item, index) => index), hiddenCount: 0 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const indexes = items
|
|
39
|
+
.map((item, index) => ({ index, rank: selectionRank(item) }))
|
|
40
|
+
.sort((left, right) => left.rank - right.rank || left.index - right.index)
|
|
41
|
+
.slice(0, limit)
|
|
42
|
+
.map((row) => row.index)
|
|
43
|
+
.sort((left, right) => left - right);
|
|
44
|
+
return { indexes, hiddenCount: items.length - indexes.length };
|
|
45
|
+
}
|
|
3
46
|
|
|
4
47
|
export function renderTodoWidget(
|
|
5
48
|
items: readonly TodoItem[],
|
|
@@ -9,33 +52,199 @@ export function renderTodoWidget(
|
|
|
9
52
|
return undefined;
|
|
10
53
|
}
|
|
11
54
|
|
|
55
|
+
const rows = boardRowLines(items);
|
|
56
|
+
const visible = selectVisibleBoardRows(items, COMPACT_ROW_LIMIT);
|
|
57
|
+
const renderedRows = visible.indexes.flatMap((index) => {
|
|
58
|
+
const item = items[index];
|
|
59
|
+
const row = rows[index];
|
|
60
|
+
if (item === undefined || row === undefined) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
return [colorizeBoardRow(row, item.status, theme)];
|
|
64
|
+
});
|
|
65
|
+
const lines = [
|
|
66
|
+
theme.fg('accent', theme.bold(boardHeader(items))),
|
|
67
|
+
...renderedRows,
|
|
68
|
+
];
|
|
69
|
+
if (visible.hiddenCount > 0) {
|
|
70
|
+
lines.push(theme.fg('dim', hiddenRowsHint(visible.hiddenCount)));
|
|
71
|
+
}
|
|
72
|
+
return lines;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TodoBoardOverlayController {
|
|
76
|
+
toggle(ctx: ExtensionContext): void;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createTodoBoardOverlayController(
|
|
80
|
+
items: () => readonly TodoItem[],
|
|
81
|
+
): TodoBoardOverlayController {
|
|
82
|
+
let close: (() => void) | undefined;
|
|
83
|
+
let opening = false;
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
toggle(ctx) {
|
|
87
|
+
if (close !== undefined) {
|
|
88
|
+
close();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (opening || ctx.mode !== 'tui') {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
opening = true;
|
|
96
|
+
void ctx.ui
|
|
97
|
+
.custom<void>(
|
|
98
|
+
(tui, theme, _keybindings, done) => {
|
|
99
|
+
close = done;
|
|
100
|
+
return new TodoBoardOverlay(tui, items, theme, done);
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
overlay: true,
|
|
104
|
+
overlayOptions: {
|
|
105
|
+
anchor: 'center',
|
|
106
|
+
width: '70%',
|
|
107
|
+
minWidth: 40,
|
|
108
|
+
maxHeight: '80%',
|
|
109
|
+
visible: (terminalWidth) => terminalWidth >= 60,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
.finally(() => {
|
|
114
|
+
close = undefined;
|
|
115
|
+
opening = false;
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class TodoBoardOverlay implements Component {
|
|
122
|
+
private offset = 0;
|
|
123
|
+
private readonly tui: TUI;
|
|
124
|
+
private readonly items: () => readonly TodoItem[];
|
|
125
|
+
private readonly theme: Theme;
|
|
126
|
+
private readonly close: () => void;
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
tui: TUI,
|
|
130
|
+
items: () => readonly TodoItem[],
|
|
131
|
+
theme: Theme,
|
|
132
|
+
close: () => void,
|
|
133
|
+
) {
|
|
134
|
+
this.tui = tui;
|
|
135
|
+
this.items = items;
|
|
136
|
+
this.theme = theme;
|
|
137
|
+
this.close = close;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
handleInput(data: string): void {
|
|
141
|
+
const maxOffset = Math.max(0, this.items().length - EXTENDED_ROW_LIMIT);
|
|
142
|
+
if (matchesKey(data, Key.escape) || matchesKey(data, Key.f9)) {
|
|
143
|
+
this.close();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (matchesKey(data, Key.up)) {
|
|
147
|
+
this.offset = Math.max(0, this.offset - 1);
|
|
148
|
+
this.tui.requestRender();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (matchesKey(data, Key.down)) {
|
|
152
|
+
this.offset = Math.min(maxOffset, this.offset + 1);
|
|
153
|
+
this.tui.requestRender();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (matchesKey(data, Key.pageUp)) {
|
|
157
|
+
this.offset = Math.max(0, this.offset - EXTENDED_ROW_LIMIT);
|
|
158
|
+
this.tui.requestRender();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (matchesKey(data, Key.pageDown)) {
|
|
162
|
+
this.offset = Math.min(maxOffset, this.offset + EXTENDED_ROW_LIMIT);
|
|
163
|
+
this.tui.requestRender();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
render(width: number): string[] {
|
|
168
|
+
const innerWidth = Math.max(1, width - 2);
|
|
169
|
+
const items = this.items();
|
|
170
|
+
const rows = boardRowLines(items);
|
|
171
|
+
const visibleRows = items
|
|
172
|
+
.map((item, index) => ({ item, row: rows[index] }))
|
|
173
|
+
.slice(this.offset, this.offset + EXTENDED_ROW_LIMIT);
|
|
174
|
+
const border = (text: string) => this.theme.fg('border', text);
|
|
175
|
+
const line = (text: string) => truncateToWidth(text, innerWidth, '…', true);
|
|
176
|
+
const title = line(` Work board (${String(items.length)}) `);
|
|
177
|
+
const lines = [
|
|
178
|
+
border(`╭${'─'.repeat(innerWidth)}╮`),
|
|
179
|
+
`${border('│')}${line(this.theme.fg('accent', this.theme.bold(title)))}${border('│')}`,
|
|
180
|
+
`${border('├')}${border('─'.repeat(innerWidth))}${border('┤')}`,
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
for (const entry of visibleRows) {
|
|
184
|
+
const row =
|
|
185
|
+
entry.row === undefined
|
|
186
|
+
? ''
|
|
187
|
+
: colorizeBoardRow(entry.row, entry.item.status, this.theme);
|
|
188
|
+
lines.push(`${border('│')}${line(row)}${border('│')}`);
|
|
189
|
+
}
|
|
190
|
+
if (items.length === 0) {
|
|
191
|
+
lines.push(
|
|
192
|
+
`${border('│')}${line(this.theme.fg('muted', ' No items on the board.'))}${border('│')}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const scrollHint = items.length > EXTENDED_ROW_LIMIT ? ' ↑↓ scroll ·' : '';
|
|
197
|
+
lines.push(`${border('├')}${border('─'.repeat(innerWidth))}${border('┤')}`);
|
|
198
|
+
lines.push(
|
|
199
|
+
`${border('│')}${line(this.theme.fg('dim', `${scrollHint} F9 or Esc close`))}${border('│')}`,
|
|
200
|
+
);
|
|
201
|
+
lines.push(border(`╰${'─'.repeat(innerWidth)}╯`));
|
|
202
|
+
return lines;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
invalidate(): void {}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function selectionRank(item: TodoItem): number {
|
|
209
|
+
if (item.status === TODO_STATUS.inProgress) {
|
|
210
|
+
return SELECTION_RANK.inProgress;
|
|
211
|
+
}
|
|
212
|
+
if (item.status === TODO_STATUS.pending) {
|
|
213
|
+
return SELECTION_RANK.pending;
|
|
214
|
+
}
|
|
215
|
+
return SELECTION_RANK.resolved;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function boardHeader(items: readonly TodoItem[]): string {
|
|
12
219
|
const active = items.filter(
|
|
13
220
|
(item) => item.status === TODO_STATUS.inProgress,
|
|
14
221
|
).length;
|
|
15
222
|
const queued = items.filter(
|
|
16
223
|
(item) => item.status === TODO_STATUS.pending,
|
|
17
224
|
).length;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
225
|
+
return `Sideroom board (${String(active)} active, ${String(queued)} queued)`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function boardRowLines(items: readonly TodoItem[]): string[] {
|
|
229
|
+
return formatBoardBlock(items)
|
|
230
|
+
.split('\n')
|
|
231
|
+
.slice(1, 1 + items.length);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function colorizeBoardRow(
|
|
235
|
+
row: string,
|
|
236
|
+
status: TodoStatus,
|
|
237
|
+
theme: Theme,
|
|
238
|
+
): string {
|
|
239
|
+
if (status === TODO_STATUS.inProgress) {
|
|
240
|
+
return theme.fg('success', row);
|
|
241
|
+
}
|
|
242
|
+
if (status === TODO_STATUS.pending) {
|
|
243
|
+
return theme.fg('text', row);
|
|
244
|
+
}
|
|
245
|
+
return theme.fg('muted', row);
|
|
246
|
+
}
|
|
39
247
|
|
|
40
|
-
|
|
248
|
+
function hiddenRowsHint(hiddenCount: number): string {
|
|
249
|
+
return `…+${String(hiddenCount)} more · F9: view all`;
|
|
41
250
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmrdeveloper/sideroom-pi",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "Pi coding-agent package that asks clarifying questions, shows a live todo board, enforces coding guidelines, and will not call it done while your checks are red.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"version-packages": "changeset version",
|
|
17
17
|
"commitlint": "commitlint",
|
|
18
18
|
"types": "tsc --noEmit",
|
|
19
|
-
"test": "node --test 'extensions/**/*.test.ts'",
|
|
19
|
+
"test": "node --test 'extensions/**/*.test.ts' 'scripts/**/*.test.mjs'",
|
|
20
20
|
"lint": "biome lint extensions scripts commitlint.config.mjs",
|
|
21
21
|
"lint:fix": "biome lint --write extensions scripts commitlint.config.mjs",
|
|
22
22
|
"format": "biome format --write extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
|