@danypops/pi-papyrus 0.55.9 → 0.56.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 +138 -90
- package/extension/src/note/note-widget.ts +12 -9
- package/package.json +2 -2
package/extension/src/index.ts
CHANGED
|
@@ -23,9 +23,10 @@ import {
|
|
|
23
23
|
type TaskStatus,
|
|
24
24
|
} from "@danypops/papyrus";
|
|
25
25
|
import type { PushChannelClient } from "@danypops/vehicle-client/daemon-client";
|
|
26
|
-
import {
|
|
26
|
+
import { vehicleWidgetOwner } from "@danypops/vehicle-client-pi/widget-header";
|
|
27
27
|
import type { ExtensionAPI, ExtensionContext, ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
28
28
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
29
|
+
import { renderWidgetSectionGroup, type WidgetSection } from "malevich-tui-components";
|
|
29
30
|
import { Type } from "typebox";
|
|
30
31
|
import { formatMetadata } from "./artifact/artifact-format.ts";
|
|
31
32
|
import { BoundedPoll } from "./bounded-poll.ts";
|
|
@@ -34,7 +35,7 @@ import { PAPYRUS_CONTEXT_HUB_PRODUCER_NAME, papyrusContextSegment } from "./cont
|
|
|
34
35
|
import { buildContextInjection } from "./context/context-injection-telemetry.ts";
|
|
35
36
|
import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss/discuss-ask-view.ts";
|
|
36
37
|
import { resolveNameFields } from "./domain-tools.ts";
|
|
37
|
-
import {
|
|
38
|
+
import { buildNoteWidgetSection } from "./note/note-widget.ts";
|
|
38
39
|
import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook/playbook-bridge.ts";
|
|
39
40
|
import { callService, subscribeTaskPushChannel } from "./service-client.ts";
|
|
40
41
|
import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
|
|
@@ -91,15 +92,14 @@ async function artifactNamesById(ids: readonly string[]): Promise<Map<string, st
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
// ---------------------------------------------------------------------------
|
|
94
|
-
// Task widget (
|
|
95
|
+
// Task + Notes widget (one shared aboveEditor tree -- PapyrusWidgetGroup below
|
|
96
|
+
// composes both overlays' own sections via renderWidgetSectionGroup, instead of
|
|
97
|
+
// each registering its own separate "Papyrus · <Label>" flat header)
|
|
95
98
|
// ---------------------------------------------------------------------------
|
|
96
99
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
if (projection.openTotal === 0) return [];
|
|
101
|
-
const header = truncateToWidth(theme.fg("muted", vehicleWidgetTitle(PAPYRUS_VEHICLE_NAME, "Tasks", projection.scopeLabel)), width, "…");
|
|
102
|
-
const lines: string[] = [header];
|
|
100
|
+
/** The rows only -- no header line; PapyrusWidgetGroup's own owner header covers that now. */
|
|
101
|
+
export function renderTaskSectionBodyLines(theme: Theme, projection: TaskWidgetProjection, width: number): string[] {
|
|
102
|
+
const lines: string[] = [];
|
|
103
103
|
for (let index = 0; index < projection.rows.length; index++) {
|
|
104
104
|
const row = projection.rows[index]!;
|
|
105
105
|
const laterSibling = projection.rows.slice(index + 1).some((candidate) => candidate.depth === row.depth);
|
|
@@ -117,22 +117,26 @@ export function renderTaskWidgetLines(theme: Theme, projection: TaskWidgetProjec
|
|
|
117
117
|
return lines;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/** "Tasks" or "Tasks · <scope>" -- no more "Papyrus ·" prefix; the group's own owner header covers that. */
|
|
121
|
+
export function taskSectionLabel(projection: TaskWidgetProjection): string {
|
|
122
|
+
return projection.scopeLabel ? `Tasks · ${projection.scopeLabel}` : "Tasks";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildTaskWidgetSection(theme: Theme, projection: TaskWidgetProjection): WidgetSection | undefined {
|
|
126
|
+
if (projection.openTotal === 0) return undefined;
|
|
127
|
+
return { label: taskSectionLabel(projection), render: (width) => renderTaskSectionBodyLines(theme, projection, width) };
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
export class TaskOverlay {
|
|
121
|
-
private
|
|
122
|
-
private registered = false;
|
|
123
|
-
private tui: any | undefined;
|
|
131
|
+
private widgetGroup: PapyrusWidgetGroup | undefined;
|
|
124
132
|
private snapshot: TaskGraph = { nodes: [], rootIds: [] };
|
|
125
133
|
private projectRoot: string | undefined;
|
|
126
134
|
private sessionId: string | undefined;
|
|
127
135
|
private readonly poll = new BoundedPoll();
|
|
128
136
|
private pushChannel: PushChannelClient | undefined;
|
|
129
137
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.uiCtx = ctx;
|
|
133
|
-
this.registered = false;
|
|
134
|
-
this.tui = undefined;
|
|
135
|
-
}
|
|
138
|
+
setWidgetGroup(group: PapyrusWidgetGroup): void {
|
|
139
|
+
this.widgetGroup = group;
|
|
136
140
|
}
|
|
137
141
|
|
|
138
142
|
setProjectRoot(projectRoot: string): void {
|
|
@@ -162,7 +166,7 @@ export class TaskOverlay {
|
|
|
162
166
|
this.snapshot = { nodes: [], rootIds: [] };
|
|
163
167
|
}
|
|
164
168
|
try {
|
|
165
|
-
this.
|
|
169
|
+
this.widgetGroup?.requestUpdate();
|
|
166
170
|
} catch {
|
|
167
171
|
// A rendering bug must not crash the extension host over a best-effort status widget.
|
|
168
172
|
}
|
|
@@ -183,43 +187,15 @@ export class TaskOverlay {
|
|
|
183
187
|
});
|
|
184
188
|
}
|
|
185
189
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (this.snapshot.nodes.length === 0) {
|
|
191
|
-
if (this.registered) {
|
|
192
|
-
this.uiCtx.setWidget(WIDGET_KEY, undefined);
|
|
193
|
-
this.registered = false;
|
|
194
|
-
this.tui = undefined;
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (!this.registered) {
|
|
200
|
-
this.uiCtx.setWidget(
|
|
201
|
-
WIDGET_KEY,
|
|
202
|
-
(tui: any, theme: Theme) => {
|
|
203
|
-
this.tui = tui;
|
|
204
|
-
return {
|
|
205
|
-
render: (width: number) => this.renderLines(theme, width),
|
|
206
|
-
invalidate: () => {
|
|
207
|
-
// Theme changed — force re-registration
|
|
208
|
-
this.registered = false;
|
|
209
|
-
this.tui = undefined;
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
},
|
|
213
|
-
{ placement: "aboveEditor" },
|
|
214
|
-
);
|
|
215
|
-
this.registered = true;
|
|
216
|
-
} else {
|
|
217
|
-
this.tui?.requestRender?.();
|
|
218
|
-
}
|
|
190
|
+
/** Theme-free existence check -- PapyrusWidgetGroup's own eager (pre-paint) hide decision needs
|
|
191
|
+
* this without needing a theme, which is only ever available inside the widget's own render(width). */
|
|
192
|
+
hasOpenWork(): boolean {
|
|
193
|
+
return buildTaskWidgetProjection(this.snapshot).openTotal > 0;
|
|
219
194
|
}
|
|
220
195
|
|
|
221
|
-
|
|
222
|
-
|
|
196
|
+
/** undefined when there is no open work to show -- PapyrusWidgetGroup's own signal to omit this section entirely. */
|
|
197
|
+
buildSection(theme: Theme): WidgetSection | undefined {
|
|
198
|
+
return buildTaskWidgetSection(theme, buildTaskWidgetProjection(this.snapshot));
|
|
223
199
|
}
|
|
224
200
|
|
|
225
201
|
/**
|
|
@@ -240,17 +216,12 @@ export class TaskOverlay {
|
|
|
240
216
|
this.stopPolling();
|
|
241
217
|
this.pushChannel?.close();
|
|
242
218
|
this.pushChannel = undefined;
|
|
243
|
-
this.
|
|
244
|
-
this.registered = false;
|
|
245
|
-
this.tui = undefined;
|
|
246
|
-
this.uiCtx = undefined;
|
|
219
|
+
this.widgetGroup = undefined;
|
|
247
220
|
this.projectRoot = undefined;
|
|
248
221
|
this.sessionId = undefined;
|
|
249
222
|
}
|
|
250
223
|
}
|
|
251
224
|
|
|
252
|
-
const NOTE_WIDGET_KEY = "pi-papyrus-notes";
|
|
253
|
-
|
|
254
225
|
/**
|
|
255
226
|
* Deliberately simple, unlike TaskOverlay's tree: just an open-note count for this session's own
|
|
256
227
|
* CWD -- notes.list already scopes to project_root exactly (a note's projectRoot is fixed at
|
|
@@ -258,19 +229,13 @@ const NOTE_WIDGET_KEY = "pi-papyrus-notes";
|
|
|
258
229
|
* default.
|
|
259
230
|
*/
|
|
260
231
|
export class NoteOverlay {
|
|
261
|
-
private
|
|
262
|
-
private registered = false;
|
|
263
|
-
private tui: any | undefined;
|
|
232
|
+
private widgetGroup: PapyrusWidgetGroup | undefined;
|
|
264
233
|
private openCount = 0;
|
|
265
234
|
private projectRoot: string | undefined;
|
|
266
235
|
private readonly poll = new BoundedPoll();
|
|
267
236
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
this.uiCtx = ctx;
|
|
271
|
-
this.registered = false;
|
|
272
|
-
this.tui = undefined;
|
|
273
|
-
}
|
|
237
|
+
setWidgetGroup(group: PapyrusWidgetGroup): void {
|
|
238
|
+
this.widgetGroup = group;
|
|
274
239
|
}
|
|
275
240
|
|
|
276
241
|
setProjectRoot(projectRoot: string): void {
|
|
@@ -289,18 +254,80 @@ export class NoteOverlay {
|
|
|
289
254
|
this.openCount = 0;
|
|
290
255
|
}
|
|
291
256
|
try {
|
|
292
|
-
this.
|
|
257
|
+
this.widgetGroup?.requestUpdate();
|
|
293
258
|
} catch {
|
|
294
259
|
// A rendering bug must not crash the extension host over a best-effort status widget.
|
|
295
260
|
}
|
|
296
261
|
}
|
|
297
262
|
|
|
298
|
-
|
|
263
|
+
/** Theme-free existence check -- see TaskOverlay's own hasOpenWork() for why this needs to stay theme-free. */
|
|
264
|
+
hasOpenNotes(): boolean {
|
|
265
|
+
return this.openCount > 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** undefined when there are no open notes -- PapyrusWidgetGroup's own signal to omit this section entirely. */
|
|
269
|
+
buildSection(): WidgetSection | undefined {
|
|
270
|
+
return buildNoteWidgetSection(this.openCount);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
startPolling(intervalMs: number = NOTE_WIDGET_POLL_INTERVAL_MS): void {
|
|
274
|
+
this.poll.start(intervalMs, () => {
|
|
275
|
+
void this.refresh();
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
stopPolling(): void {
|
|
280
|
+
this.poll.stop();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
dispose(): void {
|
|
284
|
+
this.stopPolling();
|
|
285
|
+
this.widgetGroup = undefined;
|
|
286
|
+
this.projectRoot = undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const PAPYRUS_WIDGET_KEY = "pi-papyrus";
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Owns the ONE real aboveEditor widget registration for both TaskOverlay and NoteOverlay, composing
|
|
294
|
+
* their own current sections via renderWidgetSectionGroup -- "Papyrus" once, with "Tasks"/"Notes"
|
|
295
|
+
* as its own indented children (or side-by-side columns once the terminal is wide enough), instead
|
|
296
|
+
* of each overlay registering its own separate "Papyrus · <Label>" flat-header widget. Hidden
|
|
297
|
+
* entirely (fully unregistered, not just empty lines) only once NEITHER overlay has anything to
|
|
298
|
+
* show -- either overlay alone still renders its own section normally.
|
|
299
|
+
*/
|
|
300
|
+
export class PapyrusWidgetGroup {
|
|
301
|
+
private uiCtx: ExtensionUIContext | undefined;
|
|
302
|
+
private registered = false;
|
|
303
|
+
private tui: any | undefined;
|
|
304
|
+
private taskOverlay: TaskOverlay | undefined;
|
|
305
|
+
private noteOverlay: NoteOverlay | undefined;
|
|
306
|
+
|
|
307
|
+
setUI(ctx: ExtensionUIContext): void {
|
|
308
|
+
if (ctx !== this.uiCtx) {
|
|
309
|
+
this.uiCtx = ctx;
|
|
310
|
+
this.registered = false;
|
|
311
|
+
this.tui = undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
setOverlays(taskOverlay: TaskOverlay, noteOverlay: NoteOverlay): void {
|
|
316
|
+
this.taskOverlay = taskOverlay;
|
|
317
|
+
this.noteOverlay = noteOverlay;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Called by either overlay after its own refresh() -- re-renders (or registers/unregisters) the shared widget. */
|
|
321
|
+
requestUpdate(): void {
|
|
299
322
|
if (!this.uiCtx) return;
|
|
300
323
|
|
|
301
|
-
|
|
324
|
+
// Eager (pre-paint) hide check, theme-free -- matches both overlays' own prior convention of
|
|
325
|
+
// deciding this immediately after a data refresh, not waiting for Pi's own next repaint to
|
|
326
|
+
// notice via renderLines()'s own (still-present, defense-in-depth) empty check below.
|
|
327
|
+
const hasContent = (this.taskOverlay?.hasOpenWork() ?? false) || (this.noteOverlay?.hasOpenNotes() ?? false);
|
|
328
|
+
if (!hasContent) {
|
|
302
329
|
if (this.registered) {
|
|
303
|
-
this.uiCtx.setWidget(
|
|
330
|
+
this.uiCtx.setWidget(PAPYRUS_WIDGET_KEY, undefined);
|
|
304
331
|
this.registered = false;
|
|
305
332
|
this.tui = undefined;
|
|
306
333
|
}
|
|
@@ -309,12 +336,13 @@ export class NoteOverlay {
|
|
|
309
336
|
|
|
310
337
|
if (!this.registered) {
|
|
311
338
|
this.uiCtx.setWidget(
|
|
312
|
-
|
|
339
|
+
PAPYRUS_WIDGET_KEY,
|
|
313
340
|
(tui: any, theme: Theme) => {
|
|
314
341
|
this.tui = tui;
|
|
315
342
|
return {
|
|
316
|
-
render: (width: number) =>
|
|
343
|
+
render: (width: number) => this.renderLines(theme, width),
|
|
317
344
|
invalidate: () => {
|
|
345
|
+
// Theme changed — force re-registration
|
|
318
346
|
this.registered = false;
|
|
319
347
|
this.tui = undefined;
|
|
320
348
|
},
|
|
@@ -328,23 +356,34 @@ export class NoteOverlay {
|
|
|
328
356
|
}
|
|
329
357
|
}
|
|
330
358
|
|
|
331
|
-
|
|
332
|
-
this.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
359
|
+
private renderLines(theme: Theme, width: number): string[] {
|
|
360
|
+
const sections = [this.taskOverlay?.buildSection(theme), this.noteOverlay?.buildSection()].filter(
|
|
361
|
+
(section): section is WidgetSection => section !== undefined,
|
|
362
|
+
);
|
|
363
|
+
if (sections.length === 0) {
|
|
364
|
+
// Hide entirely (unregister), not just an empty-but-registered widget -- matches both
|
|
365
|
+
// overlays' own prior "nothing open" convention.
|
|
366
|
+
if (this.uiCtx && this.registered) {
|
|
367
|
+
this.uiCtx.setWidget(PAPYRUS_WIDGET_KEY, undefined);
|
|
368
|
+
this.registered = false;
|
|
369
|
+
this.tui = undefined;
|
|
370
|
+
}
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
return renderWidgetSectionGroup({
|
|
374
|
+
owner: vehicleWidgetOwner(PAPYRUS_VEHICLE_NAME),
|
|
375
|
+
sections,
|
|
376
|
+
ownerStyle: (s) => theme.fg("muted", s),
|
|
377
|
+
}).render(width);
|
|
339
378
|
}
|
|
340
379
|
|
|
341
380
|
dispose(): void {
|
|
342
|
-
this.
|
|
343
|
-
this.uiCtx?.setWidget(NOTE_WIDGET_KEY, undefined);
|
|
381
|
+
this.uiCtx?.setWidget(PAPYRUS_WIDGET_KEY, undefined);
|
|
344
382
|
this.registered = false;
|
|
345
383
|
this.tui = undefined;
|
|
346
384
|
this.uiCtx = undefined;
|
|
347
|
-
this.
|
|
385
|
+
this.taskOverlay = undefined;
|
|
386
|
+
this.noteOverlay = undefined;
|
|
348
387
|
}
|
|
349
388
|
}
|
|
350
389
|
|
|
@@ -639,6 +678,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
639
678
|
]);
|
|
640
679
|
let overlay: TaskOverlay | undefined;
|
|
641
680
|
let noteOverlay: NoteOverlay | undefined;
|
|
681
|
+
let widgetGroup: PapyrusWidgetGroup | undefined;
|
|
642
682
|
|
|
643
683
|
pi.registerCommand("tasks", {
|
|
644
684
|
description: "Browse and manage Papyrus tasks (interactive)",
|
|
@@ -728,16 +768,22 @@ export default async function (pi: ExtensionAPI) {
|
|
|
728
768
|
// keystrokes from the moment that tool call happens to begin, missing typing already in
|
|
729
769
|
// progress when it started (the exact case Discuss's typing-courtesy wait protects against).
|
|
730
770
|
ensureTypingCourtesyTracking(ctx.ui);
|
|
771
|
+
widgetGroup ??= new PapyrusWidgetGroup();
|
|
772
|
+
widgetGroup.setUI(ctx.ui);
|
|
773
|
+
|
|
731
774
|
overlay ??= new TaskOverlay();
|
|
732
|
-
overlay.
|
|
775
|
+
overlay.setWidgetGroup(widgetGroup);
|
|
733
776
|
overlay.setProjectRoot(ctx.cwd);
|
|
734
777
|
overlay.setSessionId(ctx.sessionManager.getSessionId());
|
|
735
|
-
await overlay.refresh();
|
|
736
|
-
overlay.startPolling(TASK_WIDGET_POLL_INTERVAL_MS);
|
|
737
778
|
|
|
738
779
|
noteOverlay ??= new NoteOverlay();
|
|
739
|
-
noteOverlay.
|
|
780
|
+
noteOverlay.setWidgetGroup(widgetGroup);
|
|
740
781
|
noteOverlay.setProjectRoot(ctx.cwd);
|
|
782
|
+
|
|
783
|
+
widgetGroup.setOverlays(overlay, noteOverlay);
|
|
784
|
+
|
|
785
|
+
await overlay.refresh();
|
|
786
|
+
overlay.startPolling(TASK_WIDGET_POLL_INTERVAL_MS);
|
|
741
787
|
await noteOverlay.refresh();
|
|
742
788
|
noteOverlay.startPolling(NOTE_WIDGET_POLL_INTERVAL_MS);
|
|
743
789
|
});
|
|
@@ -756,6 +802,8 @@ export default async function (pi: ExtensionAPI) {
|
|
|
756
802
|
overlay = undefined;
|
|
757
803
|
noteOverlay?.dispose();
|
|
758
804
|
noteOverlay = undefined;
|
|
805
|
+
widgetGroup?.dispose();
|
|
806
|
+
widgetGroup = undefined;
|
|
759
807
|
try {
|
|
760
808
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
761
809
|
await callService("session.release", { session_id: sessionId, ...sessionSecretField(sessionId) });
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { vehicleWidgetTitle } from "@danypops/vehicle-client-pi/widget-header";
|
|
3
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
1
|
+
import type { WidgetSection } from "malevich-tui-components";
|
|
5
2
|
|
|
6
|
-
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Notes has no body rows of its own -- the open count lives on the section's own label. undefined
|
|
5
|
+
* (hide the section entirely) at 0, matching TaskOverlay's own "nothing open" hiding rule.
|
|
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.
|
|
10
|
+
*/
|
|
11
|
+
export function buildNoteWidgetSection(openCount: number): WidgetSection | undefined {
|
|
12
|
+
if (openCount === 0) return undefined;
|
|
13
|
+
return { label: `Notes ${openCount}`, render: () => [] };
|
|
11
14
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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"],
|
|
@@ -24,7 +24,7 @@
|
|
|
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.31.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@danypops/pi-tui-harness": "^0.0.2",
|