@danypops/pi-papyrus 0.56.3 → 0.57.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/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,23 @@ 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(
|
|
138
|
+
export function buildTaskWidgetSection(
|
|
139
|
+
theme: Theme,
|
|
140
|
+
projection: TaskWidgetProjection,
|
|
141
|
+
rotation: AutoRotatingWindow,
|
|
142
|
+
): WidgetSection | undefined {
|
|
127
143
|
if (projection.openTotal === 0) return undefined;
|
|
128
|
-
|
|
144
|
+
rotation.setTotalRows(projection.rows.length);
|
|
145
|
+
return {
|
|
146
|
+
label: taskSectionLabel(projection, rotation),
|
|
147
|
+
render: (width) => renderTaskSectionBodyLines(theme, projection, width, rotation),
|
|
148
|
+
};
|
|
129
149
|
}
|
|
130
150
|
|
|
131
151
|
export class TaskOverlay {
|
|
@@ -135,6 +155,11 @@ export class TaskOverlay {
|
|
|
135
155
|
private sessionId: string | undefined;
|
|
136
156
|
private readonly poll = new BoundedPoll();
|
|
137
157
|
private pushChannel: PushChannelClient | undefined;
|
|
158
|
+
private readonly rotation = new AutoRotatingWindow({
|
|
159
|
+
totalRows: 0,
|
|
160
|
+
pageSize: TASK_WIDGET_VISIBLE_ROWS,
|
|
161
|
+
intervalMs: WIDGET_CARD_ROTATION_INTERVAL_MS,
|
|
162
|
+
});
|
|
138
163
|
|
|
139
164
|
setWidgetGroup(group: PapyrusWidgetGroup): void {
|
|
140
165
|
this.widgetGroup = group;
|
|
@@ -196,7 +221,7 @@ export class TaskOverlay {
|
|
|
196
221
|
|
|
197
222
|
/** undefined when there is no open work to show -- PapyrusWidgetGroup's own signal to omit this section entirely. */
|
|
198
223
|
buildSection(theme: Theme): WidgetSection | undefined {
|
|
199
|
-
return buildTaskWidgetSection(theme, buildTaskWidgetProjection(this.snapshot));
|
|
224
|
+
return buildTaskWidgetSection(theme, buildTaskWidgetProjection(this.snapshot), this.rotation);
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
/**
|
|
@@ -224,16 +249,21 @@ export class TaskOverlay {
|
|
|
224
249
|
}
|
|
225
250
|
|
|
226
251
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* default.
|
|
252
|
+
* notes.list already scopes to project_root exactly (a note's projectRoot is fixed at capture
|
|
253
|
+
* time), so passing this overlay's projectRoot is what makes it CWD-aware by default. Keeps a
|
|
254
|
+
* bounded set of actual note titles (NOTE_WIDGET_OPEN_LIMIT), not just a count.
|
|
231
255
|
*/
|
|
232
256
|
export class NoteOverlay {
|
|
233
257
|
private widgetGroup: PapyrusWidgetGroup | undefined;
|
|
234
|
-
private
|
|
258
|
+
private notes: NoteWidgetRow[] = [];
|
|
259
|
+
private totalOpenCount = 0;
|
|
235
260
|
private projectRoot: string | undefined;
|
|
236
261
|
private readonly poll = new BoundedPoll();
|
|
262
|
+
private readonly rotation = new AutoRotatingWindow({
|
|
263
|
+
totalRows: 0,
|
|
264
|
+
pageSize: NOTE_WIDGET_VISIBLE_ROWS,
|
|
265
|
+
intervalMs: WIDGET_CARD_ROTATION_INTERVAL_MS,
|
|
266
|
+
});
|
|
237
267
|
|
|
238
268
|
setWidgetGroup(group: PapyrusWidgetGroup): void {
|
|
239
269
|
this.widgetGroup = group;
|
|
@@ -250,9 +280,11 @@ export class NoteOverlay {
|
|
|
250
280
|
project_root: this.projectRoot,
|
|
251
281
|
limit: NOTE_LIST_MAX_LIMIT,
|
|
252
282
|
});
|
|
253
|
-
this.
|
|
283
|
+
this.totalOpenCount = rows.length;
|
|
284
|
+
this.notes = rows.slice(0, NOTE_WIDGET_OPEN_LIMIT).map((row) => ({ id: row.id, title: row.title }));
|
|
254
285
|
} catch {
|
|
255
|
-
this.
|
|
286
|
+
this.totalOpenCount = 0;
|
|
287
|
+
this.notes = [];
|
|
256
288
|
}
|
|
257
289
|
try {
|
|
258
290
|
this.widgetGroup?.requestUpdate();
|
|
@@ -263,12 +295,12 @@ export class NoteOverlay {
|
|
|
263
295
|
|
|
264
296
|
/** Theme-free existence check -- see TaskOverlay's own hasOpenWork() for why this needs to stay theme-free. */
|
|
265
297
|
hasOpenNotes(): boolean {
|
|
266
|
-
return this.
|
|
298
|
+
return this.totalOpenCount > 0;
|
|
267
299
|
}
|
|
268
300
|
|
|
269
301
|
/** undefined when there are no open notes -- PapyrusWidgetGroup's own signal to omit this section entirely. */
|
|
270
302
|
buildSection(): WidgetSection | undefined {
|
|
271
|
-
return buildNoteWidgetSection(this.
|
|
303
|
+
return buildNoteWidgetSection(this.notes, this.totalOpenCount, this.rotation);
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
startPolling(intervalMs: number = NOTE_WIDGET_POLL_INTERVAL_MS): void {
|
|
@@ -291,12 +323,11 @@ export class NoteOverlay {
|
|
|
291
323
|
const PAPYRUS_WIDGET_KEY = "pi-papyrus";
|
|
292
324
|
|
|
293
325
|
/**
|
|
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.
|
|
326
|
+
* Owns the ONE real aboveEditor widget registration for both TaskOverlay and NoteOverlay, tiling
|
|
327
|
+
* their own current sections as bordered CardRow cards, instead of each overlay registering its
|
|
328
|
+
* own separate "Papyrus · <Label>" flat-header widget. Hidden entirely (fully unregistered, not
|
|
329
|
+
* just empty lines) only once NEITHER overlay has anything to show -- either overlay alone still
|
|
330
|
+
* renders its own section normally.
|
|
300
331
|
*/
|
|
301
332
|
export class PapyrusWidgetGroup {
|
|
302
333
|
private uiCtx: ExtensionUIContext | undefined;
|
|
@@ -304,6 +335,10 @@ export class PapyrusWidgetGroup {
|
|
|
304
335
|
private tui: any | undefined;
|
|
305
336
|
private taskOverlay: TaskOverlay | undefined;
|
|
306
337
|
private noteOverlay: NoteOverlay | undefined;
|
|
338
|
+
/** Repaint-only ticker (no data refetch) so a card's own auto-rotating overflow page visibly
|
|
339
|
+
* advances even when nothing else has changed -- pageIndex is time-based, so a plain repaint is
|
|
340
|
+
* all a page transition needs. */
|
|
341
|
+
private readonly rotationPoll = new BoundedPoll();
|
|
307
342
|
|
|
308
343
|
setUI(ctx: ExtensionUIContext): void {
|
|
309
344
|
if (ctx !== this.uiCtx) {
|
|
@@ -331,6 +366,7 @@ export class PapyrusWidgetGroup {
|
|
|
331
366
|
this.uiCtx.setWidget(PAPYRUS_WIDGET_KEY, undefined);
|
|
332
367
|
this.registered = false;
|
|
333
368
|
this.tui = undefined;
|
|
369
|
+
this.rotationPoll.stop();
|
|
334
370
|
}
|
|
335
371
|
return;
|
|
336
372
|
}
|
|
@@ -352,6 +388,7 @@ export class PapyrusWidgetGroup {
|
|
|
352
388
|
{ placement: "aboveEditor" },
|
|
353
389
|
);
|
|
354
390
|
this.registered = true;
|
|
391
|
+
this.rotationPoll.start(WIDGET_CARD_ROTATION_INTERVAL_MS, () => this.tui?.requestRender?.());
|
|
355
392
|
} else {
|
|
356
393
|
this.tui?.requestRender?.();
|
|
357
394
|
}
|
|
@@ -368,22 +405,24 @@ export class PapyrusWidgetGroup {
|
|
|
368
405
|
this.uiCtx.setWidget(PAPYRUS_WIDGET_KEY, undefined);
|
|
369
406
|
this.registered = false;
|
|
370
407
|
this.tui = undefined;
|
|
408
|
+
this.rotationPoll.stop();
|
|
371
409
|
}
|
|
372
410
|
return [];
|
|
373
411
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
//
|
|
412
|
+
// Each card carries its own full "Papyrus · <label>" title -- there's no separate shared
|
|
413
|
+
// owner header in a card grid the way there was in the old tree/split layout.
|
|
414
|
+
const owner = vehicleWidgetOwner(PAPYRUS_VEHICLE_NAME);
|
|
415
|
+
const cards: WidgetSection[] = sections.map((section) => ({
|
|
416
|
+
label: `${owner} · ${section.label}`,
|
|
417
|
+
render: section.render,
|
|
418
|
+
style: (s: string) => theme.fg("muted", s),
|
|
419
|
+
}));
|
|
420
|
+
return renderCardRow(cards, width, {
|
|
421
|
+
// ANSI-aware measure: every task row is real ANSI-colored content (theme.fg() for
|
|
422
|
+
// focus/status glyphs). See tool-rendering/artifact-card.ts's own identical measure.
|
|
385
423
|
measure: artifactCardMeasure,
|
|
386
|
-
|
|
424
|
+
frameStyle: (s) => theme.fg("borderMuted", s),
|
|
425
|
+
});
|
|
387
426
|
}
|
|
388
427
|
|
|
389
428
|
dispose(): void {
|
|
@@ -393,6 +432,7 @@ export class PapyrusWidgetGroup {
|
|
|
393
432
|
this.uiCtx = undefined;
|
|
394
433
|
this.taskOverlay = undefined;
|
|
395
434
|
this.noteOverlay = undefined;
|
|
435
|
+
this.rotationPoll.stop();
|
|
396
436
|
}
|
|
397
437
|
}
|
|
398
438
|
|
|
@@ -1,14 +1,34 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { AutoRotatingWindow, WidgetSection } from "malevich-tui-components";
|
|
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.1",
|
|
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"],
|
|
@@ -12,23 +12,23 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
15
|
+
"@danypops/vehicle-client-pi": "^0.44.0",
|
|
16
16
|
"@earendil-works/pi-coding-agent": "*",
|
|
17
17
|
"@earendil-works/pi-tui": "*",
|
|
18
18
|
"typebox": "*"
|
|
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",
|
|
31
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
31
|
+
"@danypops/vehicle-client-pi": "^0.44.0",
|
|
32
32
|
"@danypops/vehicle-conformance": "^0.3.0",
|
|
33
33
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
34
34
|
"bun-types": "latest",
|