@danypops/papyrus 0.25.0 → 0.27.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.
@@ -43,7 +43,7 @@ function continuationPrompt(task: ActiveTaskMarker): string {
43
43
  "Reconcile its lifecycle, take the next concrete action, use tools, submit it for review when implementation effort is ready, and run gates plus checklist review before completion.",
44
44
  "Do not shrink the task's scope to whatever fits in this turn, and do not treat a status update or summary as a substitute for doing the work or as proof of completion.",
45
45
  "If something blocks progress, do not reject or pause on the first obstacle -- only after it genuinely recurs, and only when the task truly cannot proceed without external input.",
46
- `Active task: ${task.id}: ${task.title.slice(0, TITLE_LIMIT)}`,
46
+ `Active task: ${task.title.slice(0, TITLE_LIMIT)}`,
47
47
  ].join("\n");
48
48
  }
49
49
 
@@ -78,7 +78,7 @@ export async function showArtifactDetails(
78
78
  depth: DETAIL_GRAPH_DEPTH,
79
79
  max_nodes: DETAIL_GRAPH_NODES,
80
80
  });
81
- if (!artifact) { ctx.ui.notify(`Artifact ${id} not found`, "error"); return; }
81
+ if (!artifact) { ctx.ui.notify("Artifact not found", "error"); return; }
82
82
  await showArtifactDetailView(ctx, artifact);
