@groeponline/pi-wishcraft 0.26.0 → 0.27.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/AGENTS.md CHANGED
@@ -57,6 +57,8 @@ user customization.
57
57
  - **Bun/Node-native TS:** `import` paths carry `.ts` extensions
58
58
  (`allowImportingTsExtensions`), `node:`-prefixed builtins, `node --test`
59
59
  with type stripping. No build step.
60
+ - **English UI:** operator overlays, notify strings, and `/wishcraft` copy are
61
+ English. Do not add Dutch UI strings.
60
62
  - **Tests:** behavior changes in `src/` need a focused regression test near
61
63
  the existing tests for that subsystem; structural tests assert on module
62
64
  files in `src/` (never `index.ts`).
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.27.1] - 2026-08-20
6
+
7
+ ### Changed
8
+ - Operator UI is English: skill manager, `/wishcraft` TUI, overlay empty-match row, and skill-health warnings.
9
+
10
+ ## [0.27.0] - 2026-08-20
11
+
12
+ ### Added
13
+ - `/ideas` review overlay: `reviewStatus` (idea / in-progress / done), tags, and Run with skill X. Welcome queue widget shows the next idea plus `/ideas next`.
14
+
5
15
  ## [0.26.0] - 2026-08-20
6
16
 
7
17
  ### Added
package/ROADMAP.md CHANGED
@@ -219,50 +219,49 @@ de zes gevallen + de parse-vóór-wrap invariant.
219
219
 
220
220
  ### PR G — overlay-submenus (CHE-42) — ✅ #19 + expansion (#13 reopen)
221
221
 
