@groeponline/pi-wishcraft 0.17.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.
Files changed (106) hide show
  1. package/AGENTS.md +68 -0
  2. package/CHANGELOG.md +724 -0
  3. package/CONTRIBUTING.md +37 -0
  4. package/README.md +648 -0
  5. package/RELEASE.md +117 -0
  6. package/ROADMAP.md +52 -0
  7. package/bash-mode/completion-providers.ts +269 -0
  8. package/bash-mode/completion.ts +416 -0
  9. package/bash-mode/editor-ghost.ts +40 -0
  10. package/bash-mode/editor-input.ts +80 -0
  11. package/bash-mode/editor.ts +437 -0
  12. package/bash-mode/history.ts +263 -0
  13. package/bash-mode/shell-session.ts +286 -0
  14. package/bash-mode/transcript.ts +108 -0
  15. package/bash-mode/types.ts +80 -0
  16. package/index.ts +6 -0
  17. package/package.json +55 -0
  18. package/queue/store.ts +443 -0
  19. package/queue/types.ts +54 -0
  20. package/src/config/custom-items.ts +182 -0
  21. package/src/config/extension-statuses.ts +51 -0
  22. package/src/config/layout.ts +60 -0
  23. package/src/config/parse.ts +127 -0
  24. package/src/config/powerline-config.ts +18 -0
  25. package/src/config/presets.ts +245 -0
  26. package/src/config/primitives.ts +117 -0
  27. package/src/config/segment-ids.ts +114 -0
  28. package/src/config/segment-options.ts +128 -0
  29. package/src/config/settings-patch.ts +26 -0
  30. package/src/config/types.ts +277 -0
  31. package/src/core/frontmatter.ts +40 -0
  32. package/src/editor/autocomplete-chain.ts +41 -0
  33. package/src/extension/activate.ts +28 -0
  34. package/src/extension/bash-mode-actions.ts +104 -0
  35. package/src/extension/commands.ts +268 -0
  36. package/src/extension/constants.ts +46 -0
  37. package/src/extension/custom-editor.ts +406 -0
  38. package/src/extension/git-invalidation.ts +40 -0
  39. package/src/extension/layout.ts +160 -0
  40. package/src/extension/menu-views.ts +393 -0
  41. package/src/extension/powerline-widgets.ts +95 -0
  42. package/src/extension/prompt-history.ts +219 -0
  43. package/src/extension/queue-commands.ts +245 -0
  44. package/src/extension/queue-context.ts +12 -0
  45. package/src/extension/queue-integration.ts +434 -0
  46. package/src/extension/segment-context.ts +212 -0
  47. package/src/extension/session-lifecycle.ts +373 -0
  48. package/src/extension/settings-io.ts +202 -0
  49. package/src/extension/shortcuts-config.ts +357 -0
  50. package/src/extension/shortcuts-router.ts +383 -0
  51. package/src/extension/skills/inline-invocation.ts +174 -0
  52. package/src/extension/skills/ook.md +6 -0
  53. package/src/extension/skills/test.md +6 -0
  54. package/src/extension/stale-context.ts +10 -0
  55. package/src/extension/stash-history.ts +103 -0
  56. package/src/extension/state.ts +159 -0
  57. package/src/extension/status-line-renderers.ts +222 -0
  58. package/src/extension/types.ts +97 -0
  59. package/src/extension/vibe-command.ts +160 -0
  60. package/src/extension/welcome-control.ts +27 -0
  61. package/src/extension/welcome-integration.ts +153 -0
  62. package/src/git/status.ts +332 -0
  63. package/src/paths/agent-dirs.ts +67 -0
  64. package/src/render/timer.ts +46 -0
  65. package/src/segments/core.ts +256 -0
  66. package/src/segments/custom.ts +114 -0
  67. package/src/segments/index.ts +3 -0
  68. package/src/segments/registry.ts +87 -0
  69. package/src/segments/shared.ts +36 -0
  70. package/src/segments/system.ts +235 -0
  71. package/src/segments/usage.ts +178 -0
  72. package/src/shell/cd-command.ts +190 -0
  73. package/src/shortcuts/matching.ts +61 -0
  74. package/src/theme/colors.ts +60 -0
  75. package/src/theme/icons.ts +175 -0
  76. package/src/theme/separators.ts +41 -0
  77. package/src/theme/theme.ts +211 -0
  78. package/src/tools/graph.ts +75 -0
  79. package/src/tools/patch.ts +179 -0
  80. package/src/tools/ripgrep.ts +104 -0
  81. package/src/usage/context.ts +97 -0
  82. package/src/usage/ledger.ts +293 -0
  83. package/src/usage/rates.ts +155 -0
  84. package/src/welcome/auto-dismiss.ts +43 -0
  85. package/src/welcome/banner.ts +68 -0
  86. package/src/welcome/discover.ts +234 -0
  87. package/src/welcome/format.ts +18 -0
  88. package/src/welcome/index.ts +5 -0
  89. package/src/welcome/layout.ts +36 -0
  90. package/src/welcome/overlay.ts +80 -0
  91. package/src/welcome/renderer.ts +157 -0
  92. package/src/welcome/sessions.ts +107 -0
  93. package/src/welcome/types.ts +41 -0
  94. package/src/welcome/widgets/graph-widget.ts +25 -0
  95. package/src/welcome/widgets/index.ts +20 -0
  96. package/src/welcome/widgets/queue-widget.ts +26 -0
  97. package/src/welcome/widgets/sessions-widget.ts +23 -0
  98. package/src/welcome/widgets/shortcuts-widget.ts +17 -0
  99. package/src/welcome/widgets/system-widget.ts +29 -0
  100. package/src/working-vibes/generate.ts +144 -0
  101. package/src/working-vibes/index.ts +24 -0
  102. package/src/working-vibes/manager.ts +198 -0
  103. package/src/working-vibes/provider.ts +163 -0
  104. package/src/working-vibes/storage.ts +357 -0
  105. package/theme.example.json +24 -0
  106. package/tsconfig.json +13 -0
