@danypops/papyrus 0.33.1 → 0.33.3
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/domain-tools.ts +8 -3
- package/extension/src/index.ts +40 -9
- package/package.json +1 -1
- package/src/constants.ts +6 -0
|
@@ -164,7 +164,7 @@ export function normalizeJsonEncodedField(params: Record<string, unknown>, key:
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/** Resolves every {nameKey -> idKey} pair present and not already satisfied by an explicit id, in place. */
|
|
167
|
-
async function resolveNameFields(
|
|
167
|
+
export async function resolveNameFields(
|
|
168
168
|
params: Record<string, unknown>,
|
|
169
169
|
fields: ReadonlyArray<{ nameKey: string; idKey: string; listOperation: OperationName; baseRequest: Record<string, unknown> }>,
|
|
170
170
|
): Promise<void> {
|
|
@@ -474,7 +474,7 @@ export function registerNotesTool(pi: ExtensionAPI): void {
|
|
|
474
474
|
pi.registerTool({
|
|
475
475
|
name: "notes",
|
|
476
476
|
label: "Notes",
|
|
477
|
-
description: "Deferred human-intent inbox. ACTIONS: capture, list, show, consume, promote, archive. Capture stores a request without creating work. Consume marks it considered. To promote, first create the resulting Task, Doc, Rule, or Skill through its domain tool, then link it with target_id. Archive requires an explicit disposition. PREFER `name` (the note's exact title) over `id` for show/consume/promote/archive --
|
|
477
|
+
description: "Deferred human-intent inbox. ACTIONS: capture, list, show, consume, promote, archive. Capture stores a request without creating work. Consume marks it considered. To promote, first create the resulting Task, Doc, Rule, or Skill through its domain tool, then link it with target_id (or target_name). Archive requires an explicit disposition. PREFER `name` (the note's exact title) over `id` for show/consume/promote/archive, and `target_name` over `target_id` for promote -- all are backend implementation details, resolved from name automatically (target_name searches across every kind, since a promotion target can be a task, doc, rule, or skill).",
|
|
478
478
|
parameters: Type.Object({
|
|
479
479
|
action: Type.String(),
|
|
480
480
|
id: Type.Optional(Type.String()),
|
|
@@ -485,6 +485,7 @@ export function registerNotesTool(pi: ExtensionAPI): void {
|
|
|
485
485
|
text: Type.Optional(Type.String()),
|
|
486
486
|
limit: Type.Optional(Type.Number()),
|
|
487
487
|
target_id: Type.Optional(Type.String()),
|
|
488
|
+
target_name: Type.Optional(Type.String()),
|
|
488
489
|
disposition: Type.Optional(Type.Union(NOTE_DISPOSITIONS.map((value) => Type.Literal(value)))),
|
|
489
490
|
reason: Type.Optional(Type.String()),
|
|
490
491
|
session_id: Type.Optional(Type.String()),
|
|
@@ -497,7 +498,11 @@ export function registerNotesTool(pi: ExtensionAPI): void {
|
|
|
497
498
|
const params: Record<string, unknown> = { ...rawParams };
|
|
498
499
|
const action = params.action;
|
|
499
500
|
const baseRequest = { project_root: params.project_root ?? ctx.cwd, actor: "agent", source: "notes-tool" };
|
|
500
|
-
await resolveNameFields(params, [
|
|
501
|
+
await resolveNameFields(params, [
|
|
502
|
+
{ nameKey: "name", idKey: "id", listOperation: "notes.list", baseRequest },
|
|
503
|
+
// Kind-agnostic: a promotion target can be a task, doc, rule, or skill, so this searches every kind rather than only notes.
|
|
504
|
+
{ nameKey: "target_name", idKey: "target_id", listOperation: "artifact.query", baseRequest },
|
|
505
|
+
]);
|
|
501
506
|
const request = { ...params, ...baseRequest };
|
|
502
507
|
if (action === "capture") {
|
|
503
508
|
const artifact = await callService<Record<string, unknown>, Artifact>("notes.capture", request);
|
package/extension/src/index.ts
CHANGED
|
@@ -16,12 +16,13 @@ import {
|
|
|
16
16
|
TASK_DRIVER_MAX_UNCHANGED_TURNS,
|
|
17
17
|
PAPYRUS_CONTEXT_INJECTION_CHANNEL,
|
|
18
18
|
CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
|
|
19
|
+
TASK_WIDGET_POLL_INTERVAL_MS,
|
|
19
20
|
} from "../../src/constants.ts";
|
|
20
21
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
21
22
|
import type { GateResult } from "../../src/domain/gate.ts";
|
|
22
23
|
import { formatMetadata } from "./artifact-format.ts";
|
|
23
24
|
import { callService } from "./service-client.ts";
|
|
24
|
-
import { registerDomainTools } from "./domain-tools.ts";
|
|
25
|
+
import { registerDomainTools, resolveNameFields } from "./domain-tools.ts";
|
|
25
26
|
import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
|
|
26
27
|
import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook-bridge.ts";
|
|
27
28
|
import type { TaskGraph, TaskStatus } from "../../src/task-service.ts";
|
|
@@ -102,6 +103,7 @@ export class TaskOverlay {
|
|
|
102
103
|
private snapshot: TaskGraph = { nodes: [], rootIds: [] };
|
|
103
104
|
private projectRoot: string | undefined;
|
|
104
105
|
private sessionId: string | undefined;
|
|
106
|
+
private pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
105
107
|
|
|
106
108
|
setUI(ctx: ExtensionUIContext): void {
|
|
107
109
|
if (ctx !== this.uiCtx) {
|
|
@@ -175,7 +177,24 @@ export class TaskOverlay {
|
|
|
175
177
|
return renderTaskWidgetLines(theme, buildTaskWidgetProjection(this.snapshot), width);
|
|
176
178
|
}
|
|
177
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Fallback for a Task mutation no event announces -- the CLI run directly from a shell, or
|
|
182
|
+
* a second concurrent Pi session against the same daemon. Idempotent: a second call is a
|
|
183
|
+
* no-op rather than starting a competing timer.
|
|
184
|
+
*/
|
|
185
|
+
startPolling(intervalMs: number = TASK_WIDGET_POLL_INTERVAL_MS): void {
|
|
186
|
+
if (this.pollTimer) return;
|
|
187
|
+
this.pollTimer = setInterval(() => { void this.refresh(); }, intervalMs);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
stopPolling(): void {
|
|
191
|
+
if (!this.pollTimer) return;
|
|
192
|
+
clearInterval(this.pollTimer);
|
|
193
|
+
this.pollTimer = undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
dispose(): void {
|
|
197
|
+
this.stopPolling();
|
|
179
198
|
this.uiCtx?.setWidget(WIDGET_KEY, undefined);
|
|
180
199
|
this.registered = false;
|
|
181
200
|
this.tui = undefined;
|
|
@@ -312,12 +331,15 @@ export default async function (pi: ExtensionAPI) {
|
|
|
312
331
|
"ACTIONS: link (from+relation+to), unlink (from+relation+to — idempotent, no error if already absent; for Task depends_on/contains prefer the tasks tool's undepend/uncontain), " +
|
|
313
332
|
"tree (id → bounded BFS subgraph), " +
|
|
314
333
|
"history (who did what, when — requires id, actor, or session_id). " +
|
|
315
|
-
"status (id+status) exists at the protocol level but is refused for every kind with its own lifecycle (Doc/Rule/Skill/Playbook/Task/Note all reject it) -- use that kind's own domain tool for status changes (docs.activate, rules.enable, tasks.start, etc), never this."
|
|
334
|
+
"status (id+status) exists at the protocol level but is refused for every kind with its own lifecycle (Doc/Rule/Skill/Playbook/Task/Note all reject it) -- use that kind's own domain tool for status changes (docs.activate, rules.enable, tasks.start, etc), never this. " +
|
|
335
|
+
"PREFER `from_name`/`to_name` over `from`/`to` for link/unlink -- both are backend implementation details, resolved from name automatically, searching across every kind since either end of an edge can be any artifact.",
|
|
316
336
|
parameters: Type.Object({
|
|
317
337
|
action: Type.String({ description: "link | unlink | tree | status | history" }),
|
|
318
338
|
from: Type.Optional(Type.String()),
|
|
339
|
+
from_name: Type.Optional(Type.String()),
|
|
319
340
|
relation: Type.Optional(Type.String()),
|
|
320
341
|
to: Type.Optional(Type.String()),
|
|
342
|
+
to_name: Type.Optional(Type.String()),
|
|
321
343
|
id: Type.Optional(Type.String()),
|
|
322
344
|
status: Type.Optional(Type.String()),
|
|
323
345
|
depth: Type.Optional(Type.Number({ description: "tree traversal depth; bounded by a hard ceiling" })),
|
|
@@ -329,18 +351,26 @@ export default async function (pi: ExtensionAPI) {
|
|
|
329
351
|
}),
|
|
330
352
|
renderCall(args, theme) { return renderPapyrusToolCall("Artifact graph", args, theme); },
|
|
331
353
|
renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
|
|
332
|
-
async execute(_id,
|
|
354
|
+
async execute(_id, rawParams, _signal, _onUpdate, _ctx) {
|
|
333
355
|
try {
|
|
356
|
+
const params: Record<string, unknown> = { ...rawParams };
|
|
357
|
+
if (params.action === "link" || params.action === "unlink") {
|
|
358
|
+
// Kind-agnostic: either end of an edge can be a task, doc, rule, skill, or playbook.
|
|
359
|
+
await resolveNameFields(params, [
|
|
360
|
+
{ nameKey: "from_name", idKey: "from", listOperation: "artifact.query", baseRequest: {} },
|
|
361
|
+
{ nameKey: "to_name", idKey: "to", listOperation: "artifact.query", baseRequest: {} },
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
334
364
|
if (params.action === "link") {
|
|
335
|
-
await callService("graph.link", { from: params.from
|
|
336
|
-
const names = await artifactNamesById([params.from
|
|
337
|
-
const output = `Linked "${names.get(params.from
|
|
365
|
+
await callService("graph.link", { from: params.from as string, relation: params.relation as string, to: params.to as string });
|
|
366
|
+
const names = await artifactNamesById([params.from as string, params.to as string]);
|
|
367
|
+
const output = `Linked "${names.get(params.from as string) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to as string) ?? "unknown artifact"}"`;
|
|
338
368
|
return text(output, createPreviewDetails("graph.link", "Artifact relationship", output));
|
|
339
369
|
}
|
|
340
370
|
if (params.action === "unlink") {
|
|
341
|
-
const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: params.from
|
|
342
|
-
const names = await artifactNamesById([params.from
|
|
343
|
-
const relationship = `"${names.get(params.from
|
|
371
|
+
const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: params.from as string, relation: params.relation as string, to: params.to as string });
|
|
372
|
+
const names = await artifactNamesById([params.from as string, params.to as string]);
|
|
373
|
+
const relationship = `"${names.get(params.from as string) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to as string) ?? "unknown artifact"}"`;
|
|
344
374
|
const output = result.removed ? `Unlinked ${relationship}` : `No such relationship: ${relationship}`;
|
|
345
375
|
return text(output, createPreviewDetails("graph.unlink", "Artifact relationship", output));
|
|
346
376
|
}
|
|
@@ -549,6 +579,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
549
579
|
overlay.setProjectRoot(ctx.cwd);
|
|
550
580
|
overlay.setSessionId(ctx.sessionManager.getSessionId());
|
|
551
581
|
await overlay.refresh();
|
|
582
|
+
overlay.startPolling(TASK_WIDGET_POLL_INTERVAL_MS);
|
|
552
583
|
});
|
|
553
584
|
|
|
554
585
|
pi.on("session_before_compact", () => { taskContinuation.onCompaction(); });
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -65,6 +65,12 @@ export const TASK_CONTEXT_CURRENT_LIMIT = 3;
|
|
|
65
65
|
export const TASK_CONTEXT_REJECTED_LIMIT = 3;
|
|
66
66
|
export const TASK_WIDGET_OPEN_LIMIT = 3;
|
|
67
67
|
export const TASK_DETAIL_MIN_VISIBLE_LINES = 8;
|
|
68
|
+
/**
|
|
69
|
+
* Event-triggered refresh (tool_execution_end, session_compact/tree) can't see a Task
|
|
70
|
+
* mutation from outside this Pi session -- another concurrent session, or the CLI run
|
|
71
|
+
* directly from a shell. This bounded poll is the fallback for exactly that gap.
|
|
72
|
+
*/
|
|
73
|
+
export const TASK_WIDGET_POLL_INTERVAL_MS = 20_000;
|
|
68
74
|
export const TASK_DETAIL_MAX_VISIBLE_LINES = 24;
|
|
69
75
|
export const TASK_DETAIL_RESERVED_ROWS = 8;
|
|
70
76
|
export const TASK_DETAIL_HORIZONTAL_PAN_COLUMNS = 4;
|