@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
package/queue/store.ts ADDED
@@ -0,0 +1,443 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import { dirname, resolve } from "node:path";
10
+ import { randomUUID } from "node:crypto";
11
+ import { getAgentPath } from "../src/paths/agent-dirs.ts";
12
+ import type {
13
+ CreateQueueItemInput,
14
+ PowerlineQueueItem,
15
+ QueueAliasMap,
16
+ QueueContext,
17
+ QueueIntent,
18
+ QueueStatus,
19
+ QueueSummary,
20
+ QueueTarget,
21
+ } from "./types.ts";
22
+ import { ACTIVE_QUEUE_STATUSES } from "./types.ts";
23
+
24
+ const STORE_DIR = "powerline-footer";
25
+ const INBOX_FILE = "inbox.jsonl";
26
+ const ALIASES_FILE = "projects.json";
27
+ const LOCK_RETRY_MS = 25;
28
+ const LOCK_TIMEOUT_MS = 2000;
29
+
30
+ function isRecord(value: unknown): value is Record<string, unknown> {
31
+ return typeof value === "object" && value !== null && !Array.isArray(value);
32
+ }
33
+
34
+ function normalizeCwd(cwd: string): string {
35
+ return resolve(cwd);
36
+ }
37
+
38
+ function normalizeOptionalString(value: unknown): string | undefined {
39
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
40
+ }
41
+
42
+ function normalizeTarget(value: unknown): QueueTarget | null {
43
+ if (!isRecord(value)) return null;
44
+ if (value.kind === "current-session") return { kind: "current-session" };
45
+ if (value.kind === "global") return { kind: "global" };
46
+ if (
47
+ value.kind === "project" &&
48
+ typeof value.cwd === "string" &&
49
+ value.cwd.trim()
50
+ ) {
51
+ const alias = normalizeOptionalString(value.alias);
52
+ return alias
53
+ ? { kind: "project", cwd: normalizeCwd(value.cwd), alias }
54
+ : { kind: "project", cwd: normalizeCwd(value.cwd) };
55
+ }
56
+ return null;
57
+ }
58
+
59
+ function normalizeIntent(value: unknown): QueueIntent | null {
60
+ return value === "steer" ||
61
+ value === "follow-up" ||
62
+ value === "post-compact" ||
63
+ value === "idea"
64
+ ? value
65
+ : null;
66
+ }
67
+
68
+ function normalizeStatus(value: unknown): QueueStatus | null {
69
+ return value === "queued" ||
70
+ value === "blocked" ||
71
+ value === "delivering" ||
72
+ value === "sent" ||
73
+ value === "failed"
74
+ ? value
75
+ : null;
76
+ }
77
+
78
+ function normalizeItem(value: unknown): PowerlineQueueItem | null {
79
+ if (!isRecord(value)) return null;
80
+ if (typeof value.id !== "string" || !value.id.trim()) return null;
81
+ if (typeof value.text !== "string" || !value.text.trim()) return null;
82
+ if (typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt))
83
+ return null;
84
+ if (typeof value.updatedAt !== "number" || !Number.isFinite(value.updatedAt))
85
+ return null;
86
+ if (
87
+ !isRecord(value.source) ||
88
+ typeof value.source.cwd !== "string" ||
89
+ !value.source.cwd.trim()
90
+ )
91
+ return null;
92
+
93
+ const target = normalizeTarget(value.target);
94
+ const intent = normalizeIntent(value.intent);
95
+ const status = normalizeStatus(value.status);
96
+ if (!target || !intent || !status) return null;
97
+
98
+ const sessionId = normalizeOptionalString(value.source.sessionId);
99
+ const error = normalizeOptionalString(value.error);
100
+
101
+ return {
102
+ id: value.id.trim(),
103
+ text: value.text,
104
+ createdAt: value.createdAt,
105
+ updatedAt: value.updatedAt,
106
+ source: sessionId
107
+ ? { cwd: normalizeCwd(value.source.cwd), sessionId }
108
+ : { cwd: normalizeCwd(value.source.cwd) },
109
+ target,
110
+ intent,
111
+ status,
112
+ ...(error ? { error } : {}),
113
+ };
114
+ }
115
+
116
+ function readJsonObject(path: string): Record<string, unknown> {
117
+ if (!existsSync(path)) return {};
118
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
119
+ return isRecord(parsed) ? parsed : {};
120
+ }
121
+
122
+ function sleepSync(ms: number): void {
123
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
124
+ }
125
+
126
+ export function getQueueStorePaths(): {
127
+ inboxPath: string;
128
+ aliasesPath: string;
129
+ } {
130
+ return {
131
+ inboxPath: getAgentPath(STORE_DIR, INBOX_FILE),
132
+ aliasesPath: getAgentPath(STORE_DIR, ALIASES_FILE),
133
+ };
134
+ }
135
+
136
+ export function createQueueItem(
137
+ input: CreateQueueItemInput,
138
+ ): PowerlineQueueItem {
139
+ const now = input.now ?? Date.now();
140
+ const sourceSessionId = input.source.sessionId?.trim();
141
+ return {
142
+ id: randomUUID().slice(0, 8),
143
+ text: input.text,
144
+ createdAt: now,
145
+ updatedAt: now,
146
+ source: sourceSessionId
147
+ ? { cwd: normalizeCwd(input.source.cwd), sessionId: sourceSessionId }
148
+ : { cwd: normalizeCwd(input.source.cwd) },
149
+ target: normalizeTarget(input.target) ?? input.target,
150
+ intent: input.intent,
151
+ status: input.status ?? "queued",
152
+ };
153
+ }
154
+
155
+ export class PowerlineQueueStore {
156
+ private readonly inboxPath: string;
157
+ private readonly aliasesPath: string;
158
+
159
+ constructor(
160
+ inboxPath: string = getQueueStorePaths().inboxPath,
161
+ aliasesPath: string = getQueueStorePaths().aliasesPath,
162
+ ) {
163
+ this.inboxPath = inboxPath;
164
+ this.aliasesPath = aliasesPath;
165
+ }
166
+
167
+ list(): PowerlineQueueItem[] {
168
+ if (!existsSync(this.inboxPath)) return [];
169
+ const lines = readFileSync(this.inboxPath, "utf-8").split("\n");
170
+ const items: PowerlineQueueItem[] = [];
171
+ for (const line of lines) {
172
+ const trimmed = line.trim();
173
+ if (!trimmed) continue;
174
+ try {
175
+ const item = normalizeItem(JSON.parse(trimmed));
176
+ if (item) items.push(item);
177
+ } catch {
178
+ // Ignore malformed internal lines rather than breaking the footer during startup.
179
+ }
180
+ }
181
+ return items.sort((a, b) => a.createdAt - b.createdAt);
182
+ }
183
+
184
+ add(input: CreateQueueItemInput): PowerlineQueueItem {
185
+ const item = createQueueItem(input);
186
+ return this.withStoreLock(() => {
187
+ this.writeItems([...this.list(), item]);
188
+ return item;
189
+ });
190
+ }
191
+
192
+ get(idPrefix: string): PowerlineQueueItem | null {
193
+ const normalized = idPrefix.trim();
194
+ if (!normalized) return null;
195
+ const matches = this.list().filter(
196
+ (item) => item.id === normalized || item.id.startsWith(normalized),
197
+ );
198
+ return matches.length === 1 ? matches[0] : null;
199
+ }
200
+
201
+ update(
202
+ id: string,
203
+ updates: Partial<Omit<PowerlineQueueItem, "id" | "createdAt">>,
204
+ ): PowerlineQueueItem | null {
205
+ return this.withStoreLock(() => {
206
+ let updated: PowerlineQueueItem | null = null;
207
+ const next = this.list().map((item) => {
208
+ if (item.id !== id) return item;
209
+ updated = {
210
+ ...item,
211
+ ...updates,
212
+ updatedAt: updates.updatedAt ?? Date.now(),
213
+ };
214
+ return updated;
215
+ });
216
+ if (updated) this.writeItems(next);
217
+ return updated;
218
+ });
219
+ }
220
+
221
+ clear(id: string): PowerlineQueueItem | null {
222
+ return this.update(id, { status: "sent", error: undefined });
223
+ }
224
+
225
+ activeItems(context: QueueContext): PowerlineQueueItem[] {
226
+ return this.list().filter((item) => isActiveForContext(item, context));
227
+ }
228
+
229
+ queuedDeliveryItems(
230
+ context: QueueContext,
231
+ intent?: QueueIntent,
232
+ ): PowerlineQueueItem[] {
233
+ return this.activeItems(context).filter((item) => {
234
+ if (item.status !== "queued") return false;
235
+ if (item.intent === "idea") return false;
236
+ return intent ? item.intent === intent : true;
237
+ });
238
+ }
239
+
240
+ summarize(context: QueueContext, compacting: boolean): QueueSummary {
241
+ const active = this.activeItems(context);
242
+ const queueItems = active.filter((item) => item.intent !== "idea");
243
+ const ideaItems = active.filter((item) => item.intent === "idea");
244
+ const blockedItems = active.filter(
245
+ (item) => item.status === "blocked" || item.status === "failed",
246
+ );
247
+ const leading = [...blockedItems, ...queueItems, ...ideaItems][0] ?? null;
248
+
249
+ return {
250
+ queueCount: queueItems.length,
251
+ ideaCount: ideaItems.length,
252
+ blockedCount: blockedItems.length,
253
+ compacting,
254
+ leadingText: leading?.text ?? null,
255
+ leadingIntent: leading?.intent ?? null,
256
+ leadingStatus: leading?.status ?? null,
257
+ };
258
+ }
259
+
260
+ readAliases(): QueueAliasMap {
261
+ const parsed = readJsonObject(this.aliasesPath);
262
+ const aliases: QueueAliasMap = {};
263
+ for (const [alias, cwd] of Object.entries(parsed)) {
264
+ if (
265
+ /^[a-zA-Z0-9_-]+$/.test(alias) &&
266
+ typeof cwd === "string" &&
267
+ cwd.trim()
268
+ ) {
269
+ aliases[alias] = normalizeCwd(cwd);
270
+ }
271
+ }
272
+ return aliases;
273
+ }
274
+
275
+ setAlias(alias: string, cwd: string): QueueAliasMap {
276
+ const normalizedAlias = alias.trim();
277
+ if (!/^[a-zA-Z0-9_-]+$/.test(normalizedAlias)) {
278
+ throw new Error(
279
+ "Alias must contain only letters, numbers, dashes, or underscores",
280
+ );
281
+ }
282
+ return this.withStoreLock(() => {
283
+ const aliases = {
284
+ ...this.readAliases(),
285
+ [normalizedAlias]: normalizeCwd(cwd),
286
+ };
287
+ this.writeJson(this.aliasesPath, aliases);
288
+ return aliases;
289
+ });
290
+ }
291
+
292
+ resolveAlias(alias: string): string | null {
293
+ return this.readAliases()[alias] ?? null;
294
+ }
295
+
296
+ private withStoreLock<T>(fn: () => T): T {
297
+ mkdirSync(dirname(this.inboxPath), { recursive: true });
298
+ const lockPath = `${this.inboxPath}.lock`;
299
+ const startedAt = Date.now();
300
+
301
+ while (true) {
302
+ try {
303
+ mkdirSync(lockPath);
304
+ break;
305
+ } catch (error) {
306
+ const code =
307
+ error && typeof error === "object" && "code" in error
308
+ ? error.code
309
+ : undefined;
310
+ if (code !== "EEXIST") throw error;
311
+
312
+ if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
313
+ throw new Error("Timed out waiting for Powerline queue store lock");
314
+ }
315
+ sleepSync(LOCK_RETRY_MS);
316
+ }
317
+ }
318
+
319
+ try {
320
+ return fn();
321
+ } finally {
322
+ rmSync(lockPath, { recursive: true, force: true });
323
+ }
324
+ }
325
+
326
+ private writeItems(items: readonly PowerlineQueueItem[]): void {
327
+ mkdirSync(dirname(this.inboxPath), { recursive: true });
328
+ const activeOrRecent = items
329
+ .filter(
330
+ (item) =>
331
+ item.status !== "sent" ||
332
+ Date.now() - item.updatedAt < 24 * 60 * 60 * 1000,
333
+ )
334
+ .map((item) => JSON.stringify(item))
335
+ .join("\n");
336
+ this.writeAtomic(
337
+ this.inboxPath,
338
+ activeOrRecent ? `${activeOrRecent}\n` : "",
339
+ );
340
+ }
341
+
342
+ private writeJson(path: string, value: unknown): void {
343
+ mkdirSync(dirname(path), { recursive: true });
344
+ this.writeAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
345
+ }
346
+
347
+ private writeAtomic(path: string, content: string): void {
348
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
349
+ writeFileSync(tempPath, content, "utf-8");
350
+ renameSync(tempPath, path);
351
+ }
352
+ }
353
+
354
+ export function currentQueueContext(
355
+ cwd: string,
356
+ sessionId?: string,
357
+ ): QueueContext {
358
+ return sessionId?.trim()
359
+ ? { cwd: normalizeCwd(cwd), sessionId: sessionId.trim() }
360
+ : { cwd: normalizeCwd(cwd) };
361
+ }
362
+
363
+ export function isActiveForContext(
364
+ item: PowerlineQueueItem,
365
+ context: QueueContext,
366
+ ): boolean {
367
+ if (!ACTIVE_QUEUE_STATUSES.has(item.status)) return false;
368
+ const currentCwd = normalizeCwd(context.cwd);
369
+ if (item.target.kind === "global") return true;
370
+ if (item.target.kind === "project")
371
+ return normalizeCwd(item.target.cwd) === currentCwd;
372
+ if (item.source.sessionId) return context.sessionId === item.source.sessionId;
373
+ return item.source.cwd === currentCwd;
374
+ }
375
+
376
+ export function targetForIdea(
377
+ rawTarget: string | null,
378
+ store: PowerlineQueueStore,
379
+ cwd: string,
380
+ ): QueueTarget {
381
+ if (!rawTarget) return { kind: "project", cwd: normalizeCwd(cwd) };
382
+ if (rawTarget === "current") return { kind: "current-session" };
383
+ if (rawTarget === "global") return { kind: "global" };
384
+
385
+ const aliasCwd = store.resolveAlias(rawTarget);
386
+ if (!aliasCwd) {
387
+ throw new Error(
388
+ `Unknown project alias @${rawTarget}. Use /queue alias ${rawTarget} <path> first.`,
389
+ );
390
+ }
391
+ return { kind: "project", cwd: aliasCwd, alias: rawTarget };
392
+ }
393
+
394
+ export function parseTargetPrefix(text: string): {
395
+ target: string | null;
396
+ text: string;
397
+ } {
398
+ const trimmed = text.trim();
399
+ const match = /^@([a-zA-Z0-9_-]+)(?:\s+|$)/.exec(trimmed);
400
+ if (!match) return { target: null, text: trimmed };
401
+ return { target: match[1], text: trimmed.slice(match[0].length).trim() };
402
+ }
403
+
404
+ export function parseSigilIdeaCapture(
405
+ text: string,
406
+ sigil: string | false,
407
+ ): { target: string | null; text: string } | null {
408
+ if (sigil === false) return null;
409
+ const normalizedSigil = sigil.trim();
410
+ if (!normalizedSigil) return null;
411
+
412
+ const trimmed = text.trim();
413
+ if (!trimmed.startsWith(normalizedSigil)) return null;
414
+
415
+ const afterSigil = trimmed.slice(normalizedSigil.length);
416
+ if (!/^\s/.test(afterSigil)) return null;
417
+
418
+ const parsed = parseTargetPrefix(afterSigil.trim());
419
+ return parsed.text ? parsed : null;
420
+ }
421
+
422
+ export function formatQueueDeliveryText(item: PowerlineQueueItem): string {
423
+ if (item.intent !== "idea") return item.text;
424
+ return `[powerline idea ${item.id}, captured ${new Date(item.createdAt).toISOString()} from ${item.source.cwd}]\n${item.text}`;
425
+ }
426
+
427
+ export function formatIdeaIssuePrompt(item: PowerlineQueueItem): string {
428
+ const target =
429
+ item.target.kind === "project"
430
+ ? `project ${item.target.alias ? `@${item.target.alias} ` : ""}${item.target.cwd}`
431
+ : item.target.kind;
432
+
433
+ return `Please process this saved Powerline idea as a GitHub issue candidate.\n\n${formatQueueDeliveryText(item)}\n\nIssue filing rules:\n- If subagents are available, spawn one low-budget issue-filing lane for this idea; otherwise do the same checks directly.\n- First identify the target repository from the idea target (${target}), source cwd (${item.source.cwd}), and current session context.\n- If the target repository is unclear or is not owned/controlled by the user, ask before filing anything.\n- If the target repository is clear and owned/controlled by the user, dedupe against existing open issues first.\n- If a matching open issue already exists, report it and do not create another issue.\n- If no matching issue exists, create one self-contained GitHub issue in that repository with a clear title, context, acceptance criteria, and the Powerline idea provenance above.\n- Use explicit repository targeting for GitHub commands and do not change source files for this handoff.`;
434
+ }
435
+
436
+ export function parseCompactQueuedPrompt(text: string): string | null {
437
+ const trimmed = text.trim();
438
+ const match = /^\/compact\s+(.+)$/.exec(trimmed);
439
+ if (!match) return null;
440
+
441
+ const prompt = match[1].trim();
442
+ return prompt ? prompt : null;
443
+ }
package/queue/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ export type QueueIntent = "steer" | "follow-up" | "post-compact" | "idea";
2
+ export type QueueStatus = "queued" | "blocked" | "delivering" | "sent" | "failed";
3
+
4
+ export type QueueTarget =
5
+ | { kind: "current-session" }
6
+ | { kind: "project"; cwd: string; alias?: string }
7
+ | { kind: "global" };
8
+
9
+ export interface QueueSource {
10
+ cwd: string;
11
+ sessionId?: string;
12
+ }
13
+
14
+ export interface PowerlineQueueItem {
15
+ id: string;
16
+ text: string;
17
+ createdAt: number;
18
+ updatedAt: number;
19
+ source: QueueSource;
20
+ target: QueueTarget;
21
+ intent: QueueIntent;
22
+ status: QueueStatus;
23
+ error?: string;
24
+ }
25
+
26
+ export interface QueueAliasMap {
27
+ [alias: string]: string;
28
+ }
29
+
30
+ export interface QueueSummary {
31
+ queueCount: number;
32
+ ideaCount: number;
33
+ blockedCount: number;
34
+ compacting: boolean;
35
+ leadingText: string | null;
36
+ leadingIntent: QueueIntent | null;
37
+ leadingStatus: QueueStatus | null;
38
+ }
39
+
40
+ export interface CreateQueueItemInput {
41
+ text: string;
42
+ source: QueueSource;
43
+ target: QueueTarget;
44
+ intent: QueueIntent;
45
+ status?: QueueStatus;
46
+ now?: number;
47
+ }
48
+
49
+ export interface QueueContext {
50
+ cwd: string;
51
+ sessionId?: string;
52
+ }
53
+
54
+ export const ACTIVE_QUEUE_STATUSES = new Set<QueueStatus>(["queued", "blocked", "delivering", "failed"]);
@@ -0,0 +1,182 @@
1
+ import {
2
+ isRecord,
3
+ normalizeCustomColor,
4
+ normalizeCustomItemId,
5
+ normalizeCustomItemPosition,
6
+ normalizeCustomPrefix,
7
+ normalizeCustomSegmentType,
8
+ normalizeSeparator,
9
+ } from "./primitives.ts";
10
+ import { normalizeSegmentOptions } from "./segment-options.ts";
11
+ import { normalizeStatusLineSegmentId } from "./segment-ids.ts";
12
+ import type {
13
+ ColorScheme,
14
+ CustomSegmentConfig,
15
+ CustomStatusItem,
16
+ StatusLineSegmentId,
17
+ } from "./types.ts";
18
+
19
+ export function normalizeCustomSegments(
20
+ raw: unknown,
21
+ ): Record<string, CustomSegmentConfig> {
22
+ const result: Record<string, CustomSegmentConfig> = {};
23
+ if (!isRecord(raw)) return result;
24
+
25
+ for (const [id, entry] of Object.entries(raw)) {
26
+ if (!normalizeCustomItemId(id)) continue;
27
+ if (!isRecord(entry)) continue;
28
+
29
+ const type = normalizeCustomSegmentType(entry.type);
30
+ if (!type) continue;
31
+
32
+ if (type === "command") {
33
+ if (typeof entry.command !== "string" || !entry.command.trim()) continue;
34
+ result[id] = {
35
+ type,
36
+ command: entry.command.trim(),
37
+ prefix: normalizeCustomPrefix(entry.prefix),
38
+ color: normalizeCustomColor(entry.color),
39
+ cacheMs:
40
+ typeof entry.cacheMs === "number" && entry.cacheMs >= 0
41
+ ? Math.floor(entry.cacheMs)
42
+ : undefined,
43
+ };
44
+ } else if (type === "env") {
45
+ if (typeof entry.env !== "string" || !entry.env.trim()) continue;
46
+ result[id] = {
47
+ type,
48
+ env: entry.env.trim(),
49
+ prefix: normalizeCustomPrefix(entry.prefix),
50
+ color: normalizeCustomColor(entry.color),
51
+ fallback:
52
+ typeof entry.fallback === "string" ? entry.fallback : undefined,
53
+ };
54
+ } else {
55
+ if (typeof entry.text !== "string") continue;
56
+ result[id] = {
57
+ type,
58
+ text: entry.text,
59
+ color: normalizeCustomColor(entry.color),
60
+ };
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ function normalizeCustomPresetSegmentList(
68
+ raw: unknown,
69
+ customItemIds: ReadonlySet<string>,
70
+ customSegmentIds: ReadonlySet<string>,
71
+ ): StatusLineSegmentId[] | undefined {
72
+ if (!Array.isArray(raw)) return undefined;
73
+ const result: StatusLineSegmentId[] = [];
74
+ for (const entry of raw) {
75
+ const id = normalizeStatusLineSegmentId(
76
+ entry,
77
+ customItemIds,
78
+ customSegmentIds,
79
+ );
80
+ if (id) result.push(id);
81
+ }
82
+ return result;
83
+ }
84
+
85
+ export function normalizeCustomPresets(
86
+ raw: unknown,
87
+ customItemIds: ReadonlySet<string>,
88
+ customSegmentIds: ReadonlySet<string>,
89
+ ): Record<string, import("./types.ts").CustomPresetConfig> {
90
+ const result: Record<string, import("./types.ts").CustomPresetConfig> = {};
91
+ if (!isRecord(raw)) return result;
92
+
93
+ for (const [name, entry] of Object.entries(raw)) {
94
+ const normalizedName = normalizeCustomItemId(name);
95
+ if (!normalizedName) continue;
96
+ const key = normalizedName.toLowerCase();
97
+ if (!isRecord(entry)) continue;
98
+
99
+ const preset: import("./types.ts").CustomPresetConfig = {};
100
+ const left = normalizeCustomPresetSegmentList(
101
+ entry.left,
102
+ customItemIds,
103
+ customSegmentIds,
104
+ );
105
+ const right = normalizeCustomPresetSegmentList(
106
+ entry.right,
107
+ customItemIds,
108
+ customSegmentIds,
109
+ );
110
+ const secondary = normalizeCustomPresetSegmentList(
111
+ entry.secondary,
112
+ customItemIds,
113
+ customSegmentIds,
114
+ );
115
+ if (left) preset.left = left;
116
+ if (right) preset.right = right;
117
+ if (secondary) preset.secondary = secondary;
118
+ const sep = normalizeSeparator(entry.separator);
119
+ if (sep) preset.separator = sep;
120
+ if (isRecord(entry.colors)) {
121
+ const colors: ColorScheme = {};
122
+ for (const [key, val] of Object.entries(entry.colors)) {
123
+ const cv = normalizeCustomColor(val);
124
+ if (cv) colors[key as keyof ColorScheme] = cv;
125
+ }
126
+ if (Object.keys(colors).length > 0) preset.colors = colors;
127
+ }
128
+ if (isRecord(entry.segmentOptions)) {
129
+ preset.segmentOptions = normalizeSegmentOptions(entry.segmentOptions);
130
+ }
131
+ result[key] = preset;
132
+ }
133
+
134
+ return result;
135
+ }
136
+
137
+ function normalizeCustomStatusItem(
138
+ raw: unknown,
139
+ idOverride?: string,
140
+ ): CustomStatusItem | null {
141
+ if (!isRecord(raw)) return null;
142
+ const id = normalizeCustomItemId(idOverride ?? raw.id);
143
+ if (!id) return null;
144
+
145
+ const statusKey =
146
+ typeof raw.statusKey === "string" && raw.statusKey.trim()
147
+ ? raw.statusKey.trim()
148
+ : id;
149
+
150
+ return {
151
+ id,
152
+ statusKey,
153
+ position: normalizeCustomItemPosition(raw.position),
154
+ color: normalizeCustomColor(raw.color),
155
+ prefix: normalizeCustomPrefix(raw.prefix),
156
+ hideWhenMissing: raw.hideWhenMissing !== false,
157
+ excludeFromExtensionStatuses: raw.excludeFromExtensionStatuses !== false,
158
+ };
159
+ }
160
+
161
+ export function normalizeCustomItems(raw: unknown): CustomStatusItem[] {
162
+ const normalized: CustomStatusItem[] = [];
163
+
164
+ if (Array.isArray(raw)) {
165
+ for (const entry of raw) {
166
+ const item = normalizeCustomStatusItem(entry);
167
+ if (item) normalized.push(item);
168
+ }
169
+ } else if (isRecord(raw)) {
170
+ for (const [id, entry] of Object.entries(raw)) {
171
+ const item = normalizeCustomStatusItem(entry, id);
172
+ if (item) normalized.push(item);
173
+ }
174
+ }
175
+
176
+ const deduped = new Map<string, CustomStatusItem>();
177
+ for (const item of normalized) {
178
+ deduped.set(item.id, item);
179
+ }
180
+
181
+ return [...deduped.values()];
182
+ }