@@ -0,0 +1,245 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import {
4
+ captureIdeaFromText,
5
+ deliverQueueItem,
6
+ findNextIdea,
7
+ openQueuePicker,
8
+ requestQueueRender,
9
+ resolveCommandTarget,
10
+ sendIdeaIssueHandoffById,
11
+ sendOrRetryQueueItem,
12
+ } from "./queue-integration.ts";
13
+ import { getCurrentEditorText } from "./shortcuts-router.ts";
14
+ import { getQueueContext } from "./queue-context.ts";
15
+ import type { RuntimeState } from "./types.ts";
16
+
17
+ export function registerQueueCommands(
18
+ pi: ExtensionAPI,
19
+ rt: RuntimeState,
20
+ ): void {
21
+ pi.registerCommand("idea", {
22
+ description:
23
+ "Capture an idea without interrupting the current agent. Usage: /idea [@alias|@global|@current] <text> | /idea issue [id]",
24
+ handler: async (args, ctx) => {
25
+ rt.currentCtx = ctx;
26
+ const trimmedArgs = args.trim();
27
+ const [action, id] = trimmedArgs.split(/\s+/).filter(Boolean);
28
+ if (action === "issue") {
29
+ sendIdeaIssueHandoffById(pi, rt, ctx, id);
30
+ return;
31
+ }
32
+
33
+ const raw =
34
+ trimmedArgs || getCurrentEditorText(ctx, rt.currentEditor).trim();
35
+ if (!raw) {
36
+ ctx.ui.notify(
37
+ "Usage: /idea [@alias|@global|@current] <text> | /idea issue [id]",
38
+ "info",
39
+ );
40
+ return;
41
+ }
42
+
43
+ try {
44
+ const item = captureIdeaFromText(rt, ctx, raw);
45
+ if (item && !trimmedArgs) {
46
+ ctx.ui.setEditorText("");
47
+ }
48
+ } catch (error) {
49
+ ctx.ui.notify(
50
+ error instanceof Error ? error.message : String(error),
51
+ "error",
52
+ );
53
+ }
54
+ },
55
+ });
56
+
57
+ pi.registerCommand("ideas", {
58
+ description:
59
+ "Review or send captured ideas. Usage: /ideas next | /ideas issue [id] | /ideas [send|retry|clear|edit] <id>",
60
+ handler: async (args, ctx) => {
61
+ rt.currentCtx = ctx;
62
+ const parts = args.trim().split(/\s+/).filter(Boolean);
63
+ const action = parts[0];
64
+ const id = parts[1];
65
+
66
+ if (!action) {
67
+ await openQueuePicker(pi, rt, ctx, "ideas");
68
+ return;
69
+ }
70
+
71
+ if (action === "next") {
72
+ const item = findNextIdea(rt, ctx);
73
+ if (!item) {
74
+ ctx.ui.notify("No ideas captured", "info");
75
+ return;
76
+ }
77
+ const updated = rt.queueStore.update(item.id, {
78
+ status: "queued",
79
+ target: { kind: "current-session" },
80
+ error: undefined,
81
+ });
82
+ if (updated) deliverQueueItem(pi, rt, ctx, updated);
83
+ return;
84
+ }
85
+
86
+ if (action === "issue") {
87
+ sendIdeaIssueHandoffById(pi, rt, ctx, id);
88
+ return;
89
+ }
90
+
91
+ if (!id) {
92
+ ctx.ui.notify(
93
+ "Usage: /ideas next | /ideas issue [id] | /ideas [send|retry|clear|edit] <id>",
94
+ "info",
95
+ );
96
+ return;
97
+ }
98
+
99
+ const item = rt.queueStore.get(id);
100
+ if (!item || item.intent !== "idea") {
101
+ ctx.ui.notify(`No unique idea matches ${id}`, "warning");
102
+ return;
103
+ }
104
+
105
+ if (action === "send" || action === "retry") {
106
+ const updated = rt.queueStore.update(item.id, {
107
+ status: "queued",
108
+ target: { kind: "current-session" },
109
+ error: undefined,
110
+ });
111
+ if (updated) deliverQueueItem(pi, rt, ctx, updated);
112
+ return;
113
+ }
114
+
115
+ if (action === "edit") {
116
+ ctx.ui.setEditorText(item.text);
117
+ rt.queueStore.clear(item.id);
118
+ requestQueueRender(rt);
119
+ return;
120
+ }
121
+
122
+ if (action === "clear") {
123
+ rt.queueStore.clear(item.id);
124
+ ctx.ui.notify(`Cleared idea ${item.id}`, "info");
125
+ requestQueueRender(rt);
126
+ return;
127
+ }
128
+
129
+ ctx.ui.notify(
130
+ "Usage: /ideas next | /ideas issue [id] | /ideas [send|retry|clear|edit] <id>",
131
+ "info",
132
+ );
133
+ },
134
+ });
135
+
136
+ pi.registerCommand("queue", {
137
+ description: "Manage Powerline queued prompts and project aliases",
138
+ handler: async (args, ctx) => {
139
+ rt.currentCtx = ctx;
140
+ const parts = args.trim().split(/\s+/).filter(Boolean);
141
+ const action = parts[0];
142
+
143
+ if (!action) {
144
+ await openQueuePicker(pi, rt, ctx, "queue");
145
+ return;
146
+ }
147
+
148
+ if (action === "alias") {
149
+ const alias = parts[1];
150
+ const aliasPath = parts.slice(2).join(" ") || ctx.cwd || process.cwd();
151
+ if (!alias) {
152
+ ctx.ui.notify("Usage: /queue alias <name> [path]", "info");
153
+ return;
154
+ }
155
+ try {
156
+ rt.queueStore.setAlias(alias, aliasPath);
157
+ ctx.ui.notify(`Alias @${alias} → ${aliasPath}`, "info");
158
+ } catch (error) {
159
+ ctx.ui.notify(
160
+ error instanceof Error ? error.message : String(error),
161
+ "error",
162
+ );
163
+ }
164
+ return;
165
+ }
166
+
167
+ if (action === "send" || action === "retry") {
168
+ const id = parts[1];
169
+ if (!id) {
170
+ const item = rt.queueStore.queuedDeliveryItems(
171
+ getQueueContext(ctx),
172
+ )[0];
173
+ if (!item) {
174
+ ctx.ui.notify("No queued item to send", "info");
175
+ return;
176
+ }
177
+ sendOrRetryQueueItem(pi, rt, ctx, item.id);
178
+ return;
179
+ }
180
+ sendOrRetryQueueItem(pi, rt, ctx, id);
181
+ return;
182
+ }
183
+
184
+ if (action === "clear") {
185
+ const id = parts[1];
186
+ if (id === "all") {
187
+ const active = rt.queueStore
188
+ .activeItems(getQueueContext(ctx))
189
+ .filter((item) => item.intent !== "idea");
190
+ for (const item of active) rt.queueStore.clear(item.id);
191
+ ctx.ui.notify(
192
+ `Cleared ${active.length} queued item${active.length === 1 ? "" : "s"}`,
193
+ "info",
194
+ );
195
+ requestQueueRender(rt);
196
+ return;
197
+ }
198
+ if (!id) {
199
+ ctx.ui.notify("Usage: /queue clear <id|all>", "info");
200
+ return;
201
+ }
202
+ const item = rt.queueStore.get(id);
203
+ if (!item || item.intent === "idea") {
204
+ ctx.ui.notify(`No unique queued item matches ${id}`, "warning");
205
+ return;
206
+ }
207
+ rt.queueStore.clear(item.id);
208
+ ctx.ui.notify(`Cleared ${item.id}`, "info");
209
+ requestQueueRender(rt);
210
+ return;
211
+ }
212
+
213
+ if (action === "target") {
214
+ const id = parts[1];
215
+ const spec = parts[2];
216
+ if (!id || !spec) {
217
+ ctx.ui.notify(
218
+ "Usage: /queue target <id> @alias|global|current",
219
+ "info",
220
+ );
221
+ return;
222
+ }
223
+ const item = rt.queueStore.get(id);
224
+ if (!item) {
225
+ ctx.ui.notify(`No unique queue item matches ${id}`, "warning");
226
+ return;
227
+ }
228
+ try {
229
+ const target = resolveCommandTarget(rt, ctx, spec);
230
+ rt.queueStore.update(item.id, { target });
231
+ ctx.ui.notify(`Retargeted ${item.id}`, "info");
232
+ requestQueueRender(rt);
233
+ } catch (error) {
234
+ ctx.ui.notify(
235
+ error instanceof Error ? error.message : String(error),
236
+ "error",
237
+ );
238
+ }
239
+ return;
240
+ }
241
+
242
+ ctx.ui.notify("Usage: /queue [send|retry|clear|target|alias]", "info");
243
+ },
244
+ });
245
+ }
@@ -0,0 +1,12 @@
1
+ import { currentQueueContext } from "../../queue/store.ts";
2
+
3
+ export function getQueueSessionId(ctx: any): string | undefined {
4
+ const sessionId = ctx.sessionManager?.getSessionId?.();
5
+ return typeof sessionId === "string" && sessionId.trim()
6
+ ? sessionId
7
+ : undefined;
8
+ }
9
+
10
+ export function getQueueContext(ctx: any) {
11
+ return currentQueueContext(ctx.cwd ?? process.cwd(), getQueueSessionId(ctx));
12
+ }
@@ -0,0 +1,434 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { SelectItem } from "@earendil-works/pi-tui";
3
+
4
+ import {
5
+ formatIdeaIssuePrompt,
6
+ formatQueueDeliveryText,
7
+ parseTargetPrefix,
8
+ targetForIdea,
9
+ } from "../../queue/store.ts";
10
+ import type {
11
+ PowerlineQueueItem,
12
+ QueueIntent,
13
+ QueueTarget,
14
+ } from "../../queue/types.ts";
15
+ import { requestImmediateStatusRender } from "./segment-context.ts";
16
+ import { buildStashPreview } from "./stash-history.ts";
17
+ import { showSelectOverlay } from "./menu-views.ts";
18
+ import { getQueueContext } from "./queue-context.ts";
19
+ import { isStaleExtensionContextError } from "./stale-context.ts";
20
+ import { config } from "./state.ts";
21
+ import type { RuntimeState } from "./types.ts";
22
+
23
+ export function requestQueueRender(rt: RuntimeState): void {
24
+ requestImmediateStatusRender(rt, { deferDuringTyping: false });
25
+ }
26
+
27
+ export function queueItemLabel(item: PowerlineQueueItem): string {
28
+ const status =
29
+ item.status === "queued" ? item.intent : `${item.intent}/${item.status}`;
30
+ return `${item.id} ${status} ${buildStashPreview(item.text, 56)}`;
31
+ }
32
+
33
+ export function queueItemDescription(item: PowerlineQueueItem): string {
34
+ if (item.target.kind === "global") return "global";
35
+ if (item.target.kind === "current-session") return "current session";
36
+ return item.target.alias ? `@${item.target.alias}` : item.target.cwd;
37
+ }
38
+
39
+ export function captureQueueItem(
40
+ rt: RuntimeState,
41
+ ctx: any,
42
+ text: string,
43
+ intent: QueueIntent,
44
+ target: QueueTarget,
45
+ ): PowerlineQueueItem {
46
+ const item = rt.queueStore.add({
47
+ text,
48
+ intent,
49
+ target,
50
+ source: getQueueContext(ctx),
51
+ });
52
+ requestQueueRender(rt);
53
+ return item;
54
+ }
55
+
56
+ export function captureIdeaFromParsedInput(
57
+ rt: RuntimeState,
58
+ ctx: any,
59
+ parsed: { target: string | null; text: string },
60
+ ): PowerlineQueueItem | null {
61
+ const trimmed = parsed.text.trim();
62
+ if (!trimmed) {
63
+ ctx.ui.notify("Nothing to capture", "info");
64
+ return null;
65
+ }
66
+
67
+ const target = targetForIdea(
68
+ parsed.target,
69
+ rt.queueStore,
70
+ ctx.cwd ?? process.cwd(),
71
+ );
72
+ const item = captureQueueItem(rt, ctx, trimmed, "idea", target);
73
+ ctx.ui.notify(`Idea saved (${item.id}) — /ideas to review`, "info");
74
+ return item;
75
+ }
76
+
77
+ export function captureIdeaFromText(
78
+ rt: RuntimeState,
79
+ ctx: any,
80
+ text: string,
81
+ ): PowerlineQueueItem | null {
82
+ return captureIdeaFromParsedInput(rt, ctx, parseTargetPrefix(text));
83
+ }
84
+
85
+ export function captureCurrentProjectIdea(
86
+ rt: RuntimeState,
87
+ ctx: any,
88
+ text: string,
89
+ ): PowerlineQueueItem | null {
90
+ const trimmed = text.trim();
91
+ if (!trimmed) {
92
+ ctx.ui.notify("Nothing to capture", "info");
93
+ return null;
94
+ }
95
+
96
+ const item = captureQueueItem(rt, ctx, trimmed, "idea", {
97
+ kind: "project",
98
+ cwd: ctx.cwd ?? process.cwd(),
99
+ });
100
+ ctx.ui.notify(`Idea saved (${item.id}) — /ideas to review`, "info");
101
+ return item;
102
+ }
103
+
104
+ export function isSigilIdeaDraft(text: string): boolean {
105
+ const sigil = config.queue.captureSigil;
106
+ if (sigil === false) return false;
107
+ const normalizedSigil = sigil.trim();
108
+ if (!normalizedSigil) return false;
109
+ const trimmed = text.trimStart();
110
+ return (
111
+ trimmed.startsWith(normalizedSigil) &&
112
+ /^\s/.test(trimmed.slice(normalizedSigil.length))
113
+ );
114
+ }
115
+
116
+ export function captureSigilGlyph(): string {
117
+ const sigil =
118
+ config.queue.captureSigil === false ? "#" : config.queue.captureSigil;
119
+ return Array.from(sigil)[0] ?? "#";
120
+ }
121
+
122
+ export function capturePostCompactPrompt(
123
+ rt: RuntimeState,
124
+ ctx: any,
125
+ text: string,
126
+ ): PowerlineQueueItem | null {
127
+ const trimmed = text.trim();
128
+ if (!trimmed) return null;
129
+
130
+ const item = captureQueueItem(rt, ctx, trimmed, "post-compact", {
131
+ kind: "current-session",
132
+ });
133
+ ctx.ui.notify(`Queued for after compaction (${item.id})`, "info");
134
+ return item;
135
+ }
136
+
137
+ export function deliveryModeForItem(
138
+ ctx: any,
139
+ item: PowerlineQueueItem,
140
+ ): "steer" | "followUp" | undefined {
141
+ const idle = typeof ctx.isIdle === "function" ? ctx.isIdle() : true;
142
+ if (idle) return undefined;
143
+ return item.intent === "steer" ? "steer" : "followUp";
144
+ }
145
+
146
+ export function deliverQueueItem(
147
+ pi: ExtensionAPI,
148
+ rt: RuntimeState,
149
+ ctx: any,
150
+ item: PowerlineQueueItem,
151
+ ): boolean {
152
+ if (rt.powerlineCompacting) {
153
+ rt.queueStore.update(item.id, { status: "queued" });
154
+ requestQueueRender(rt);
155
+ return false;
156
+ }
157
+
158
+ rt.queueStore.update(item.id, { status: "delivering", error: undefined });
159
+ requestQueueRender(rt);
160
+
161
+ let sent = false;
162
+ try {
163
+ const deliverAs = deliveryModeForItem(ctx, item);
164
+ const deliveryText = formatQueueDeliveryText(item);
165
+ if (deliverAs) {
166
+ pi.sendUserMessage(deliveryText, { deliverAs });
167
+ } else {
168
+ pi.sendUserMessage(deliveryText);
169
+ }
170
+ sent = true;
171
+ rt.queueStore.update(item.id, { status: "sent", error: undefined });
172
+ try {
173
+ ctx.ui.notify(`Sent queued item ${item.id}`, "info");
174
+ } catch (error) {
175
+ if (!isStaleExtensionContextError(error)) throw error;
176
+ }
177
+ requestQueueRender(rt);
178
+ return true;
179
+ } catch (error) {
180
+ if (isStaleExtensionContextError(error)) {
181
+ // The extension context was replaced (reload/session swap) mid-delivery.
182
+ // Leave the item queued so it can be retried instead of marking it failed.
183
+ if (!sent) {
184
+ rt.queueStore.update(item.id, { status: "queued", error: undefined });
185
+ requestQueueRender(rt);
186
+ }
187
+ return false;
188
+ }
189
+ const message = error instanceof Error ? error.message : String(error);
190
+ rt.queueStore.update(item.id, { status: "failed", error: message });
191
+ ctx.ui.notify(`Failed to send ${item.id}: ${message}`, "error");
192
+ requestQueueRender(rt);
193
+ return false;
194
+ }
195
+ }
196
+
197
+ export function schedulePostCompactionDelivery(
198
+ pi: ExtensionAPI,
199
+ rt: RuntimeState,
200
+ ctx: any,
201
+ ): void {
202
+ if (rt.queueDeliveryTimer) clearTimeout(rt.queueDeliveryTimer);
203
+ const queueContext = getQueueContext(ctx);
204
+ const scheduledGeneration = rt.sessionGeneration;
205
+ rt.queueDeliveryTimer = setTimeout(() => {
206
+ rt.queueDeliveryTimer = null;
207
+ if (scheduledGeneration !== rt.sessionGeneration) return;
208
+ const item = rt.queueStore.queuedDeliveryItems(queueContext, "post-compact")[0];
209
+ if (!item) return;
210
+ try {
211
+ deliverQueueItem(pi, rt, ctx, item);
212
+ } catch (error) {
213
+ if (!isStaleExtensionContextError(error)) throw error;
214
+ }
215
+ }, 50);
216
+ }
217
+
218
+ export function blockPostCompactionQueue(
219
+ rt: RuntimeState,
220
+ ctx: any,
221
+ errorMessage: string,
222
+ ): void {
223
+ const items = rt.queueStore.queuedDeliveryItems(
224
+ getQueueContext(ctx),
225
+ "post-compact",
226
+ );
227
+ for (const item of items) {
228
+ rt.queueStore.update(item.id, { status: "blocked", error: errorMessage });
229
+ }
230
+ if (items.length > 0) {
231
+ ctx.ui.notify(
232
+ `Kept ${items.length} post-compaction item${items.length === 1 ? "" : "s"} blocked`,
233
+ "warning",
234
+ );
235
+ requestQueueRender(rt);
236
+ }
237
+ }
238
+
239
+ export function finishFailedCompaction(
240
+ rt: RuntimeState,
241
+ ctx: any,
242
+ errorMessage: string,
243
+ ): void {
244
+ rt.powerlineCompacting = false;
245
+ rt.deliverAfterRetrySettles = false;
246
+ blockPostCompactionQueue(rt, ctx, errorMessage);
247
+ requestQueueRender(rt);
248
+ }
249
+
250
+ export async function chooseQueueAction(
251
+ pi: ExtensionAPI,
252
+ rt: RuntimeState,
253
+ ctx: any,
254
+ item: PowerlineQueueItem,
255
+ ): Promise<void> {
256
+ const actions: SelectItem[] = [
257
+ {
258
+ value: "send",
259
+ label: "Send to current session",
260
+ description: "Deliver as prompt/follow-up",
261
+ },
262
+ {
263
+ value: "edit",
264
+ label: "Edit in prompt",
265
+ description: "Move text into the editor",
266
+ },
267
+ {
268
+ value: "retry",
269
+ label: "Retry",
270
+ description: "Mark queued and deliver",
271
+ },
272
+ { value: "clear", label: "Clear", description: "Mark item done" },
273
+ { value: "cancel", label: "Cancel" },
274
+ ];
275
+ const selected = await showSelectOverlay(
276
+ ctx,
277
+ `Queue item ${item.id}`,
278
+ buildStashPreview(item.text, 72),
279
+ actions,
280
+ actions.length,
281
+ );
282
+ if (!selected || selected.value === "cancel") return;
283
+
284
+ if (selected.value === "send") {
285
+ deliverQueueItem(pi, rt, ctx, item);
286
+ return;
287
+ }
288
+
289
+ if (selected.value === "retry") {
290
+ const updated = rt.queueStore.update(item.id, {
291
+ status: "queued",
292
+ error: undefined,
293
+ });
294
+ if (updated) deliverQueueItem(pi, rt, ctx, updated);
295
+ return;
296
+ }
297
+
298
+ if (selected.value === "edit") {
299
+ ctx.ui.setEditorText(item.text);
300
+ rt.queueStore.clear(item.id);
301
+ requestQueueRender(rt);
302
+ return;
303
+ }
304
+
305
+ if (selected.value === "clear") {
306
+ rt.queueStore.clear(item.id);
307
+ ctx.ui.notify(`Cleared ${item.id}`, "info");
308
+ requestQueueRender(rt);
309
+ }
310
+ }
311
+
312
+ export async function openQueuePicker(
313
+ pi: ExtensionAPI,
314
+ rt: RuntimeState,
315
+ ctx: any,
316
+ mode: "ideas" | "queue",
317
+ ): Promise<void> {
318
+ const active = rt.queueStore
319
+ .activeItems(getQueueContext(ctx))
320
+ .filter((item) =>
321
+ mode === "ideas" ? item.intent === "idea" : item.intent !== "idea",
322
+ );
323
+ if (active.length === 0) {
324
+ ctx.ui.notify(
325
+ mode === "ideas" ? "No ideas captured" : "No queued items",
326
+ "info",
327
+ );
328
+ return;
329
+ }
330
+
331
+ const items: SelectItem[] = active.map((item) => ({
332
+ value: item.id,
333
+ label: queueItemLabel(item),
334
+ description: queueItemDescription(item),
335
+ }));
336
+ const selected = await showSelectOverlay(
337
+ ctx,
338
+ mode === "ideas" ? "Powerline ideas" : "Powerline queue",
339
+ "↑↓ navigate • enter manage • esc cancel",
340
+ items,
341
+ Math.min(active.length, 12),
342
+ );
343
+ if (!selected) return;
344
+
345
+ const item = active.find((candidate) => candidate.id === selected.value);
346
+ if (item) await chooseQueueAction(pi, rt, ctx, item);
347
+ }
348
+
349
+ export function resolveCommandTarget(
350
+ rt: RuntimeState,
351
+ ctx: any,
352
+ spec: string,
353
+ ): QueueTarget {
354
+ const normalized = spec.trim().replace(/^@/, "");
355
+ return targetForIdea(
356
+ normalized || null,
357
+ rt.queueStore,
358
+ ctx.cwd ?? process.cwd(),
359
+ );
360
+ }
361
+
362
+ export function sendOrRetryQueueItem(
363
+ pi: ExtensionAPI,
364
+ rt: RuntimeState,
365
+ ctx: any,
366
+ idPrefix: string,
367
+ ): void {
368
+ const item = rt.queueStore.get(idPrefix);
369
+ if (!item) {
370
+ ctx.ui.notify(`No unique queue item matches ${idPrefix}`, "warning");
371
+ return;
372
+ }
373
+ const updated = rt.queueStore.update(item.id, {
374
+ status: "queued",
375
+ error: undefined,
376
+ });
377
+ if (updated) deliverQueueItem(pi, rt, ctx, updated);
378
+ }
379
+
380
+ export function findNextIdea(
381
+ rt: RuntimeState,
382
+ ctx: any,
383
+ ): PowerlineQueueItem | null {
384
+ return (
385
+ rt.queueStore
386
+ .activeItems(getQueueContext(ctx))
387
+ .find((candidate) => candidate.intent === "idea") ?? null
388
+ );
389
+ }
390
+
391
+ export function sendIdeaIssueHandoff(
392
+ pi: ExtensionAPI,
393
+ rt: RuntimeState,
394
+ ctx: any,
395
+ item: PowerlineQueueItem,
396
+ ): void {
397
+ rt.queueStore.update(item.id, { status: "delivering", error: undefined });
398
+ requestQueueRender(rt);
399
+
400
+ try {
401
+ const deliverAs = deliveryModeForItem(ctx, item);
402
+ const issuePrompt = formatIdeaIssuePrompt(item);
403
+ if (deliverAs) {
404
+ pi.sendUserMessage(issuePrompt, { deliverAs });
405
+ } else {
406
+ pi.sendUserMessage(issuePrompt);
407
+ }
408
+ rt.queueStore.update(item.id, { status: "sent", error: undefined });
409
+ ctx.ui.notify(`Sent idea ${item.id} for issue triage`, "info");
410
+ requestQueueRender(rt);
411
+ } catch (error) {
412
+ const message = error instanceof Error ? error.message : String(error);
413
+ rt.queueStore.update(item.id, { status: "failed", error: message });
414
+ ctx.ui.notify(`Failed to send ${item.id}: ${message}`, "error");
415
+ requestQueueRender(rt);
416
+ }
417
+ }
418
+
419
+ export function sendIdeaIssueHandoffById(
420
+ pi: ExtensionAPI,
421
+ rt: RuntimeState,
422
+ ctx: any,
423
+ id: string | undefined,
424
+ ): void {
425
+ const item = id ? rt.queueStore.get(id) : findNextIdea(rt, ctx);
426
+ if (!item || item.intent !== "idea") {
427
+ ctx.ui.notify(
428
+ id ? `No unique idea matches ${id}` : "No ideas captured",
429
+ id ? "warning" : "info",
430
+ );
431
+ return;
432
+ }
433
+ sendIdeaIssueHandoff(pi, rt, ctx, item);
434
+ }