222
- Het `alt+p`-menu krijgt gestapelde `SelectList`-overlays (pijltjes +
223
- descriptions) in plaats van platte `ctx.ui.select`. Max drie
224
- top-level ingangen. #19 landt de top-level boom (Navigate / Configure /
225
- Status). Configure gebruikt nu `showSelectOverlay` (CHE-42 rest). CHE-40
226
- is Done (#18). CHE-41 is Done: `→` in Navigate, snapshot bij openen, geen tweede `alt+i`-pad.
222
+ The `alt+p` menu gets stacked `SelectList` overlays (arrows +
223
+ descriptions) instead of flat `ctx.ui.select`. Maximum three top-level
224
+ entries. #19 lands the top-level tree (Navigate / Configure /
225
+ Status). Configure now uses `showSelectOverlay` (CHE-42 remainder). CHE-40
226
+ is Done (#18). CHE-41 is Done: `→` in Navigate, snapshot on open, no second `alt+i` path.
227
227
 
228
228
  ### PR H — skills manager v2 UI + token-overlays
229
229
 
230
- Vervangt `skill-manager.ts` (242 regels). Data-laag bouwt op Pi
230
+ Replaces `skill-manager.ts` (242 lines). The data layer builds on Pi
231
231
  core `loadSkills` / `loadSkillsFromDir` / `Skill` /
232
- `SkillFrontmatter` (publiek geëxporteerd in
232
+ `SkillFrontmatter` (publicly exported in
233
233
  `@earendil-works/pi-coding-agent`).
234
234
 
235
- UI, niet onderhandelbaar:
236
-
237
- - Zoeken die werkt: substring op naam + beschrijving + pad,
238
- case-insensitive; `ctrl+u` wist; lege match = rij
239
- `geen skills voor '<q>'`.
240
- - Categorieën met kopregels (bundled / global / project / prompts);
241
- `tab` wisselt filter; `s` sorteert naamgebruik.
242
- - `→` detail (frontmatter-tabel, usage, pad, body + scroll);
243
- `enter` insert; `e` opent `$EDITOR` (default nvim) via extern
244
- spawn zoals pi's `!`; `n` nieuwe skill in
245
- `~/.pi/agent/skills/<naam>/SKILL.md`; `d` delete + confirm.
246
- - Usage-ledger `~/.pi/agent/skill-usage.json` (naam, timestamp,
247
- trigger-type). Best-effort, nooit blokkerend op de input hot path.
248
- - Skill health: core-diagnostics als waarschuwingsicoon; `?` legt
249
- het uit.
250
-
251
- Zelfde chrome als de segment-navigator. Nederlands, direct.
252
-
253
- Tegelijk, klein:
254
-
255
- - `/tps` overlay: in/out, piek + gemiddelde over de bestaande ring.
256
- Geen nieuwe sampler.
257
- - `/usage` overlay: vandaag / deze week / deze sessie; per model;
258
- cache-hit %; ASCII-sparkline. File
259
- `~/.pi/agent/wishcraft-usage.json` (append-only, compaction bij
260
- drempel).
261
- - `wishcraft.tokenBudget.daily` kleurt het segment rood en waarschuwt
262
- in welcome bij 80% / 100%. Nooit blokkerend.
263
-
264
- Done: filter-tests op substring; usage-ledger tests; `/tps` leest
265
- dezelfde ring als het segment (geen tweede waarheid).
235
+ UI, non-negotiable:
236
+
237
+ - Working search: substring on name + description + path,
238
+ case-insensitive; `ctrl+u` clears; empty match = row
239
+ `no skills for '<q>'`.
240
+ - Categories with headings (bundled / global / project / prompts);
241
+ `tab` switches the filter; `s` sorts nameusage.
242
+ - `→` detail (frontmatter table, usage, path, body + scroll);
243
+ `enter` inserts; `e` opens `$EDITOR` (default nvim) via external
244
+ spawn like pi's `!`; `n` creates a new skill in
245
+ `~/.pi/agent/skills/<name>/SKILL.md`; `d` delete + confirm.
246
+ - Usage-ledger `~/.pi/agent/skill-usage.json` (name, timestamp,
247
+ trigger type). Best-effort, never blocking on the input hot path.
248
+ - Skill health: core-diagnostics as a warning icon; `?` explains it.
249
+
250
+ Same chrome as the segment navigator. English, direct.
251
+
252
+ At the same time, small additions:
253
+
254
+ - `/tps` overlay: in/out, peak + average over the existing ring.
255
+ No new sampler.
256
+ - `/usage` overlay: today / this week / this session; per model;
257
+ cache-hit %; ASCII sparkline. File
258
+ `~/.pi/agent/wishcraft-usage.json` (append-only, compaction at
259
+ threshold).
260
+ - `wishcraft.tokenBudget.daily` colors the segment red and warns
261
+ in the welcome at 80% / 100%. Never blocking.
262
+
263
+ Done: substring filter tests; usage-ledger tests; `/tps` reads
264
+ the same ring as the segment (no second source of truth).
266
265
 
267
266
  ---
268
267
 
@@ -314,7 +313,9 @@ die package nog leeft.
314
313
 
315
314
  ## Kwaliteit (altijd)
316
315
 
317
- - Overlay-logica testbaar via pure functies. Geen headless `ctx.ui`.
316
+ - Overlay logic is testable via pure functions. No headless `ctx.ui`.
317
+ - **English UI:** operator overlays, notify strings, and `/wishcraft` copy are
318
+ English. Do not add Dutch UI strings.
318
319
  - `prepublishOnly` = `tsc --noEmit`. Een slecht type ship't niet.
319
320
  - `madge --circular` blijft CI. Nieuwe map in `src/` → check mee.
320
321
  - Dependabot-vulns (devDep-transitief): waiven, track op GRO-603.
package/docs/commands.md CHANGED
@@ -18,8 +18,8 @@ Commands and capture shortcuts:
18
18
  - `/compact <text>`: compact now and queue `<text>` as the next prompt after successful compaction
19
19
  - `/idea [@target] <text>`: command form of idea capture, useful for scripts and users who disable the sigil
20
20
  - `/idea issue [id]`: hand the oldest active idea, or a specific idea, to the current agent for safe GitHub issue triage
21
- - `/ideas`: open the captured-ideas picker
22
- - `/ideas next`: send the oldest active idea to the current session
21
+ - `/ideas`: open the idea-review overlay (status, tags, run with skill)
22
+ - `/ideas next`: send the oldest active idea that is not review-done
23
23
  - `/ideas issue [id]`: ask the current agent to dedupe and file a GitHub issue only when the target repo is clear and owned/controlled
24
24
  - `/ideas send <id>`: send an idea to the current session
25
25
  - `/queue`: open the queued-prompt picker
@@ -55,7 +55,7 @@ Set `captureSigil` to `false` if you often submit markdown headings and prefer `
55
55
  }
56
56
  ```
57
57
 
58
- Captured data is stored under the Pi agent directory in `powerline-footer/inbox.jsonl` and `powerline-footer/projects.json`. `inbox.jsonl` is a stable read surface for orchestrators and helper agents; each line is a queue item with `id`, `text`, `createdAt`, `updatedAt`, `source`, `target`, `intent`, `status`, and optional `error`. Writes should still go through Powerline commands or the store so locking and atomic writes are preserved. Ideas sent with `/ideas next` or `/ideas send <id>` include a small provenance header so the receiving agent can treat them as deferred captured context. `/idea issue` and `/ideas issue` do not file issues directly from the extension; they send a guarded handoff prompt that tells the current agent to dedupe open issues first, create a GitHub issue only for a clear owned/controlled repo, and ask before filing when the target is unclear.
58
+ Captured data is stored under the Pi agent directory in `powerline-footer/inbox.jsonl` and `powerline-footer/projects.json`. `inbox.jsonl` is a stable read surface for orchestrators and helper agents; each line is a queue item with `id`, `text`, `createdAt`, `updatedAt`, `source`, `target`, `intent`, `status`, optional `error`, and for ideas optional `reviewStatus` (`idea` | `in-progress` | `done`) and `tags`. Legacy lines without those fields still parse. Writes should still go through Powerline commands or the store so locking and atomic writes are preserved. Ideas sent with `/ideas next` or `/ideas send <id>` include a small provenance header so the receiving agent can treat them as deferred captured context. `/ideas` opens the review overlay (same chrome as `/skills`): set review status and tags, or **Run with skill X** to insert the skill body plus the idea into the editor. The welcome queue widget shows the next idea and `/ideas next`; welcome dismisses on any key, so that widget does not bind enter-to-send. `/idea issue` and `/ideas issue` do not file issues directly from the extension; they send a guarded handoff prompt that tells the current agent to dedupe open issues first, create a GitHub issue only for a clear owned/controlled repo, and ask before filing when the target is unclear.
59
59
 
60
60
  ### Placement
61
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "0.26.0",
3
+ "version": "0.27.1",
4
4
  "description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
5
5
  "type": "module",
6
6
  "files": [
package/queue/store.ts CHANGED
@@ -11,6 +11,7 @@ import { randomUUID } from "node:crypto";
11
11
  import { getAgentPath } from "../src/paths/agent-dirs.ts";
12
12
  import type {
13
13
  CreateQueueItemInput,
14
+ IdeaReviewStatus,
14
15
  PowerlineQueueItem,
15
16
  QueueAliasMap,
16
17
  QueueContext,
@@ -19,7 +20,7 @@ import type {
19
20
  QueueSummary,
20
21
  QueueTarget,
21
22
  } from "./types.ts";
22
- import { ACTIVE_QUEUE_STATUSES } from "./types.ts";
23
+ import { ACTIVE_QUEUE_STATUSES, IDEA_REVIEW_STATUSES } from "./types.ts";
23
24
 
24
25
  const STORE_DIR = "powerline-footer";
25
26
  const INBOX_FILE = "inbox.jsonl";
@@ -77,6 +78,30 @@ function normalizeStatus(value: unknown): QueueStatus | null {
77
78
  : null;
78
79
  }
79
80
 
81
+ function normalizeReviewStatus(value: unknown): IdeaReviewStatus | undefined {
82
+ return IDEA_REVIEW_STATUSES.includes(value as IdeaReviewStatus)
83
+ ? (value as IdeaReviewStatus)
84
+ : undefined;
85
+ }
86
+
87
+ const MAX_IDEA_TAGS = 8;
88
+ const MAX_TAG_CHARS = 32;
89
+
90
+ export function normalizeIdeaTags(value: unknown): string[] | undefined {
91
+ if (!Array.isArray(value)) return undefined;
92
+ const seen = new Set<string>();
93
+ const tags: string[] = [];
94
+ for (const entry of value) {
95
+ if (typeof entry !== "string") continue;
96
+ const tag = entry.trim().slice(0, MAX_TAG_CHARS);
97
+ if (!tag || seen.has(tag)) continue;
98
+ seen.add(tag);
99
+ tags.push(tag);
100
+ if (tags.length >= MAX_IDEA_TAGS) break;
101
+ }
102
+ return tags.length > 0 ? tags : undefined;
103
+ }
104
+
80
105
  function normalizeItem(value: unknown): PowerlineQueueItem | null {
81
106
  if (!isRecord(value)) return null;
82
107
  if (typeof value.id !== "string" || !value.id.trim()) return null;
@@ -99,6 +124,9 @@ function normalizeItem(value: unknown): PowerlineQueueItem | null {
99
124
 
100
125
  const sessionId = normalizeOptionalString(value.source.sessionId);
101
126
  const error = normalizeOptionalString(value.error);
127
+ const reviewStatus =
128
+ intent === "idea" ? normalizeReviewStatus(value.reviewStatus) : undefined;
129
+ const tags = intent === "idea" ? normalizeIdeaTags(value.tags) : undefined;
102
130
 
103
131
  return {
104
132
  id: value.id.trim(),
@@ -112,6 +140,8 @@ function normalizeItem(value: unknown): PowerlineQueueItem | null {
112
140
  intent,
113
141
  status,
114
142
  ...(error ? { error } : {}),
143
+ ...(reviewStatus ? { reviewStatus } : {}),
144
+ ...(tags ? { tags } : {}),
115
145
  };
116
146
  }
117
147
 
@@ -142,6 +172,12 @@ export function createQueueItem(
142
172
  ): PowerlineQueueItem {
143
173
  const now = input.now ?? Date.now();
144
174
  const sourceSessionId = input.source.sessionId?.trim();
175
+ const reviewStatus =
176
+ input.intent === "idea"
177
+ ? (input.reviewStatus ?? "idea")
178
+ : undefined;
179
+ const tags =
180
+ input.intent === "idea" ? normalizeIdeaTags(input.tags) : undefined;
145
181
  return {
146
182
  id: randomUUID().slice(0, 8),
147
183
  text: input.text,
@@ -153,6 +189,8 @@ export function createQueueItem(
153
189
  target: normalizeTarget(input.target) ?? input.target,
154
190
  intent: input.intent,
155
191
  status: input.status ?? "queued",
192
+ ...(reviewStatus ? { reviewStatus } : {}),
193
+ ...(tags ? { tags } : {}),
156
194
  };
157
195
  }
158
196
 
package/queue/types.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  export type QueueIntent = "steer" | "follow-up" | "post-compact" | "idea";
2
2
  export type QueueStatus = "queued" | "blocked" | "delivering" | "sent" | "failed";
3
+ /** Review language for captured ideas. Separate from delivery `QueueStatus`. */
4
+ export type IdeaReviewStatus = "idea" | "in-progress" | "done";
5
+ export const IDEA_REVIEW_STATUSES = [
6
+ "idea",
7
+ "in-progress",
8
+ "done",
9
+ ] as const satisfies readonly IdeaReviewStatus[];
3
10
 
4
11
  export type QueueTarget =
5
12
  | { kind: "current-session" }
@@ -21,6 +28,8 @@ export interface PowerlineQueueItem {
21
28
  intent: QueueIntent;
22
29
  status: QueueStatus;
23
30
  error?: string;
31
+ reviewStatus?: IdeaReviewStatus;
32
+ tags?: string[];
24
33
  }
25
34
 
26
35
  export interface QueueAliasMap {
@@ -43,6 +52,8 @@ export interface CreateQueueItemInput {
43
52
  target: QueueTarget;
44
53
  intent: QueueIntent;
45
54
  status?: QueueStatus;
55
+ reviewStatus?: IdeaReviewStatus;
56
+ tags?: string[];
46
57
  now?: number;
47
58
  }
48
59
 
@@ -10,6 +10,7 @@ import {
10
10
  sendIdeaIssueHandoffById,
11
11
  sendOrRetryQueueItem,
12
12
  } from "../queue/queue-integration.ts";
13
+ import { openIdeasReview } from "../queue/idea-review.ts";
13
14
  import { getCurrentEditorText } from "../shortcuts/shortcuts-router.ts";
14
15
  import { getQueueContext } from "../queue/queue-context.ts";
15
16
  import { config } from "../core/state.ts";
@@ -65,7 +66,7 @@ export function registerQueueCommands(
65
66
  const id = parts[1];
66
67
 
67
68
  if (!action) {
68
- await openQueuePicker(pi, rt, ctx, "ideas");
69
+ await openIdeasReview(pi, rt, ctx);
69
70
  return;
70
71
  }
71
72
 
@@ -78,6 +79,7 @@ export function registerQueueCommands(
78
79
  const updated = rt.queueStore.update(item.id, {
79
80
  status: "queued",
80
81
  target: { kind: "current-session" },
82
+ reviewStatus: "in-progress",
81
83
  error: undefined,
82
84
  });
83
85
  if (updated) deliverQueueItem(pi, rt, ctx, updated);
@@ -107,6 +109,7 @@ export function registerQueueCommands(
107
109
  const updated = rt.queueStore.update(item.id, {
108
110
  status: "queued",
109
111
  target: { kind: "current-session" },
112
+ reviewStatus: "in-progress",
110
113
  error: undefined,
111
114
  });
112
115
  if (updated) deliverQueueItem(pi, rt, ctx, updated);
@@ -121,7 +124,11 @@ export function registerQueueCommands(
121
124
  }
122
125
 
123
126
  if (action === "clear") {
124
- rt.queueStore.clear(item.id);
127
+ rt.queueStore.update(item.id, {
128
+ status: "sent",
129
+ reviewStatus: "done",
130
+ error: undefined,
131
+ });
125
132
  ctx.ui.notify(`Cleared idea ${item.id}`, "info");
126
133
  requestQueueRender(rt);
127
134
  return;
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Idea-review overlay for `/ideas`.
3
+ * Review language (`reviewStatus`, `tags`) is separate from delivery QueueStatus.
4
+ * Overlay chrome matches `/skills` (showSelectOverlay, not ctx.ui.select).
5
+ */
6
+
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import type { SelectItem } from "@earendil-works/pi-tui";
9
+
10
+ import {
11
+ IDEA_REVIEW_STATUSES,
12
+ type IdeaReviewStatus,
13
+ type PowerlineQueueItem,
14
+ } from "../../../queue/types.ts";
15
+ import { buildStashPreview } from "../history/stash-history.ts";
16
+ import {
17
+ loadSkillCatalog,
18
+ readSkillBodyStrict,
19
+ insertSkillBody,
20
+ } from "../skills/skill-registry.ts";
21
+ import { showSelectOverlay } from "../ui/overlay-chrome.ts";
22
+ import type { RuntimeState } from "../core/types.ts";
23
+
24
+ export type IdeaSelectOverlay = (
25
+ ctx: any,
26
+ title: string,
27
+ hint: string,
28
+ items: SelectItem[],
29
+ maxVisible: number,
30
+ ) => Promise<SelectItem | null>;
31
+ import { getQueueContext } from "./queue-context.ts";
32
+ import {
33
+ deliverQueueItem,
34
+ requestQueueRender,
35
+ } from "./queue-integration.ts";
36
+
37
+ export const IDEA_TAG_PRESETS = [
38
+ "review",
39
+ "later",
40
+ "bug",
41
+ "feature",
42
+ "chore",
43
+ ] as const;
44
+
45
+ export function ideaReviewStatusOf(
46
+ item: Pick<PowerlineQueueItem, "reviewStatus">,
47
+ ): IdeaReviewStatus {
48
+ return item.reviewStatus ?? "idea";
49
+ }
50
+
51
+ export function formatIdeaReviewLabel(
52
+ item: PowerlineQueueItem,
53
+ previewWidth = 48,
54
+ ): string {
55
+ const review = ideaReviewStatusOf(item);
56
+ const tags = item.tags?.length ? ` [${item.tags.join(", ")}]` : "";
57
+ return `${item.id} ${review}${tags} ${buildStashPreview(item.text, previewWidth)}`;
58
+ }
59
+
60
+ export function buildIdeaPickerItems(
61
+ items: readonly PowerlineQueueItem[],
62
+ ): SelectItem[] {
63
+ return items.map((item) => ({
64
+ value: item.id,
65
+ label: formatIdeaReviewLabel(item),
66
+ description: item.tags?.join(", ") || ideaReviewStatusOf(item),
67
+ }));
68
+ }
69
+
70
+ export function buildIdeaActionItems(item: PowerlineQueueItem): SelectItem[] {
71
+ const review = ideaReviewStatusOf(item);
72
+ const tagSummary = item.tags?.length ? item.tags.join(", ") : "none";
73
+ return [
74
+ {
75
+ value: "send",
76
+ label: "Send to current session",
77
+ description: "Deliver as a prompt",
78
+ },
79
+ {
80
+ value: "skill",
81
+ label: "Run with skill X",
82
+ description: "Insert skill body plus this idea into the editor",
83
+ },
84
+ {
85
+ value: "status",
86
+ label: `Set status (${review})`,
87
+ description: "idea / in-progress / done",
88
+ },
89
+ {
90
+ value: "tags",
91
+ label: `Tags (${tagSummary})`,
92
+ description: "Toggle presets or clear",
93
+ },
94
+ {
95
+ value: "edit",
96
+ label: "Edit in prompt",
97
+ description: "Move text into the editor",
98
+ },
99
+ {
100
+ value: "clear",
101
+ label: "Clear",
102
+ description: "Mark delivery sent and review done",
103
+ },
104
+ { value: "cancel", label: "Cancel" },
105
+ ];
106
+ }
107
+
108
+ export function buildReviewStatusItems(
109
+ current: IdeaReviewStatus,
110
+ ): SelectItem[] {
111
+ return IDEA_REVIEW_STATUSES.map((status) => ({
112
+ value: status,
113
+ label: status === current ? `${status} (current)` : status,
114
+ description:
115
+ status === "idea"
116
+ ? "Captured, not started"
117
+ : status === "in-progress"
118
+ ? "Being worked"
119
+ : "Reviewed or finished",
120
+ }));
121
+ }
122
+
123
+ export function buildTagItems(tags: readonly string[] | undefined): SelectItem[] {
124
+ const current = new Set(tags ?? []);
125
+ const items: SelectItem[] = IDEA_TAG_PRESETS.map((tag) => ({
126
+ value: `toggle:${tag}`,
127
+ label: current.has(tag) ? `✓ ${tag}` : tag,
128
+ description: current.has(tag) ? "Remove tag" : "Add tag",
129
+ }));
130
+ items.push({
131
+ value: "clear",
132
+ label: "Clear tags",
133
+ description: "Remove all tags",
134
+ });
135
+ items.push({ value: "back", label: "Back" });
136
+ return items;
137
+ }
138
+
139
+ export function toggleIdeaTag(
140
+ tags: readonly string[] | undefined,
141
+ tag: string,
142
+ ): string[] | undefined {
143
+ const current = [...(tags ?? [])];
144
+ const next = current.includes(tag)
145
+ ? current.filter((entry) => entry !== tag)
146
+ : [...current, tag];
147
+ return next.length > 0 ? next : undefined;
148
+ }
149
+
150
+ export function composeSkillIdeaInsert(
151
+ skillBody: string,
152
+ ideaText: string,
153
+ ): string {
154
+ const body = skillBody.trim();
155
+ const idea = ideaText.trim();
156
+ if (!body) return idea;
157
+ if (!idea) return body;
158
+ return `${body}\n\n${idea}`;
159
+ }
160
+
161
+ export function pickNextReviewIdea(
162
+ items: readonly PowerlineQueueItem[],
163
+ ): PowerlineQueueItem | null {
164
+ const ideas = items.filter((item) => item.intent === "idea");
165
+ return ideas.find((item) => ideaReviewStatusOf(item) !== "done") ?? null;
166
+ }
167
+
168
+ function activeIdeas(rt: RuntimeState, ctx: any): PowerlineQueueItem[] {
169
+ return rt.queueStore
170
+ .activeItems(getQueueContext(ctx))
171
+ .filter((item) => item.intent === "idea");
172
+ }
173
+
174
+ export function insertSkillAndIdea(
175
+ ctx: any,
176
+ skillName: string,
177
+ filePath: string,
178
+ ideaText: string,
179
+ ): void {
180
+ try {
181
+ insertSkillBody(ctx, skillName, readSkillBodyStrict(filePath), ideaText);
182
+ ctx.ui.notify("Skill and idea inserted into the prompt", "info");
183
+ } catch (error) {
184
+ ctx.ui.notify(`Could not read skill: ${error instanceof Error ? error.message : String(error)}`, "error");
185
+ }
186
+ }
187
+
188
+ async function pickSkillForIdea(
189
+ ctx: any,
190
+ item: PowerlineQueueItem,
191
+ selectOverlay: IdeaSelectOverlay,
192
+ ): Promise<void> {
193
+ const entries = loadSkillCatalog(ctx.cwd ?? process.cwd());
194
+ if (entries.length === 0) {
195
+ ctx.ui.notify("No skills installed", "info");
196
+ return;
197
+ }
198
+ const selected = await selectOverlay(
199
+ ctx,
200
+ `Run ${item.id} with skill`,
201
+ "↑↓ navigate • enter insert • esc cancel",
202
+ entries.map((entry) => ({
203
+ value: entry.filePath,
204
+ label: entry.name,
205
+ description: entry.description || entry.category,
206
+ })),
207
+ Math.min(entries.length, 12),
208
+ );
209
+ if (!selected) return;
210
+ const entry = entries.find((candidate) => candidate.filePath === selected.value);
211
+ if (entry) insertSkillAndIdea(ctx, entry.name, entry.filePath, item.text);
212
+ }
213
+
214
+ async function chooseIdeaReviewAction(
215
+ pi: ExtensionAPI,
216
+ rt: RuntimeState,
217
+ ctx: any,
218
+ item: PowerlineQueueItem,
219
+ selectOverlay: IdeaSelectOverlay,
220
+ ): Promise<void> {
221
+ const selected = await selectOverlay(
222
+ ctx,
223
+ `Idea ${item.id}`,
224
+ buildStashPreview(item.text, 72),
225
+ buildIdeaActionItems(item),
226
+ 8,
227
+ );
228
+ if (!selected || selected.value === "cancel") return;
229
+
230
+ if (selected.value === "send") {
231
+ const updated = rt.queueStore.update(item.id, {
232
+ status: "queued",
233
+ target: { kind: "current-session" },
234
+ reviewStatus: "in-progress",
235
+ error: undefined,
236
+ });
237
+ if (updated) deliverQueueItem(pi, rt, ctx, updated);
238
+ return;
239
+ }
240
+
241
+ if (selected.value === "skill") {
242
+ await pickSkillForIdea(ctx, item, selectOverlay);
243
+ return;
244
+ }
245
+
246
+ if (selected.value === "status") {
247
+ const next = await selectOverlay(
248
+ ctx,
249
+ `Status for ${item.id}`,
250
+ "idea / in-progress / done",
251
+ buildReviewStatusItems(ideaReviewStatusOf(item)),
252
+ IDEA_REVIEW_STATUSES.length,
253
+ );
254
+ if (!next) return;
255
+ rt.queueStore.update(item.id, {
256
+ reviewStatus: next.value as IdeaReviewStatus,
257
+ });
258
+ ctx.ui.notify(`Idea ${item.id} → ${next.value}`, "info");
259
+ requestQueueRender(rt);
260
+ const fresh = rt.queueStore.get(item.id);
261
+ if (fresh) await chooseIdeaReviewAction(pi, rt, ctx, fresh, selectOverlay);
262
+ return;
263
+ }
264
+
265
+ if (selected.value === "tags") {
266
+ const next = await selectOverlay(
267
+ ctx,
268
+ `Tags for ${item.id}`,
269
+ "enter toggles • esc back",
270
+ buildTagItems(item.tags),
271
+ IDEA_TAG_PRESETS.length + 2,
272
+ );
273
+ if (!next || next.value === "back") {
274
+ const fresh = rt.queueStore.get(item.id);
275
+ if (fresh) await chooseIdeaReviewAction(pi, rt, ctx, fresh, selectOverlay);
276
+ return;
277
+ }
278
+ const tags =
279
+ next.value === "clear"
280
+ ? undefined
281
+ : toggleIdeaTag(item.tags, next.value.replace(/^toggle:/, ""));
282
+ rt.queueStore.update(item.id, { tags });
283
+ requestQueueRender(rt);
284
+ const fresh = rt.queueStore.get(item.id);
285
+ if (fresh) await chooseIdeaReviewAction(pi, rt, ctx, fresh, selectOverlay);
286
+ return;
287
+ }
288
+
289
+ if (selected.value === "edit") {
290
+ ctx.ui.setEditorText(item.text);
291
+ rt.queueStore.clear(item.id);
292
+ requestQueueRender(rt);
293
+ return;
294
+ }
295
+
296
+ if (selected.value === "clear") {
297
+ rt.queueStore.update(item.id, {
298
+ status: "sent",
299
+ reviewStatus: "done",
300
+ error: undefined,
301
+ });
302
+ ctx.ui.notify(`Cleared idea ${item.id}`, "info");
303
+ requestQueueRender(rt);
304
+ }
305
+ }
306
+
307
+ export async function openIdeasReview(
308
+ pi: ExtensionAPI,
309
+ rt: RuntimeState,
310
+ ctx: any,
311
+ selectOverlay: IdeaSelectOverlay = showSelectOverlay,
312
+ ): Promise<void> {
313
+ const ideas = activeIdeas(rt, ctx);
314
+ if (ideas.length === 0) {
315
+ ctx.ui.notify("No ideas captured", "info");
316
+ return;
317
+ }
318
+
319
+ const selected = await selectOverlay(
320
+ ctx,
321
+ "Wishcraft ideas",
322
+ "↑↓ navigate • enter review • esc cancel",
323
+ buildIdeaPickerItems(ideas),
324
+ Math.min(ideas.length, 12),
325
+ );
326
+ if (!selected) return;
327
+
328
+ const item = ideas.find((candidate) => candidate.id === selected.value);
329
+ if (item) await chooseIdeaReviewAction(pi, rt, ctx, item, selectOverlay);
330
+ }
@@ -384,11 +384,10 @@ export function findNextIdea(
384
384
  rt: RuntimeState,
385
385
  ctx: any,
386
386
  ): PowerlineQueueItem | null {
387
- return (
388
- rt.queueStore
389
- .activeItems(getQueueContext(ctx))
390
- .find((candidate) => candidate.intent === "idea") ?? null
391
- );
387
+ const ideas = rt.queueStore
388
+ .activeItems(getQueueContext(ctx))
389
+ .filter((candidate) => candidate.intent === "idea");
390
+ return ideas.find((candidate) => candidate.reviewStatus !== "done") ?? null;
392
391
  }
393
392
 
394
393
  export function sendIdeaIssueHandoff(