83
83
  } catch (error) {
84
84
  ctx.ui.notify(`Show details failed: ${error instanceof Error ? error.message : error}`, "error");
@@ -92,7 +92,7 @@ export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: str
92
92
  if (!relation) return;
93
93
  try {
94
94
  await callService("graph.link", { from: fromId, relation, to: target });
95
- ctx.ui.notify(`Linked ${fromId} --${relation}--> ${target}`, "info");
95
+ ctx.ui.notify(`Artifacts linked via ${relation}`, "info");
96
96
  } catch (error) {
97
97
  ctx.ui.notify(`Link failed: ${error instanceof Error ? error.message : error}`, "error");
98
98
  }
@@ -101,8 +101,8 @@ export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: str
101
101
  export async function setArtifactStatus(ctx: ExtensionCommandContext, id: string, status: string): Promise<void> {
102
102
  try {
103
103
  const artifact = await callService<Record<string, unknown>, Artifact | null>("graph.status", { id, status });
104
- if (!artifact) { ctx.ui.notify(`Artifact ${id} not found`, "error"); return; }
105
- ctx.ui.notify(`${artifact.id} → [${artifact.status}]`, "info");
104
+ if (!artifact) { ctx.ui.notify("Artifact not found", "error"); return; }
105
+ ctx.ui.notify(`${artifact.title} → [${artifact.status}]`, "info");
106
106
  } catch (error) {
107
107
  ctx.ui.notify(`Status change failed: ${error instanceof Error ? error.message : error}`, "error");
108
108
  }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * discuss-ask-layout.ts — row layout for the searchable single-select list used by Discuss's
3
+ * live:true ask UI (discuss-ask-view.ts). Wraps long option titles/descriptions to a target
4
+ * width and returns flat annotated rows, windowed around the current selection when the full
5
+ * list would overflow the available rows.
6
+ *
7
+ * Adapted from pi-ask-user's single-select-layout.ts (MIT, Copyright (c) 2026 Enzo Lucchesi --
8
+ * see THIRD_PARTY_LICENSES.md) so Discuss owns this UI directly instead of depending on that
9
+ * package at runtime.
10
+ */
11
+
12
+ export interface AskOption {
13
+ title: string;
14
+ description?: string;
15
+ }
16
+
17
+ export interface AnnotatedRow {
18
+ line: string;
19
+ selected: boolean;
20
+ }
21
+
22
+ export interface RenderSingleSelectRowsParams {
23
+ options: AskOption[];
24
+ selectedIndex: number;
25
+ width: number;
26
+ allowFreeform: boolean;
27
+ allowComment?: boolean;
28
+ commentEnabled?: boolean;
29
+ maxRows?: number;
30
+ hideDescriptions?: boolean;
31
+ }
32
+
33
+ function wrapText(text: string, width: number): string[] {
34
+ const normalized = text.replace(/\s+/g, " ").trim();
35
+ if (!normalized) return [""];
36
+ if (width <= 1) return normalized.split("");
37
+
38
+ const words = normalized.split(" ");
39
+ const lines: string[] = [];
40
+ let current = "";
41
+
42
+ for (const word of words) {
43
+ if (!current) {
44
+ if (word.length <= width) {
45
+ current = word;
46
+ } else {
47
+ for (let i = 0; i < word.length; i += width) {
48
+ lines.push(word.slice(i, i + width));
49
+ }
50
+ }
51
+ continue;
52
+ }
53
+
54
+ const candidate = `${current} ${word}`;
55
+ if (candidate.length <= width) {
56
+ current = candidate;
57
+ continue;
58
+ }
59
+
60
+ lines.push(current);
61
+ if (word.length <= width) {
62
+ current = word;
63
+ } else {
64
+ current = "";
65
+ for (let i = 0; i < word.length; i += width) {
66
+ const chunk = word.slice(i, i + width);
67
+ if (chunk.length === width || i + width < word.length) lines.push(chunk);
68
+ else current = chunk;
69
+ }
70
+ }
71
+ }
72
+
73
+ if (current) lines.push(current);
74
+ return lines;
75
+ }
76
+
77
+ function padLine(prefix: string, content: string): string {
78
+ return `${prefix}${content}`.trimEnd();
79
+ }
80
+
81
+ interface ItemBlock {
82
+ itemIndex: number;
83
+ lines: string[];
84
+ }
85
+
86
+ type ListItem =
87
+ | { type: "option"; option: AskOption }
88
+ | { type: "comment-toggle"; option: AskOption }
89
+ | { type: "freeform"; option: AskOption };
90
+
91
+ function buildItemBlocks(
92
+ options: AskOption[],
93
+ width: number,
94
+ allowFreeform: boolean,
95
+ allowComment: boolean,
96
+ commentEnabled: boolean,
97
+ selectedIndex: number,
98
+ hideDescriptions = false,
99
+ ): ItemBlock[] {
100
+ const normalizedWidth = Math.max(12, width);
101
+ const freeformLabel = "Type something. — Enter a custom response";
102
+ const commentToggleLabel = `${commentEnabled ? "[✓]" : "[ ]"} Add extra context after selection`;
103
+ const allItems: ListItem[] = options.map((option) => ({ type: "option", option }));
104
+ if (allowComment) allItems.push({ type: "comment-toggle", option: { title: commentToggleLabel } });
105
+ if (allowFreeform) allItems.push({ type: "freeform", option: { title: freeformLabel } });
106
+
107
+ return allItems.map((item, itemIndex) => {
108
+ const pointer = itemIndex === selectedIndex ? "→" : " ";
109
+ const lines: string[] = [];
110
+
111
+ if (item.type === "comment-toggle" || item.type === "freeform") {
112
+ const prefix = `${pointer} `;
113
+ const wrapped = wrapText(item.option.title, Math.max(8, normalizedWidth - prefix.length));
114
+ wrapped.forEach((line, lineIndex) => {
115
+ lines.push(padLine(lineIndex === 0 ? prefix : " ".repeat(prefix.length), line));
116
+ });
117
+ return { itemIndex, lines };
118
+ }
119
+
120
+ const numberPrefix = `${pointer} ${itemIndex + 1}. `;
121
+ const continuationPrefix = " ".repeat(numberPrefix.length);
122
+ const titleLines = wrapText(item.option.title, Math.max(8, normalizedWidth - numberPrefix.length));
123
+ titleLines.forEach((line, lineIndex) => {
124
+ lines.push(padLine(lineIndex === 0 ? numberPrefix : continuationPrefix, line));
125
+ });
126
+
127
+ if (item.option.description && !hideDescriptions) {
128
+ const descriptionPrefix = " ";
129
+ const descriptionLines = wrapText(item.option.description, Math.max(8, normalizedWidth - descriptionPrefix.length));
130
+ descriptionLines.forEach((line) => lines.push(padLine(descriptionPrefix, line)));
131
+ }
132
+
133
+ return { itemIndex, lines };
134
+ });
135
+ }
136
+
137
+ function flatten(blocks: ItemBlock[], selectedIndex: number): AnnotatedRow[] {
138
+ return blocks.flatMap((block) => block.lines.map((line) => ({ line, selected: block.itemIndex === selectedIndex })));
139
+ }
140
+
141
+ export function renderSingleSelectRows({
142
+ options,
143
+ selectedIndex,
144
+ width,
145
+ allowFreeform,
146
+ allowComment = false,
147
+ commentEnabled = false,
148
+ maxRows,
149
+ hideDescriptions,
150
+ }: RenderSingleSelectRowsParams): AnnotatedRow[] {
151
+ const itemCount = options.length + (allowComment ? 1 : 0) + (allowFreeform ? 1 : 0);
152
+ const blocks = buildItemBlocks(options, width, allowFreeform, allowComment, commentEnabled, selectedIndex, hideDescriptions);
153
+ const allRows = flatten(blocks, selectedIndex);
154
+
155
+ if (!Number.isFinite(maxRows) || !maxRows || maxRows <= 0 || allRows.length <= maxRows) return allRows;
156
+
157
+ const safeMaxRows = Math.max(1, Math.floor(maxRows));
158
+ const selectedBlock = blocks[selectedIndex] ?? blocks[0];
159
+ if (!selectedBlock) return [];
160
+
161
+ const indicator = ` (${selectedIndex + 1}/${itemCount})`;
162
+ const availableRows = safeMaxRows > 1 ? safeMaxRows - 1 : 1;
163
+
164
+ if (selectedBlock.lines.length >= availableRows) {
165
+ const visible = selectedBlock.lines.slice(0, availableRows).map((line) => ({ line, selected: true }));
166
+ if (safeMaxRows > 1) visible.push({ line: indicator, selected: false });
167
+ return visible.slice(0, safeMaxRows);
168
+ }
169
+
170
+ let start = selectedIndex;
171
+ let end = selectedIndex + 1;
172
+ let usedRows = selectedBlock.lines.length;
173
+
174
+ while (true) {
175
+ const nextCanFit = end < blocks.length && usedRows + blocks[end]!.lines.length <= availableRows;
176
+ if (nextCanFit) {
177
+ usedRows += blocks[end]!.lines.length;
178
+ end += 1;
179
+ continue;
180
+ }
181
+ const prevCanFit = start > 0 && usedRows + blocks[start - 1]!.lines.length <= availableRows;
182
+ if (prevCanFit) {
183
+ start -= 1;
184
+ usedRows += blocks[start]!.lines.length;
185
+ continue;
186
+ }
187
+ break;
188
+ }
189
+
190
+ const visible = flatten(blocks.slice(start, end), selectedIndex);
191
+ visible.push({ line: indicator, selected: false });
192
+ return visible.slice(0, safeMaxRows);
193
+ }