@danypops/pi-papyrus 0.56.3 → 0.57.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/extension/src/index.ts +74 -41
- package/extension/src/note/note-widget.ts +30 -10
- package/package.json +3 -3
package/extension/src/index.ts
CHANGED
|
@@ -13,20 +13,24 @@ import {
|
|
|
13
13
|
type Artifact,
|
|
14
14
|
type GateResult,
|
|
15
15
|
NOTE_LIST_MAX_LIMIT,
|
|
16
|
+
NOTE_WIDGET_OPEN_LIMIT,
|
|
16
17
|
NOTE_WIDGET_POLL_INTERVAL_MS,
|
|
18
|
+
NOTE_WIDGET_VISIBLE_ROWS,
|
|
17
19
|
PAPYRUS_CONTEXT_INJECTION_CHANNEL,
|
|
18
20
|
PAPYRUS_VEHICLE_NAME,
|
|
19
21
|
TASK_DRIVER_MAX_TURNS,
|
|
20
22
|
TASK_DRIVER_MAX_UNCHANGED_TURNS,
|
|
21
23
|
TASK_WIDGET_POLL_INTERVAL_MS,
|
|
24
|
+
TASK_WIDGET_VISIBLE_ROWS,
|
|
22
25
|
type TaskGraph,
|
|
23
26
|
type TaskStatus,
|
|
27
|
+
WIDGET_CARD_ROTATION_INTERVAL_MS,
|
|
24
28
|
} from "@danypops/papyrus";
|
|
25
29
|
import type { PushChannelClient } from "@danypops/vehicle-client/daemon-client";
|
|
26
30
|
import { vehicleWidgetOwner } from "@danypops/vehicle-client-pi/widget-header";
|
|
27
31
|
import type { ExtensionAPI, ExtensionContext, ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
28
32
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
29
|
-
import {
|
|
33
|
+
import { AutoRotatingWindow, renderCardRow, type WidgetSection } from "malevich-tui-components";
|
|
30
34
|
import { Type } from "typebox";
|
|
31
35
|
import { formatMetadata } from "./artifact/artifact-format.ts";
|
|
32
36
|
import { BoundedPoll } from "./bounded-poll.ts";
|
|
@@ -35,7 +39,7 @@ import { PAPYRUS_CONTEXT_HUB_PRODUCER_NAME, papyrusContextSegment } from "./cont
|
|
|
35
39
|
import { buildContextInjection } from "./context/context-injection-telemetry.ts";
|
|
36
40
|
import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss/discuss-ask-view.ts";
|
|
37
41
|
import { resolveNameFields } from "./domain-tools.ts";
|
|
38
|
-
import { buildNoteWidgetSection } from "./note/note-widget.ts";
|
|
42
|
+
import { buildNoteWidgetSection, type NoteWidgetRow } from "./note/note-widget.ts";
|
|
39
43
|
import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook/playbook-bridge.ts";
|
|
40
44
|
import { callService, subscribeTaskPushChannel } from "./service-client.ts";
|
|
41
45
|
import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
|
|
@@ -93,15 +97,22 @@ async function artifactNamesById(ids: readonly string[]): Promise<Map<string, st
|
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
// ---------------------------------------------------------------------------
|
|
96
|
-
// Task + Notes widget (one shared aboveEditor
|
|
97
|
-
//
|
|
98
|
-
//
|
|
100
|
+
// Task + Notes widget (one shared aboveEditor CardRow grid -- PapyrusWidgetGroup below
|
|
101
|
+
// tiles both overlays' own sections as bordered cards, instead of each registering
|
|
102
|
+
// its own separate "Papyrus · <Label>" flat header)
|
|
99
103
|
// ---------------------------------------------------------------------------
|
|
100
104
|
|
|
101
|
-
/**
|
|
102
|
-
|
|
105
|
+
/** Renders the page bounded by `rotation`'s own currentPageBounds() -- tree-connector glyphs
|
|
106
|
+
* are computed against the FULL row list at each row's own absolute index. */
|
|
107
|
+
export function renderTaskSectionBodyLines(
|
|
108
|
+
theme: Theme,
|
|
109
|
+
projection: TaskWidgetProjection,
|
|
110
|
+
width: number,
|
|
111
|
+
rotation?: AutoRotatingWindow,
|
|
112
|
+
): string[] {
|
|
113
|
+
const { start, end } = rotation?.currentPageBounds() ?? { start: 0, end: projection.rows.length };
|
|
103
114
|
const lines: string[] = [];
|
|
104
|
-
for (let index =
|
|
115
|
+
for (let index = start; index < end; index++) {
|
|
105
116
|
const row = projection.rows[index]!;
|
|
106
117
|
const laterSibling = projection.rows.slice(index + 1).some((candidate) => candidate.depth === row.depth);
|
|
107
118
|
const hierarchy = taskTreeConnector({ depth: row.depth, hasChildren: row.hasOpenChildren, hasLaterSibling: laterSibling });
|
|
@@ -118,14 +129,16 @@ export function renderTaskSectionBodyLines(theme: Theme, projection: TaskWidgetP
|
|
|
118
129
|
return lines;
|
|
119
130
|
}
|
|
120
131
|
|
|
121
|
-
/** "Tasks" or "Tasks · <scope>"
|
|
122
|
-
export function taskSectionLabel(projection: TaskWidgetProjection): string {
|
|
123
|
-
|
|
132
|
+
/** "Tasks" or "Tasks · <scope>", plus a "page/total ⟳" suffix once genuinely paging. */
|
|
133
|
+
export function taskSectionLabel(projection: TaskWidgetProjection, rotation?: AutoRotatingWindow): string {
|
|
134
|
+
const base = projection.scopeLabel ? `Tasks · ${projection.scopeLabel}` : "Tasks";
|
|
135
|
+
return rotation?.isPaging ? `${base} · ${rotation.pageIndex + 1}/${rotation.pageCount} ⟳` : base;
|
|
124
136
|
}
|
|
125
137
|
|
|
126
|
-
export function buildTaskWidgetSection(theme: Theme, projection: TaskWidgetProjection): WidgetSection | undefined {
|
|
138
|
+
export function buildTaskWidgetSection(theme: Theme, projection: TaskWidgetProjection, rotation: AutoRotatingWindow): WidgetSection | undefined {
|
|
127
139
|
if (projection.openTotal === 0) return undefined;
|
|
128
|
-
|
|
140
|
+
rotation.setTotalRows(projection.rows.length);
|
|
141
|
+
return { label: taskSectionLabel(projection, rotation), render: (width) => renderTaskSectionBodyLines(theme, projection, width, rotation) };
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
export class TaskOverlay {
|
|
@@ -135,6 +148,11 @@ export class TaskOverlay {
|
|
|
135
148
|
private sessionId: string | undefined;
|
|
136
149
|
private readonly poll = new BoundedPoll();
|
|
137
150
|
private pushChannel: PushChannelClient | undefined;
|
|
151
|
+
private readonly rotation = new AutoRotatingWindow({
|
|
152
|
+
totalRows: 0,
|
|
153
|
+
pageSize: TASK_WIDGET_VISIBLE_ROWS,
|
|
154
|
+
intervalMs: WIDGET_CARD_ROTATION_INTERVAL_MS,
|
|
155
|
+
});
|
|
138
156
|
|
|
139
157
|
setWidgetGroup(group: PapyrusWidgetGroup): void {
|
|
140
158
|
this.widgetGroup = group;
|
|
@@ -196,7 +214,7 @@ export class TaskOverlay {
|
|
|
196
214
|
|
|
197
215
|
/** undefined when there is no open work to show -- PapyrusWidgetGroup's own signal to omit this section entirely. */
|
|
198
216
|
buildSection(theme: Theme): WidgetSection | undefined {
|
|
199
|
-
return buildTaskWidgetSection(theme, buildTaskWidgetProjection(this.snapshot));
|
|
217
|
+
return buildTaskWidgetSection(theme, buildTaskWidgetProjection(this.snapshot), this.rotation);
|
|
200
218
|
}
|
|
201
219
|
|
|
202
220
|
/**
|
|
@@ -224,16 +242,21 @@ export class TaskOverlay {
|
|
|
224
242
|
}
|
|
225
243
|
|
|
226
244
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* default.
|
|
245
|
+
* notes.list already scopes to project_root exactly (a note's projectRoot is fixed at capture
|
|
246
|
+
* time), so passing this overlay's projectRoot is what makes it CWD-aware by default. Keeps a
|
|
247
|
+
* bounded set of actual note titles (NOTE_WIDGET_OPEN_LIMIT), not just a count.
|
|
231
248
|
*/
|
|
232
249
|
export class NoteOverlay {
|
|
233
250
|
private widgetGroup: PapyrusWidgetGroup | undefined;
|
|
234
|
-
private
|
|
251
|
+
private notes: NoteWidgetRow[] = [];
|
|
252
|
+
private totalOpenCount = 0;
|
|
235
253
|
private projectRoot: string | undefined;
|
|
236
254
|
private readonly poll = new BoundedPoll();
|
|
255
|
+
private readonly rotation = new AutoRotatingWindow({
|
|
256
|
+
totalRows: 0,
|
|
257
|
+
pageSize: NOTE_WIDGET_VISIBLE_ROWS,
|
|
258
|
+
intervalMs: WIDGET_CARD_ROTATION_INTERVAL_MS,
|
|
259
|
+
});
|
|
237
260
|
|
|
238
261
|
setWidgetGroup(group: PapyrusWidgetGroup): void {
|
|
239
262
|
this.widgetGroup = group;
|
|
@@ -250,9 +273,11 @@ export class NoteOverlay {
|
|
|
250
273
|
project_root: this.projectRoot,
|
|
251
274
|
limit: NOTE_LIST_MAX_LIMIT,
|
|
252
275
|
});
|
|
253
|
-
this.
|
|
276
|
+
this.totalOpenCount = rows.length;
|
|
277
|
+
this.notes = rows.slice(0, NOTE_WIDGET_OPEN_LIMIT).map((row) => ({ id: row.id, title: row.title }));
|
|
254
278
|
} catch {
|
|
255
|
-
this.
|
|
279
|
+
this.totalOpenCount = 0;
|
|
280
|
+
this.notes = [];
|
|
256
281
|
}
|
|
257
282
|
try {
|
|
258
283
|
this.widgetGroup?.requestUpdate();
|
|
@@ -263,12 +288,12 @@ export class NoteOverlay {
|
|
|
263
288
|
|
|
264
289
|
/** Theme-free existence check -- see TaskOverlay's own hasOpenWork() for why this needs to stay theme-free. */
|
|
265
290
|
hasOpenNotes(): boolean {
|
|
266
|
-
return this.
|
|
291
|
+
return this.totalOpenCount > 0;
|
|
267
292
|
}
|
|
268
293
|
|
|
269
294
|
/** undefined when there are no open notes -- PapyrusWidgetGroup's own signal to omit this section entirely. */
|
|
270
295
|
buildSection(): WidgetSection | undefined {
|
|
271
|
-
return buildNoteWidgetSection(this.
|
|
296
|
+
return buildNoteWidgetSection(this.notes, this.totalOpenCount, this.rotation);
|
|
272
297
|
}
|
|
273
298
|
|
|
274
299
|
startPolling(intervalMs: number = NOTE_WIDGET_POLL_INTERVAL_MS): void {
|
|
@@ -291,12 +316,11 @@ export class NoteOverlay {
|
|
|
291
316
|
const PAPYRUS_WIDGET_KEY = "pi-papyrus";
|
|
292
317
|
|
|
293
318
|
/**
|
|
294
|
-
* Owns the ONE real aboveEditor widget registration for both TaskOverlay and NoteOverlay,
|
|
295
|
-
* their own current sections
|
|
296
|
-
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
* show -- either overlay alone still renders its own section normally.
|
|
319
|
+
* Owns the ONE real aboveEditor widget registration for both TaskOverlay and NoteOverlay, tiling
|
|
320
|
+
* their own current sections as bordered CardRow cards, instead of each overlay registering its
|
|
321
|
+
* own separate "Papyrus · <Label>" flat-header widget. Hidden entirely (fully unregistered, not
|
|
322
|
+
* just empty lines) only once NEITHER overlay has anything to show -- either overlay alone still
|
|
323
|
+
* renders its own section normally.
|
|
300
324
|
*/
|
|
301
325
|
export class PapyrusWidgetGroup {
|
|
302
326
|
private uiCtx: ExtensionUIContext | undefined;
|
|
@@ -304,6 +328,10 @@ export class PapyrusWidgetGroup {
|
|
|
304
328
|
private tui: any | undefined;
|
|
305
329
|
private taskOverlay: TaskOverlay | undefined;
|
|
306
330
|
private noteOverlay: NoteOverlay | undefined;
|
|
331
|
+
/** Repaint-only ticker (no data refetch) so a card's own auto-rotating overflow page visibly
|
|
332
|
+
* advances even when nothing else has changed -- pageIndex is time-based, so a plain repaint is
|
|
333
|
+
* all a page transition needs. */
|
|
334
|
+
private readonly rotationPoll = new BoundedPoll();
|
|
307
335
|
|
|
308
336
|
setUI(ctx: ExtensionUIContext): void {
|
|
309
337
|
if (ctx !== this.uiCtx) {
|
|
@@ -331,6 +359,7 @@ export class PapyrusWidgetGroup {
|
|
|
331
359
|
this.uiCtx.setWidget(PAPYRUS_WIDGET_KEY, undefined);
|
|
332
360
|
this.registered = false;
|
|
333
361
|
this.tui = undefined;
|
|
362
|
+
this.rotationPoll.stop();
|
|
334
363
|
}
|
|
335
364
|
return;
|
|
336
365
|
}
|
|
@@ -352,6 +381,7 @@ export class PapyrusWidgetGroup {
|
|
|
352
381
|
{ placement: "aboveEditor" },
|
|
353
382
|
);
|
|
354
383
|
this.registered = true;
|
|
384
|
+
this.rotationPoll.start(WIDGET_CARD_ROTATION_INTERVAL_MS, () => this.tui?.requestRender?.());
|
|
355
385
|
} else {
|
|
356
386
|
this.tui?.requestRender?.();
|
|
357
387
|
}
|
|
@@ -368,22 +398,24 @@ export class PapyrusWidgetGroup {
|
|
|
368
398
|
this.uiCtx.setWidget(PAPYRUS_WIDGET_KEY, undefined);
|
|
369
399
|
this.registered = false;
|
|
370
400
|
this.tui = undefined;
|
|
401
|
+
this.rotationPoll.stop();
|
|
371
402
|
}
|
|
372
403
|
return [];
|
|
373
404
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
//
|
|
405
|
+
// Each card carries its own full "Papyrus · <label>" title -- there's no separate shared
|
|
406
|
+
// owner header in a card grid the way there was in the old tree/split layout.
|
|
407
|
+
const owner = vehicleWidgetOwner(PAPYRUS_VEHICLE_NAME);
|
|
408
|
+
const cards: WidgetSection[] = sections.map((section) => ({
|
|
409
|
+
label: `${owner} · ${section.label}`,
|
|
410
|
+
render: section.render,
|
|
411
|
+
style: (s: string) => theme.fg("muted", s),
|
|
412
|
+
}));
|
|
413
|
+
return renderCardRow(cards, width, {
|
|
414
|
+
// ANSI-aware measure: every task row is real ANSI-colored content (theme.fg() for
|
|
415
|
+
// focus/status glyphs). See tool-rendering/artifact-card.ts's own identical measure.
|
|
385
416
|
measure: artifactCardMeasure,
|
|
386
|
-
|
|
417
|
+
frameStyle: (s) => theme.fg("borderMuted", s),
|
|
418
|
+
});
|
|
387
419
|
}
|
|
388
420
|
|
|
389
421
|
dispose(): void {
|
|
@@ -393,6 +425,7 @@ export class PapyrusWidgetGroup {
|
|
|
393
425
|
this.uiCtx = undefined;
|
|
394
426
|
this.taskOverlay = undefined;
|
|
395
427
|
this.noteOverlay = undefined;
|
|
428
|
+
this.rotationPoll.stop();
|
|
396
429
|
}
|
|
397
430
|
}
|
|
398
431
|
|
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
import type { WidgetSection } from "malevich-tui-components";
|
|
1
|
+
import type { AutoRotatingWindow, WidgetSection } from "malevich-tui-components";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
export interface NoteWidgetRow {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** "Notes <total>" (the TRUE open count, even when fewer titles are actually kept/paged through),
|
|
10
|
+
* plus a "page/total ⟳" suffix once genuinely paging -- never shown when everything already fits
|
|
11
|
+
* on one page. */
|
|
12
|
+
function noteSectionLabel(totalOpenCount: number, rotation: AutoRotatingWindow): string {
|
|
13
|
+
const base = `Notes ${totalOpenCount}`;
|
|
14
|
+
return rotation.isPaging ? `${base} · ${rotation.pageIndex + 1}/${rotation.pageCount} ⟳` : base;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderNoteSectionBodyLines(notes: readonly NoteWidgetRow[], width: number, rotation: AutoRotatingWindow): string[] {
|
|
18
|
+
const { start, end } = rotation.currentPageBounds();
|
|
19
|
+
return notes.slice(start, end).map((note) => truncateToWidth(`· ${note.title}`, width, "…"));
|
|
20
|
+
}
|
|
2
21
|
|
|
3
22
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Deliberately one uniform label string, not an embedded differently-colored count: the caller's
|
|
7
|
-
* own section `style` wraps this whole branch line, and nesting a second color inside it here
|
|
8
|
-
* would risk incorrect ANSI reset composition -- every other section label in this widget group
|
|
9
|
-
* stays one tone for the same reason.
|
|
23
|
+
* `undefined` (hide the section entirely) once totalOpenCount is 0. `rotation`'s own row count is
|
|
24
|
+
* kept in sync with `notes.length` here, so a caller only ever needs to pass its own current data.
|
|
10
25
|
*/
|
|
11
|
-
export function buildNoteWidgetSection(
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
export function buildNoteWidgetSection(
|
|
27
|
+
notes: readonly NoteWidgetRow[],
|
|
28
|
+
totalOpenCount: number,
|
|
29
|
+
rotation: AutoRotatingWindow,
|
|
30
|
+
): WidgetSection | undefined {
|
|
31
|
+
if (totalOpenCount === 0) return undefined;
|
|
32
|
+
rotation.setTotalRows(notes.length);
|
|
33
|
+
return { label: noteSectionLabel(totalOpenCount, rotation), render: (width) => renderNoteSectionBodyLines(notes, width, rotation) };
|
|
14
34
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.0",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -19,12 +19,12 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@danypops/jittor": "^0.19.2",
|
|
22
|
-
"@danypops/papyrus": "^0.
|
|
22
|
+
"@danypops/papyrus": "^0.60.0",
|
|
23
23
|
"@danypops/vehicle-client": "^0.10.3",
|
|
24
24
|
"@danypops/vehicle-core": "^0.17.1",
|
|
25
25
|
"@danypops/vehicle-server": "^0.25.2",
|
|
26
26
|
"beautiful-mermaid": "1.1.3",
|
|
27
|
-
"malevich-tui-components": "^0.
|
|
27
|
+
"malevich-tui-components": "^0.32.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@danypops/pi-tui-harness": "^0.0.2",
|