@danypops/pi-papyrus 0.43.1 → 0.43.4
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/artifact-browser.ts +54 -31
- package/extension/src/artifact-detail-view.ts +52 -42
- package/extension/src/artifact-format.ts +11 -5
- package/extension/src/artifact-relationship-lines.ts +4 -5
- package/extension/src/beautiful-mermaid-renderer.ts +3 -5
- package/extension/src/context-budget.ts +8 -5
- package/extension/src/context-hub-contribution.ts +6 -2
- package/extension/src/context-injection-telemetry.ts +2 -2
- package/extension/src/discuss-ask-layout.ts +3 -1
- package/extension/src/discuss-ask-view.ts +437 -111
- package/extension/src/discuss.ts +64 -15
- package/extension/src/discussion-detail-view.ts +44 -22
- package/extension/src/docs.ts +3 -2
- package/extension/src/domain-tools.ts +79 -34
- package/extension/src/index.ts +170 -66
- package/extension/src/markdown.ts +3 -7
- package/extension/src/note-widget.ts +1 -1
- package/extension/src/notes.ts +3 -8
- package/extension/src/playbook-bridge.ts +17 -6
- package/extension/src/playbooks.ts +17 -7
- package/extension/src/rules.ts +5 -5
- package/extension/src/service-client.ts +23 -8
- package/extension/src/skill-catalog-footprint.ts +1 -1
- package/extension/src/task-detail-format.ts +9 -9
- package/extension/src/task-detail-view.ts +36 -29
- package/extension/src/task-focus-events.ts +3 -2
- package/extension/src/task-graph.ts +16 -12
- package/extension/src/task-presentation.ts +2 -6
- package/extension/src/task-widget.ts +12 -8
- package/extension/src/tasks.ts +148 -57
- package/extension/src/tool-rendering/artifact-card.ts +16 -4
- package/extension/src/tool-rendering/artifact-list.ts +23 -24
- package/extension/src/tool-rendering/index.ts +2 -6
- package/extension/src/tool-rendering/render-model.ts +96 -56
- package/extension/src/vehicle-artifact-renderers.ts +102 -0
- package/extension/src/vehicle-notes-client.ts +35 -7
- package/package.json +5 -5
package/extension/src/index.ts
CHANGED
|
@@ -8,40 +8,43 @@
|
|
|
8
8
|
* "Are we there yet?" — the agent sees its open work items.
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
-
import
|
|
12
|
-
import { Type } from "typebox";
|
|
13
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
11
|
+
import { CONTEXT_DEFAULT_RESERVE_TOKENS, CONTEXT_HUB_CONTRIBUTION_CHANNEL, CONTEXT_HUB_CONTRIBUTION_SCHEMA } from "@danypops/jittor";
|
|
14
12
|
import {
|
|
15
|
-
|
|
13
|
+
type Artifact,
|
|
14
|
+
type GateResult,
|
|
16
15
|
NOTE_LIST_MAX_LIMIT,
|
|
17
16
|
NOTE_WIDGET_POLL_INTERVAL_MS,
|
|
18
17
|
PAPYRUS_CONTEXT_INJECTION_CHANNEL,
|
|
19
18
|
TASK_DRIVER_MAX_TURNS,
|
|
20
19
|
TASK_DRIVER_MAX_UNCHANGED_TURNS,
|
|
21
20
|
TASK_WIDGET_POLL_INTERVAL_MS,
|
|
22
|
-
type Artifact,
|
|
23
|
-
type GateResult,
|
|
24
21
|
type TaskGraph,
|
|
25
22
|
type TaskStatus,
|
|
26
23
|
} from "@danypops/papyrus";
|
|
27
|
-
import { formatMetadata } from "./artifact-format.ts";
|
|
28
|
-
import { callService, subscribeTaskPushChannel } from "./service-client.ts";
|
|
29
24
|
import type { PushChannelClient } from "@danypops/vehicle-client/daemon-client";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
27
|
+
import { Type } from "typebox";
|
|
28
|
+
import {
|
|
29
|
+
ActiveTaskContinuation,
|
|
30
|
+
type ActiveTaskMarker,
|
|
31
|
+
automaticPauseReason,
|
|
32
|
+
shouldResumeFocusOnHumanInput,
|
|
33
|
+
} from "./active-task-continuation.ts";
|
|
34
|
+
import { formatMetadata } from "./artifact-format.ts";
|
|
32
35
|
import { BoundedPoll } from "./bounded-poll.ts";
|
|
33
|
-
import { renderNoteWidgetLines } from "./note-widget.ts";
|
|
34
|
-
import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
|
|
35
|
-
import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook-bridge.ts";
|
|
36
|
-
import { ActiveTaskContinuation, automaticPauseReason, shouldResumeFocusOnHumanInput, type ActiveTaskMarker } from "./active-task-continuation.ts";
|
|
37
|
-
import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task-widget.ts";
|
|
38
|
-
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
39
|
-
import { buildContextInjection } from "./context-injection-telemetry.ts";
|
|
40
36
|
import { buildTaskItemTree, computeContextBudget } from "./context-budget.ts";
|
|
41
37
|
import { PAPYRUS_CONTEXT_HUB_PRODUCER_NAME, papyrusContextSegment } from "./context-hub-contribution.ts";
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
38
|
+
import { buildContextInjection } from "./context-injection-telemetry.ts";
|
|
39
|
+
import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
|
|
40
|
+
import { registerDomainTools, resolveNameFields } from "./domain-tools.ts";
|
|
41
|
+
import { renderNoteWidgetLines } from "./note-widget.ts";
|
|
42
|
+
import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook-bridge.ts";
|
|
43
|
+
import { callService, subscribeTaskPushChannel } from "./service-client.ts";
|
|
44
44
|
import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
|
|
45
|
+
import { emitTaskFocusEvent, setTaskFocusEventBus } from "./task-focus-events.ts";
|
|
46
|
+
import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
|
|
47
|
+
import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task-widget.ts";
|
|
45
48
|
import { renderPapyrusToolCall, renderPapyrusToolResult } from "./tool-rendering/index.ts";
|
|
46
49
|
import {
|
|
47
50
|
createArtifactDetails,
|
|
@@ -50,6 +53,7 @@ import {
|
|
|
50
53
|
createModelContent,
|
|
51
54
|
createPreviewDetails,
|
|
52
55
|
} from "./tool-rendering/render-model.ts";
|
|
56
|
+
import { registerNotesVehicle } from "./vehicle-notes-client.ts";
|
|
53
57
|
|
|
54
58
|
function text(value: string, details: unknown = {}) {
|
|
55
59
|
const modelContent = createModelContent(value);
|
|
@@ -63,18 +67,25 @@ function artifactTextLabel(artifact: Artifact): string {
|
|
|
63
67
|
function artifactTextLines(artifacts: readonly Artifact[]): string[] {
|
|
64
68
|
const titleCounts = new Map<string, number>();
|
|
65
69
|
for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
|
|
66
|
-
return artifacts.map((artifact) =>
|
|
67
|
-
? `${artifactTextLabel(artifact)} (${artifact.id})`
|
|
68
|
-
|
|
70
|
+
return artifacts.map((artifact) =>
|
|
71
|
+
titleCounts.get(artifact.title)! > 1 ? `${artifactTextLabel(artifact)} (${artifact.id})` : artifactTextLabel(artifact),
|
|
72
|
+
);
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
/** Resolves graph protocol ids into model-facing names; equal titles retain ids only to disambiguate. */
|
|
72
76
|
async function artifactNamesById(ids: readonly string[]): Promise<Map<string, string>> {
|
|
73
77
|
const uniqueIds = [...new Set(ids)];
|
|
74
|
-
const artifacts = (
|
|
78
|
+
const artifacts = (
|
|
79
|
+
await Promise.all(uniqueIds.map((id) => callService<Record<string, unknown>, Artifact | null>("artifact.show", { id })))
|
|
80
|
+
).filter((artifact): artifact is Artifact => artifact !== null);
|
|
75
81
|
const titleCounts = new Map<string, number>();
|
|
76
82
|
for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
|
|
77
|
-
return new Map(
|
|
83
|
+
return new Map(
|
|
84
|
+
artifacts.map((artifact) => [
|
|
85
|
+
artifact.id,
|
|
86
|
+
titleCounts.get(artifact.title)! > 1 ? `${artifact.title} (${artifact.id})` : artifact.title,
|
|
87
|
+
]),
|
|
88
|
+
);
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
// ---------------------------------------------------------------------------
|
|
@@ -121,10 +132,14 @@ export class TaskOverlay {
|
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
134
|
|
|
124
|
-
setProjectRoot(projectRoot: string): void {
|
|
135
|
+
setProjectRoot(projectRoot: string): void {
|
|
136
|
+
this.projectRoot = projectRoot;
|
|
137
|
+
}
|
|
125
138
|
// Scopes the widget's "active" glyph to this Pi session's own Focus, so a second
|
|
126
139
|
// concurrent agent's focused task never shows as active in this session's widget.
|
|
127
|
-
setSessionId(sessionId: string): void {
|
|
140
|
+
setSessionId(sessionId: string): void {
|
|
141
|
+
this.sessionId = sessionId;
|
|
142
|
+
}
|
|
128
143
|
|
|
129
144
|
/**
|
|
130
145
|
* Never throws: called from several pi.on(...) handlers, some of which (session_compact,
|
|
@@ -135,7 +150,11 @@ export class TaskOverlay {
|
|
|
135
150
|
async refresh(): Promise<void> {
|
|
136
151
|
if (!this.projectRoot) return;
|
|
137
152
|
try {
|
|
138
|
-
this.snapshot = await callService<Record<string, unknown>, TaskGraph>("tasks.graph", {
|
|
153
|
+
this.snapshot = await callService<Record<string, unknown>, TaskGraph>("tasks.graph", {
|
|
154
|
+
limit: 500,
|
|
155
|
+
project_root: this.projectRoot,
|
|
156
|
+
session_id: this.sessionId,
|
|
157
|
+
});
|
|
139
158
|
} catch {
|
|
140
159
|
this.snapshot = { nodes: [], rootIds: [] };
|
|
141
160
|
}
|
|
@@ -156,7 +175,9 @@ export class TaskOverlay {
|
|
|
156
175
|
*/
|
|
157
176
|
private ensurePushChannel(): void {
|
|
158
177
|
if (this.pushChannel && this.pushChannel.state() !== "closed") return;
|
|
159
|
-
this.pushChannel = subscribeTaskPushChannel(() => {
|
|
178
|
+
this.pushChannel = subscribeTaskPushChannel(() => {
|
|
179
|
+
void this.refresh();
|
|
180
|
+
});
|
|
160
181
|
}
|
|
161
182
|
|
|
162
183
|
private render(): void {
|
|
@@ -203,7 +224,9 @@ export class TaskOverlay {
|
|
|
203
224
|
* a second concurrent Pi session against the same daemon.
|
|
204
225
|
*/
|
|
205
226
|
startPolling(intervalMs: number = TASK_WIDGET_POLL_INTERVAL_MS): void {
|
|
206
|
-
this.poll.start(intervalMs, () => {
|
|
227
|
+
this.poll.start(intervalMs, () => {
|
|
228
|
+
void this.refresh();
|
|
229
|
+
});
|
|
207
230
|
}
|
|
208
231
|
|
|
209
232
|
stopPolling(): void {
|
|
@@ -247,12 +270,17 @@ export class NoteOverlay {
|
|
|
247
270
|
}
|
|
248
271
|
}
|
|
249
272
|
|
|
250
|
-
setProjectRoot(projectRoot: string): void {
|
|
273
|
+
setProjectRoot(projectRoot: string): void {
|
|
274
|
+
this.projectRoot = projectRoot;
|
|
275
|
+
}
|
|
251
276
|
|
|
252
277
|
async refresh(): Promise<void> {
|
|
253
278
|
if (!this.projectRoot) return;
|
|
254
279
|
try {
|
|
255
|
-
const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", {
|
|
280
|
+
const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", {
|
|
281
|
+
project_root: this.projectRoot,
|
|
282
|
+
limit: NOTE_LIST_MAX_LIMIT,
|
|
283
|
+
});
|
|
256
284
|
this.openCount = rows.length;
|
|
257
285
|
} catch {
|
|
258
286
|
this.openCount = 0;
|
|
@@ -298,7 +326,9 @@ export class NoteOverlay {
|
|
|
298
326
|
}
|
|
299
327
|
|
|
300
328
|
startPolling(intervalMs: number = NOTE_WIDGET_POLL_INTERVAL_MS): void {
|
|
301
|
-
this.poll.start(intervalMs, () => {
|
|
329
|
+
this.poll.start(intervalMs, () => {
|
|
330
|
+
void this.refresh();
|
|
331
|
+
});
|
|
302
332
|
}
|
|
303
333
|
|
|
304
334
|
stopPolling(): void {
|
|
@@ -346,17 +376,23 @@ export default async function (pi: ExtensionAPI) {
|
|
|
346
376
|
if (isLiveAskPending()) return;
|
|
347
377
|
try {
|
|
348
378
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
349
|
-
const active = await callService<Record<string, unknown>, ActiveTaskMarker | null>("tasks.active", {
|
|
379
|
+
const active = await callService<Record<string, unknown>, ActiveTaskMarker | null>("tasks.active", {
|
|
380
|
+
project_root: ctx.cwd,
|
|
381
|
+
session_id: sessionId,
|
|
382
|
+
});
|
|
350
383
|
const decision = taskContinuation.evaluate(active, {
|
|
351
384
|
idle: ctx.isIdle(),
|
|
352
385
|
pendingMessages: ctx.hasPendingMessages(),
|
|
353
386
|
});
|
|
354
387
|
if (decision.action === "continue" && decision.prompt) {
|
|
355
|
-
pi.sendMessage(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
388
|
+
pi.sendMessage(
|
|
389
|
+
{
|
|
390
|
+
customType: "papyrus-task-continuation",
|
|
391
|
+
content: decision.prompt,
|
|
392
|
+
display: false,
|
|
393
|
+
},
|
|
394
|
+
{ triggerTurn: true, deliverAs: "nextTurn" },
|
|
395
|
+
);
|
|
360
396
|
} else if (decision.action === "pause") {
|
|
361
397
|
const paused = await callService<Record<string, unknown>, { artifact: Artifact; status: string }>("tasks.pause", {
|
|
362
398
|
actor: "system",
|
|
@@ -415,8 +451,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
415
451
|
text: Type.Optional(Type.String({ description: "substring across title and body" })),
|
|
416
452
|
limit: Type.Optional(Type.Number()),
|
|
417
453
|
}),
|
|
418
|
-
renderCall(args, theme) {
|
|
419
|
-
|
|
454
|
+
renderCall(args, theme) {
|
|
455
|
+
return renderPapyrusToolCall("Query artifacts", args, theme);
|
|
456
|
+
},
|
|
457
|
+
renderResult(result, options, theme, context) {
|
|
458
|
+
return renderPapyrusToolResult(result, options, theme, context);
|
|
459
|
+
},
|
|
420
460
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
421
461
|
try {
|
|
422
462
|
const rows = await callService<Record<string, unknown>, Artifact[]>("artifact.query", { ...params, limit: params.limit ?? 50 });
|
|
@@ -456,8 +496,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
456
496
|
since: Type.Optional(Type.String({ description: "history: RFC3339 lower bound" })),
|
|
457
497
|
limit: Type.Optional(Type.Number({ description: "history: bounded page size" })),
|
|
458
498
|
}),
|
|
459
|
-
renderCall(args, theme) {
|
|
460
|
-
|
|
499
|
+
renderCall(args, theme) {
|
|
500
|
+
return renderPapyrusToolCall("Artifact graph", args, theme);
|
|
501
|
+
},
|
|
502
|
+
renderResult(result, options, theme, context) {
|
|
503
|
+
return renderPapyrusToolResult(result, options, theme, context);
|
|
504
|
+
},
|
|
461
505
|
async execute(_id, rawParams, _signal, _onUpdate, _ctx) {
|
|
462
506
|
try {
|
|
463
507
|
const params: Record<string, unknown> = { ...rawParams };
|
|
@@ -475,7 +519,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
475
519
|
return text(output, createPreviewDetails("graph.link", "Artifact relationship", output));
|
|
476
520
|
}
|
|
477
521
|
if (params.action === "unlink") {
|
|
478
|
-
const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", {
|
|
522
|
+
const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", {
|
|
523
|
+
from: params.from as string,
|
|
524
|
+
relation: params.relation as string,
|
|
525
|
+
to: params.to as string,
|
|
526
|
+
});
|
|
479
527
|
const names = await artifactNamesById([params.from as string, params.to as string]);
|
|
480
528
|
const relationship = `"${names.get(params.from as string) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to as string) ?? "unknown artifact"}"`;
|
|
481
529
|
const output = result.removed ? `Unlinked ${relationship}` : `No such relationship: ${relationship}`;
|
|
@@ -505,12 +553,22 @@ export default async function (pi: ExtensionAPI) {
|
|
|
505
553
|
}
|
|
506
554
|
if (params.action === "history") {
|
|
507
555
|
const page = await callService<Record<string, unknown>, { events: Array<Record<string, unknown>> }>("graph.history", {
|
|
508
|
-
id: params.id,
|
|
556
|
+
id: params.id,
|
|
557
|
+
actor: params.actor,
|
|
558
|
+
session_id: params.session_id,
|
|
559
|
+
since: params.since,
|
|
560
|
+
limit: params.limit,
|
|
509
561
|
});
|
|
510
|
-
if (page.events.length === 0)
|
|
511
|
-
|
|
562
|
+
if (page.events.length === 0)
|
|
563
|
+
return text("No recorded events.", createPreviewDetails("graph.history", "Mutation event log", "No recorded events."));
|
|
564
|
+
const eventIds = page.events.map((event) => event.artifactId).filter((id): id is string => typeof id === "string");
|
|
512
565
|
const names = await artifactNamesById(eventIds);
|
|
513
|
-
const output = page.events
|
|
566
|
+
const output = page.events
|
|
567
|
+
.map(
|
|
568
|
+
(event) =>
|
|
569
|
+
`${event.occurredAt} "${typeof event.artifactId === "string" ? (names.get(event.artifactId) ?? "unknown artifact") : "unknown artifact"}" ${event.type} · ${event.actor}/${event.source}`,
|
|
570
|
+
)
|
|
571
|
+
.join("\n");
|
|
514
572
|
return text(output, createPreviewDetails("graph.history", "Mutation event log", output));
|
|
515
573
|
}
|
|
516
574
|
throw new Error(`unknown action: ${params.action}; use link, tree, status, or history`);
|
|
@@ -530,8 +588,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
530
588
|
depth: Type.Optional(Type.Number({ description: "edge traversal depth" })),
|
|
531
589
|
max_nodes: Type.Optional(Type.Number({ description: "maximum traversed nodes" })),
|
|
532
590
|
}),
|
|
533
|
-
renderCall(args, theme) {
|
|
534
|
-
|
|
591
|
+
renderCall(args, theme) {
|
|
592
|
+
return renderPapyrusToolCall("Show artifact", args, theme);
|
|
593
|
+
},
|
|
594
|
+
renderResult(result, options, theme, context) {
|
|
595
|
+
return renderPapyrusToolResult(result, options, theme, context);
|
|
596
|
+
},
|
|
535
597
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
536
598
|
try {
|
|
537
599
|
const a = await callService<Record<string, unknown>, Artifact | null>("artifact.show", {
|
|
@@ -543,7 +605,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
543
605
|
if (!a) throw new Error(`artifact ${params.id} not found`);
|
|
544
606
|
let out = `${artifactTextLabel(a)}\n\n${a.body}`;
|
|
545
607
|
if (Object.keys(a.extra).length > 0) {
|
|
546
|
-
out += `\n\nMetadata:\n${formatMetadata(a.extra)
|
|
608
|
+
out += `\n\nMetadata:\n${formatMetadata(a.extra)
|
|
609
|
+
.map((line) => ` ${line}`)
|
|
610
|
+
.join("\n")}`;
|
|
547
611
|
}
|
|
548
612
|
if (a.edges?.length) {
|
|
549
613
|
const names = await artifactNamesById(a.edges.flatMap((edge) => [edge.from, edge.to]));
|
|
@@ -585,7 +649,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
585
649
|
});
|
|
586
650
|
pi.registerCommand("docs", {
|
|
587
651
|
description: "Browse and manage Papyrus documents (interactive)",
|
|
588
|
-
handler: async (_args, ctx) => {
|
|
652
|
+
handler: async (_args, ctx) => {
|
|
653
|
+
await docsModule.showDocs(ctx);
|
|
654
|
+
},
|
|
589
655
|
});
|
|
590
656
|
pi.registerCommand("note", {
|
|
591
657
|
description: "Capture a deferred request directly in Papyrus",
|
|
@@ -604,20 +670,29 @@ export default async function (pi: ExtensionAPI) {
|
|
|
604
670
|
});
|
|
605
671
|
pi.registerCommand("rules", {
|
|
606
672
|
description: "Browse, preview, and toggle Papyrus rules (interactive)",
|
|
607
|
-
handler: async (_args, ctx) => {
|
|
673
|
+
handler: async (_args, ctx) => {
|
|
674
|
+
await rulesModule.showRules(ctx);
|
|
675
|
+
},
|
|
608
676
|
});
|
|
609
677
|
pi.registerCommand("playbooks", {
|
|
610
678
|
description: "Browse, edit, and invoke Papyrus playbooks -- trigger/steps guidance an agent reads and follows (interactive)",
|
|
611
|
-
handler: async (_args, ctx) => {
|
|
679
|
+
handler: async (_args, ctx) => {
|
|
680
|
+
await playbooksModule.showPlaybooks(ctx);
|
|
681
|
+
},
|
|
612
682
|
});
|
|
613
683
|
pi.registerCommand("playbook", {
|
|
614
|
-
description:
|
|
684
|
+
description:
|
|
685
|
+
"Open one Papyrus playbook directly by name (tab-completes active playbook titles) and place its invocation in the editor; no argument opens the full /playbooks browser instead",
|
|
615
686
|
getArgumentCompletions: (argumentPrefix) => playbooksModule.playbookArgumentCompletions(argumentPrefix),
|
|
616
|
-
handler: async (args, ctx) => {
|
|
687
|
+
handler: async (args, ctx) => {
|
|
688
|
+
await playbooksModule.openPlaybookByName(args, ctx);
|
|
689
|
+
},
|
|
617
690
|
});
|
|
618
691
|
pi.registerCommand("discuss", {
|
|
619
692
|
description: "Browse Papyrus Discussions and reply, defer, resume, settle, or block/unblock a task (interactive)",
|
|
620
|
-
handler: async (_args, ctx) => {
|
|
693
|
+
handler: async (_args, ctx) => {
|
|
694
|
+
await discussModule.showDiscussions(ctx);
|
|
695
|
+
},
|
|
621
696
|
});
|
|
622
697
|
|
|
623
698
|
// ── Task widget (TodoOverlay pattern: factory form, requestRender) ──
|
|
@@ -643,7 +718,9 @@ export default async function (pi: ExtensionAPI) {
|
|
|
643
718
|
// not worth surfacing to the user.
|
|
644
719
|
try {
|
|
645
720
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
646
|
-
const { secret } = await callService<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
|
|
721
|
+
const { secret } = await callService<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
|
|
722
|
+
session_id: sessionId,
|
|
723
|
+
});
|
|
647
724
|
cacheSessionSecret(sessionId, secret);
|
|
648
725
|
} catch {
|
|
649
726
|
// intentionally silent -- see comment above
|
|
@@ -667,9 +744,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
667
744
|
noteOverlay.startPolling(NOTE_WIDGET_POLL_INTERVAL_MS);
|
|
668
745
|
});
|
|
669
746
|
|
|
670
|
-
pi.on("session_before_compact", () => {
|
|
671
|
-
|
|
672
|
-
|
|
747
|
+
pi.on("session_before_compact", () => {
|
|
748
|
+
taskContinuation.onCompaction();
|
|
749
|
+
});
|
|
750
|
+
pi.on("session_compact", async () => {
|
|
751
|
+
await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]);
|
|
752
|
+
});
|
|
753
|
+
pi.on("session_tree", async () => {
|
|
754
|
+
await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]);
|
|
755
|
+
});
|
|
673
756
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
674
757
|
overlay?.dispose();
|
|
675
758
|
overlay = undefined;
|
|
@@ -705,16 +788,27 @@ export default async function (pi: ExtensionAPI) {
|
|
|
705
788
|
taskContinuation.onHumanInput();
|
|
706
789
|
try {
|
|
707
790
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
708
|
-
const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string; pauseReason?: string } | null>(
|
|
791
|
+
const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string; pauseReason?: string } | null>(
|
|
792
|
+
"tasks.focused",
|
|
793
|
+
{ session_id: sessionId },
|
|
794
|
+
);
|
|
709
795
|
if (focus && shouldResumeFocusOnHumanInput(focus.status, focus.pauseReason)) {
|
|
710
|
-
await callService("tasks.unpause", {
|
|
796
|
+
await callService("tasks.unpause", {
|
|
797
|
+
actor: "system",
|
|
798
|
+
source: "task-continuation",
|
|
799
|
+
reason: "human input resumed automatic task continuation",
|
|
800
|
+
session_id: sessionId,
|
|
801
|
+
...sessionSecretField(sessionId),
|
|
802
|
+
});
|
|
711
803
|
emitTaskFocusEvent({ taskId: focus.artifact.id, sessionId, status: "unpaused" });
|
|
712
804
|
}
|
|
713
805
|
} catch {
|
|
714
806
|
// The daemon may be unavailable during startup, reload, or shutdown.
|
|
715
807
|
}
|
|
716
808
|
});
|
|
717
|
-
pi.on("agent_start", () => {
|
|
809
|
+
pi.on("agent_start", () => {
|
|
810
|
+
taskContinuation.onAgentStart();
|
|
811
|
+
});
|
|
718
812
|
pi.on("agent_settled", async (_event, ctx) => {
|
|
719
813
|
await driveActiveTasks(ctx);
|
|
720
814
|
await logSessionContextSnapshot(ctx);
|
|
@@ -729,9 +823,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
729
823
|
try {
|
|
730
824
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
731
825
|
const [rules, playbooks, summary, taskGraph] = await Promise.all([
|
|
732
|
-
callService<Record<string, unknown>, Array<Pick<Artifact, "id" | "title" | "body" | "extra">>>("rules.injectable", {
|
|
733
|
-
|
|
734
|
-
|
|
826
|
+
callService<Record<string, unknown>, Array<Pick<Artifact, "id" | "title" | "body" | "extra">>>("rules.injectable", {
|
|
827
|
+
project_root: ctx.cwd,
|
|
828
|
+
session_id: sessionId,
|
|
829
|
+
}),
|
|
830
|
+
callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "extra">>>("playbooks.list", {
|
|
831
|
+
status: "active",
|
|
832
|
+
limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS,
|
|
833
|
+
}),
|
|
834
|
+
callService<Record<string, unknown>, string | null>("tasks.context", {
|
|
835
|
+
project_root: ctx.cwd,
|
|
836
|
+
session_id: sessionId,
|
|
837
|
+
verbosity: "summary",
|
|
838
|
+
}),
|
|
735
839
|
callService<Record<string, unknown>, TaskGraph>("tasks.graph", { project_root: ctx.cwd, session_id: sessionId }),
|
|
736
840
|
]);
|
|
737
841
|
const injection = buildContextInjection({
|
|
@@ -49,12 +49,8 @@ export function renderMarkdownBody(
|
|
|
49
49
|
activeTheme: ActiveTheme,
|
|
50
50
|
activeMarkdownTheme: ActiveMarkdownTheme = activePiMarkdownTheme,
|
|
51
51
|
): string[] {
|
|
52
|
-
const markdown = new Markdown(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
0,
|
|
56
|
-
createPapyrusMarkdownTheme(activeTheme, activeMarkdownTheme),
|
|
57
|
-
{ color: (text) => activeTheme().fg("text", text) },
|
|
58
|
-
);
|
|
52
|
+
const markdown = new Markdown(body || "(no body)", 0, 0, createPapyrusMarkdownTheme(activeTheme, activeMarkdownTheme), {
|
|
53
|
+
color: (text) => activeTheme().fg("text", text),
|
|
54
|
+
});
|
|
59
55
|
return markdown.render(Math.max(1, width));
|
|
60
56
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
|
|
4
4
|
/** Hidden at 0, matching TaskOverlay's own "nothing open" hiding rule. */
|
|
5
5
|
export function renderNoteWidgetLines(theme: Theme, openCount: number, width: number): string[] {
|
package/extension/src/notes.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
+
import { type Artifact, NOTE_DISPOSITIONS, NOTE_LIST_MAX_LIMIT } from "@danypops/papyrus";
|
|
1
2
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { NOTE_DISPOSITIONS, NOTE_LIST_MAX_LIMIT, type Artifact } from "@danypops/papyrus";
|
|
3
3
|
import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
|
|
4
4
|
import { NOTE_STATUS_PRESENTATION } from "./artifact-status-presentation.ts";
|
|
5
5
|
import { callService } from "./service-client.ts";
|
|
6
6
|
|
|
7
7
|
export function noteRowMeta(note: Artifact): string {
|
|
8
|
-
const history = Array.isArray(note.extra
|
|
8
|
+
const history = Array.isArray(note.extra.noteHistory) ? note.extra.noteHistory.length : 0;
|
|
9
9
|
return `${history} event${history === 1 ? "" : "s"}`;
|
|
10
10
|
}
|
|
11
11
|
|
|
@@ -52,12 +52,7 @@ export async function showNotes(ctx: ExtensionCommandContext): Promise<void> {
|
|
|
52
52
|
statusOrder: ["draft", "active", "archived"],
|
|
53
53
|
presentation: NOTE_STATUS_PRESENTATION,
|
|
54
54
|
rowMeta: noteRowMeta,
|
|
55
|
-
actions: (note) => [
|
|
56
|
-
"Show details",
|
|
57
|
-
...(note.status === "draft" ? ["Consume"] : []),
|
|
58
|
-
"Promote",
|
|
59
|
-
"Archive",
|
|
60
|
-
],
|
|
55
|
+
actions: (note) => ["Show details", ...(note.status === "draft" ? ["Consume"] : []), "Promote", "Archive"],
|
|
61
56
|
handleAction: async (choice, note, commandCtx) => {
|
|
62
57
|
if (choice === "Show details") {
|
|
63
58
|
await showArtifactDetails(commandCtx, note.id, "notes.show", { project_root: commandCtx.cwd });
|
|
@@ -17,14 +17,19 @@
|
|
|
17
17
|
* time rather than baking in stale content, so even a lingering stale name fails cleanly with a
|
|
18
18
|
* real error instead of running deleted content.
|
|
19
19
|
*/
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
import type { Artifact } from "@danypops/papyrus";
|
|
22
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
22
23
|
import { callService } from "./service-client.ts";
|
|
23
24
|
|
|
24
25
|
export const PLAYBOOK_BRIDGE_MAX_PLAYBOOKS = 100;
|
|
25
26
|
|
|
26
27
|
function slugify(title: string): string {
|
|
27
|
-
const slug = title
|
|
28
|
+
const slug = title
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
31
|
+
.replace(/^-+|-+$/g, "")
|
|
32
|
+
.slice(0, 64);
|
|
28
33
|
return slug.length > 0 ? slug : "playbook";
|
|
29
34
|
}
|
|
30
35
|
|
|
@@ -44,7 +49,7 @@ export function playbookCommandName(title: string): string {
|
|
|
44
49
|
* already work this way).
|
|
45
50
|
*/
|
|
46
51
|
export function playbookInjectionPreview(playbook: Pick<Artifact, "title" | "extra">): string {
|
|
47
|
-
const trigger = typeof playbook.extra
|
|
52
|
+
const trigger = typeof playbook.extra.trigger === "string" ? playbook.extra.trigger : "manual invocation";
|
|
48
53
|
return `• ${playbook.title} (when: ${trigger})`;
|
|
49
54
|
}
|
|
50
55
|
|
|
@@ -56,7 +61,7 @@ export async function planPlaybookCommandRegistrations(): Promise<Array<{ name:
|
|
|
56
61
|
let name = playbookCommandName(playbook.title);
|
|
57
62
|
if (usedNames.has(name)) name = `${name}-${playbook.id.slice(0, 8)}`; // a real title collision, not the common case
|
|
58
63
|
usedNames.add(name);
|
|
59
|
-
const trigger = typeof playbook.extra
|
|
64
|
+
const trigger = typeof playbook.extra.trigger === "string" ? playbook.extra.trigger : "manual invocation";
|
|
60
65
|
return { name, id: playbook.id, title: playbook.title, trigger };
|
|
61
66
|
});
|
|
62
67
|
}
|
|
@@ -77,7 +82,10 @@ export function registerPlaybookBridge(pi: ExtensionAPI): void {
|
|
|
77
82
|
// returns rendered text to drop into the editor (that's playbooks.preview
|
|
78
83
|
// now). The editor gets a short kickoff prompt instead; the actual step
|
|
79
84
|
// content surfaces via the normal Task Focus system-prompt pointer.
|
|
80
|
-
const invocation = await callService<Record<string, unknown>, { entryTaskId?: string; missingArguments?: string[] }>(
|
|
85
|
+
const invocation = await callService<Record<string, unknown>, { entryTaskId?: string; missingArguments?: string[] }>(
|
|
86
|
+
"playbooks.invoke",
|
|
87
|
+
{ id },
|
|
88
|
+
);
|
|
81
89
|
if (invocation.missingArguments) {
|
|
82
90
|
ctx.ui.notify(`"${title}" needs: ${invocation.missingArguments.join(", ")}`, "error");
|
|
83
91
|
return;
|
|
@@ -95,5 +103,8 @@ export function registerPlaybookBridge(pi: ExtensionAPI): void {
|
|
|
95
103
|
// "no new/updated playbook commands this cycle", not a broken session start.
|
|
96
104
|
}
|
|
97
105
|
};
|
|
98
|
-
pi.on("resources_discover", async () => {
|
|
106
|
+
pi.on("resources_discover", async () => {
|
|
107
|
+
await refresh();
|
|
108
|
+
return {};
|
|
109
|
+
});
|
|
99
110
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
1
|
import type { Artifact } from "@danypops/papyrus";
|
|
2
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
4
4
|
import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
|
|
5
5
|
import { PLAYBOOK_STATUS_PRESENTATION } from "./artifact-status-presentation.ts";
|
|
6
6
|
import { matchArtifactByName } from "./domain-tools.ts";
|
|
@@ -9,7 +9,10 @@ import { callService } from "./service-client.ts";
|
|
|
9
9
|
const PLAYBOOK_COMPLETION_MAX_CANDIDATES = 100;
|
|
10
10
|
|
|
11
11
|
async function activePlaybooks(): Promise<Artifact[]> {
|
|
12
|
-
return callService<Record<string, unknown>, Artifact[]>("playbooks.list", {
|
|
12
|
+
return callService<Record<string, unknown>, Artifact[]>("playbooks.list", {
|
|
13
|
+
status: "active",
|
|
14
|
+
limit: PLAYBOOK_COMPLETION_MAX_CANDIDATES,
|
|
15
|
+
});
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
/** `/playbook <tab>` completions -- title-prefix match, since that's what a human actually types, not a full-text search of body content. */
|
|
@@ -20,7 +23,11 @@ export async function playbookArgumentCompletions(argumentPrefix: string): Promi
|
|
|
20
23
|
return rows
|
|
21
24
|
.filter((row) => row.title.toLowerCase().startsWith(needle))
|
|
22
25
|
.sort((a, b) => a.title.localeCompare(b.title))
|
|
23
|
-
.map((row) => ({
|
|
26
|
+
.map((row) => ({
|
|
27
|
+
value: row.title,
|
|
28
|
+
label: row.title,
|
|
29
|
+
description: typeof row.extra.trigger === "string" ? row.extra.trigger : undefined,
|
|
30
|
+
}));
|
|
24
31
|
} catch {
|
|
25
32
|
return null; // a Papyrus daemon hiccup degrades to "no suggestions", never breaks the command line
|
|
26
33
|
}
|
|
@@ -44,7 +51,10 @@ async function invokeAndReport(id: string, label: string, ctx: ExtensionCommandC
|
|
|
44
51
|
|
|
45
52
|
/** `/playbook <name>` (no args opens the full browser instead): resolves by exact title, then invokes it directly -- one step, not browse-then-select-then-invoke. */
|
|
46
53
|
export async function openPlaybookByName(name: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
47
|
-
if (!name.trim()) {
|
|
54
|
+
if (!name.trim()) {
|
|
55
|
+
await showPlaybooks(ctx);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
48
58
|
try {
|
|
49
59
|
const id = matchArtifactByName(await activePlaybooks(), name);
|
|
50
60
|
await invokeAndReport(id, name.trim(), ctx);
|
|
@@ -60,8 +70,8 @@ function strings(value: unknown): string[] {
|
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
export function playbookRowMeta(playbook: Artifact): string {
|
|
63
|
-
const trigger = typeof playbook.extra
|
|
64
|
-
const tools = strings(playbook.extra
|
|
73
|
+
const trigger = typeof playbook.extra.trigger === "string" ? `when ${playbook.extra.trigger}` : "manual invocation";
|
|
74
|
+
const tools = strings(playbook.extra.tools);
|
|
65
75
|
return [trigger, tools.join(", ")].filter(Boolean).join(" \u00b7 ");
|
|
66
76
|
}
|
|
67
77
|
|
package/extension/src/rules.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import type { Artifact } from "@danypops/papyrus";
|
|
2
|
+
import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
|
|
4
4
|
import { RULE_STATUS_PRESENTATION, severityColor } from "./artifact-status-presentation.ts";
|
|
5
5
|
import { callService } from "./service-client.ts";
|
|
6
6
|
|
|
7
7
|
export function ruleRowMeta(rule: Artifact, theme: Theme): string {
|
|
8
|
-
const severity = typeof rule.extra
|
|
8
|
+
const severity = typeof rule.extra.severity === "string" ? rule.extra.severity : "info";
|
|
9
9
|
const severityText = theme.fg(severityColor(severity), severity.toUpperCase());
|
|
10
|
-
const condition = typeof rule.extra
|
|
10
|
+
const condition = typeof rule.extra.condition === "string" ? `when ${rule.extra.condition}` : "always";
|
|
11
11
|
return `${severityText} · ${condition}`;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function ruleInjectionPreview(rule: Pick<Artifact, "title" | "body" | "extra">): string {
|
|
15
|
-
const condition = typeof rule.extra
|
|
16
|
-
const action = rule.body || (typeof rule.extra
|
|
15
|
+
const condition = typeof rule.extra.condition === "string" ? ` (when: ${rule.extra.condition})` : "";
|
|
16
|
+
const action = rule.body || (typeof rule.extra.action === "string" ? rule.extra.action : "");
|
|
17
17
|
return `• ${rule.title}${condition}\n ${action}`;
|
|
18
18
|
}
|
|
19
19
|
|