@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,114 @@
1
+ import { isRecord, normalizeCustomItemId } from "./primitives.ts";
2
+ import { BUILTIN_STATUS_LINE_SEGMENT_IDS } from "./types.ts";
3
+ import type {
4
+ CustomStatusItem,
5
+ StatusLineLayout,
6
+ StatusLineSegmentId,
7
+ } from "./types.ts";
8
+
9
+ const BUILTIN_STATUS_LINE_SEGMENT_ID_SET = new Set<string>(
10
+ BUILTIN_STATUS_LINE_SEGMENT_IDS,
11
+ );
12
+
13
+ export function normalizeStatusLineSegmentId(
14
+ value: unknown,
15
+ customItemIds: ReadonlySet<string>,
16
+ customSegmentIds: ReadonlySet<string> = new Set(),
17
+ ): StatusLineSegmentId | null {
18
+ if (typeof value !== "string") return null;
19
+
20
+ const normalized = value.trim();
21
+ if (BUILTIN_STATUS_LINE_SEGMENT_ID_SET.has(normalized)) {
22
+ return normalized as StatusLineSegmentId;
23
+ }
24
+
25
+ const customId = normalized.startsWith("custom:")
26
+ ? normalizeCustomItemId(normalized.slice("custom:".length))
27
+ : null;
28
+ // ponytail: custom: ids resolve to either a user-defined item OR a computed segment
29
+ return customId &&
30
+ (customItemIds.has(customId) || customSegmentIds.has(customId))
31
+ ? `custom:${customId}`
32
+ : null;
33
+ }
34
+
35
+ export function normalizeDisabledSegments(
36
+ raw: unknown,
37
+ customItems: readonly CustomStatusItem[],
38
+ customSegmentIds: ReadonlySet<string>,
39
+ ): {
40
+ disabledSegments: StatusLineSegmentId[];
41
+ invalidDisabledSegments: string[];
42
+ } {
43
+ if (!Array.isArray(raw))
44
+ return { disabledSegments: [], invalidDisabledSegments: [] };
45
+
46
+ const disabledSegments: StatusLineSegmentId[] = [];
47
+ const invalidDisabledSegments: string[] = [];
48
+ const customItemIds = new Set(customItems.map((item) => item.id));
49
+ const seen = new Set<StatusLineSegmentId>();
50
+
51
+ for (const entry of raw) {
52
+ const segmentId = normalizeStatusLineSegmentId(
53
+ entry,
54
+ customItemIds,
55
+ customSegmentIds,
56
+ );
57
+ if (!segmentId) {
58
+ invalidDisabledSegments.push(
59
+ typeof entry === "string" ? entry.trim() : String(entry),
60
+ );
61
+ } else if (!seen.has(segmentId)) {
62
+ seen.add(segmentId);
63
+ disabledSegments.push(segmentId);
64
+ }
65
+ }
66
+
67
+ return { disabledSegments, invalidDisabledSegments };
68
+ }
69
+
70
+ export function normalizeLayout(
71
+ raw: unknown,
72
+ customItems: readonly CustomStatusItem[],
73
+ customSegmentIds: ReadonlySet<string>,
74
+ ): { layout: StatusLineLayout | null; invalidLayoutSegments: string[] } {
75
+ if (!isRecord(raw)) return { layout: null, invalidLayoutSegments: [] };
76
+
77
+ const layout: StatusLineLayout = {};
78
+ const invalidLayoutSegments: string[] = [];
79
+ const customItemIds = new Set(customItems.map((item) => item.id));
80
+ const globallyPlaced = new Set<StatusLineSegmentId>();
81
+
82
+ for (const row of ["left", "right", "secondary"] as const) {
83
+ const entries = raw[row];
84
+ if (!Array.isArray(entries)) continue;
85
+
86
+ const segments: StatusLineSegmentId[] = [];
87
+ const seen = new Set<StatusLineSegmentId>();
88
+ for (const entry of entries) {
89
+ const segmentId = normalizeStatusLineSegmentId(
90
+ entry,
91
+ customItemIds,
92
+ customSegmentIds,
93
+ );
94
+ if (!segmentId) {
95
+ invalidLayoutSegments.push(
96
+ `${row}:${typeof entry === "string" ? entry.trim() : String(entry)}`,
97
+ );
98
+ } else if (!seen.has(segmentId)) {
99
+ seen.add(segmentId);
100
+ if (globallyPlaced.has(segmentId)) {
101
+ invalidLayoutSegments.push(`${row}:${segmentId}`);
102
+ } else {
103
+ globallyPlaced.add(segmentId);
104
+ segments.push(segmentId);
105
+ }
106
+ }
107
+ }
108
+ layout[row] = segments;
109
+ }
110
+
111
+ return Object.keys(layout).length > 0
112
+ ? { layout, invalidLayoutSegments }
113
+ : { layout: null, invalidLayoutSegments };
114
+ }
@@ -0,0 +1,128 @@
1
+ import { normalizeCostCurrency } from "../usage/rates.ts";
2
+ import { isRecord } from "./primitives.ts";
3
+ import type { StatusLineSegmentOptions } from "./types.ts";
4
+
5
+ export function normalizeSegmentOptions(
6
+ raw: Record<string, unknown>,
7
+ ): StatusLineSegmentOptions {
8
+ const options: StatusLineSegmentOptions = {};
9
+
10
+ if (isRecord(raw.model)) {
11
+ options.model = {
12
+ ...(typeof raw.model.showThinkingLevel === "boolean"
13
+ ? { showThinkingLevel: raw.model.showThinkingLevel }
14
+ : {}),
15
+ ...(raw.model.display === "name" || raw.model.display === "qualified"
16
+ ? { display: raw.model.display }
17
+ : {}),
18
+ };
19
+ }
20
+
21
+ if (isRecord(raw.path)) {
22
+ options.path = {
23
+ ...(raw.path.mode === "basename" ||
24
+ raw.path.mode === "abbreviated" ||
25
+ raw.path.mode === "full"
26
+ ? { mode: raw.path.mode }
27
+ : {}),
28
+ ...(typeof raw.path.maxLength === "number" &&
29
+ Number.isFinite(raw.path.maxLength) &&
30
+ raw.path.maxLength > 0
31
+ ? { maxLength: Math.floor(raw.path.maxLength) }
32
+ : {}),
33
+ };
34
+ }
35
+
36
+ if (isRecord(raw.git)) {
37
+ options.git = {
38
+ ...(typeof raw.git.showBranch === "boolean"
39
+ ? { showBranch: raw.git.showBranch }
40
+ : {}),
41
+ ...(typeof raw.git.showStaged === "boolean"
42
+ ? { showStaged: raw.git.showStaged }
43
+ : {}),
44
+ ...(typeof raw.git.showUnstaged === "boolean"
45
+ ? { showUnstaged: raw.git.showUnstaged }
46
+ : {}),
47
+ ...(typeof raw.git.showUntracked === "boolean"
48
+ ? { showUntracked: raw.git.showUntracked }
49
+ : {}),
50
+ ...(raw.git.polling === "full" ||
51
+ raw.git.polling === "branch" ||
52
+ raw.git.polling === "off"
53
+ ? { polling: raw.git.polling }
54
+ : {}),
55
+ ...(typeof raw.git.hostIcon === "boolean"
56
+ ? { hostIcon: raw.git.hostIcon }
57
+ : {}),
58
+ };
59
+ }
60
+
61
+ if (isRecord(raw.time)) {
62
+ options.time = {
63
+ ...(raw.time.format === "12h" || raw.time.format === "24h"
64
+ ? { format: raw.time.format }
65
+ : {}),
66
+ ...(typeof raw.time.showSeconds === "boolean"
67
+ ? { showSeconds: raw.time.showSeconds }
68
+ : {}),
69
+ };
70
+ }
71
+
72
+ if (isRecord(raw.cost)) {
73
+ const currency = normalizeCostCurrency(raw.cost.currency);
74
+ options.cost = {
75
+ ...(raw.cost.subscriptionDisplay === "subscription" ||
76
+ raw.cost.subscriptionDisplay === "reported-cost" ||
77
+ raw.cost.subscriptionDisplay === "both"
78
+ ? { subscriptionDisplay: raw.cost.subscriptionDisplay }
79
+ : {}),
80
+ ...(currency ? { currency } : {}),
81
+ };
82
+ }
83
+
84
+ if (isRecord(raw.context)) {
85
+ options.context = {
86
+ ...(raw.context.format === "full" || raw.context.format === "percent"
87
+ ? { format: raw.context.format }
88
+ : {}),
89
+ };
90
+ }
91
+
92
+ if (isRecord(raw.cache_read)) {
93
+ options.cache_read = {
94
+ ...(raw.cache_read.format === "tokens" ||
95
+ raw.cache_read.format === "percent" ||
96
+ raw.cache_read.format === "both"
97
+ ? { format: raw.cache_read.format }
98
+ : {}),
99
+ };
100
+ }
101
+
102
+ if (isRecord(raw.openPorts)) {
103
+ options.openPorts = {
104
+ ...(typeof raw.openPorts.includeUdp === "boolean"
105
+ ? { includeUdp: raw.openPorts.includeUdp }
106
+ : {}),
107
+ };
108
+ }
109
+
110
+ return options;
111
+ }
112
+
113
+ export function mergeSegmentOptions(
114
+ defaults: StatusLineSegmentOptions = {},
115
+ overrides: StatusLineSegmentOptions = {},
116
+ ): StatusLineSegmentOptions {
117
+ return {
118
+ ...defaults,
119
+ ...overrides,
120
+ model: { ...defaults.model, ...overrides.model },
121
+ path: { ...defaults.path, ...overrides.path },
122
+ git: { ...defaults.git, ...overrides.git },
123
+ time: { ...defaults.time, ...overrides.time },
124
+ cost: { ...defaults.cost, ...overrides.cost },
125
+ context: { ...defaults.context, ...overrides.context },
126
+ cache_read: { ...defaults.cache_read, ...overrides.cache_read },
127
+ };
128
+ }
@@ -0,0 +1,26 @@
1
+ import { isRecord } from "./primitives.ts";
2
+ import type { PowerlineConfig } from "./parse.ts";
3
+ import type { StatusLinePreset } from "./types.ts";
4
+
5
+ export function nextPowerlineSettingWithPreset(
6
+ existingPowerlineSetting: unknown,
7
+ preset: StatusLinePreset,
8
+ ): unknown {
9
+ if (!isRecord(existingPowerlineSetting)) {
10
+ return preset;
11
+ }
12
+ return { ...existingPowerlineSetting, preset };
13
+ }
14
+
15
+ export function nextPowerlineSettingWithOptions(
16
+ existingPowerlineSetting: unknown,
17
+ updates: Partial<
18
+ Pick<PowerlineConfig, "welcome" | "stashSharpSShortcut" | "placement">
19
+ >,
20
+ currentPreset: StatusLinePreset,
21
+ ): unknown {
22
+ if (!isRecord(existingPowerlineSetting)) {
23
+ return { preset: currentPreset, ...updates };
24
+ }
25
+ return { ...existingPowerlineSetting, ...updates };
26
+ }
@@ -0,0 +1,277 @@
1
+ import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
2
+ import type { CostCurrencyCode } from "../usage/rates.ts";
3
+
4
+ // Theme color - either a pi theme color name or a custom hex color
5
+ export type ColorValue = ThemeColor | `#${string}`;
6
+ export type ThemeLike = Pick<Theme, "fg">;
7
+
8
+ // Semantic color names for segments
9
+ export type SemanticColor =
10
+ | "model"
11
+ | "shellMode"
12
+ | "path"
13
+ | "gitDirty"
14
+ | "gitClean"
15
+ | "thinking"
16
+ | "thinkingMinimal"
17
+ | "thinkingLow"
18
+ | "thinkingMedium"
19
+ | "context"
20
+ | "contextWarn"
21
+ | "contextError"
22
+ | "cost"
23
+ | "tokens"
24
+ | "queue"
25
+ | "separator"
26
+ | "border";
27
+
28
+ // Color scheme mapping semantic names to actual colors
29
+ export type ColorScheme = Partial<Record<SemanticColor, ColorValue>>;
30
+
31
+ // Built-in segment identifiers
32
+ export const BUILTIN_STATUS_LINE_SEGMENT_IDS = [
33
+ "model",
34
+ "shell_mode",
35
+ "path",
36
+ "git",
37
+ "subagents",
38
+ "queue",
39
+ "token_in",
40
+ "token_out",
41
+ "token_total",
42
+ "cost",
43
+ "context_pct",
44
+ "context_total",
45
+ "time_spent",
46
+ "time",
47
+ "session",
48
+ "hostname",
49
+ "cache_read",
50
+ "cache_write",
51
+ "thinking",
52
+ "tps",
53
+ "open_ports",
54
+ "extension_statuses",
55
+ ] as const;
56
+
57
+ export type BuiltinStatusLineSegmentId =
58
+ (typeof BUILTIN_STATUS_LINE_SEGMENT_IDS)[number];
59
+
60
+ // Segment identifiers (built-in + dynamically registered custom items)
61
+ export type StatusLineSegmentId =
62
+ BuiltinStatusLineSegmentId | `custom:${string}`;
63
+
64
+ // Separator styles
65
+ export type StatusLineSeparatorStyle =
66
+ | "powerline"
67
+ | "powerline-thin"
68
+ | "slash"
69
+ | "pipe"
70
+ | "block"
71
+ | "none"
72
+ | "ascii"
73
+ | "dot"
74
+ | "chevron"
75
+ | "star";
76
+
77
+ // Preset names
78
+ export type PowerlinePlacement = "above" | "below";
79
+
80
+ export type StatusLinePreset =
81
+ "default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "chef";
82
+
83
+ // Per-segment options
84
+ export interface StatusLineSegmentOptions {
85
+ model?: { showThinkingLevel?: boolean; display?: "name" | "qualified" };
86
+ path?: {
87
+ mode?: "basename" | "abbreviated" | "full";
88
+ maxLength?: number;
89
+ };
90
+ git?: {
91
+ showBranch?: boolean;
92
+ showStaged?: boolean;
93
+ showUnstaged?: boolean;
94
+ showUntracked?: boolean;
95
+ polling?: "full" | "branch" | "off";
96
+ /** Replace the branch icon with the origin remote's host logo
97
+ * (GitHub/GitLab/Bitbucket, or a generic git logo). Default false. */
98
+ hostIcon?: boolean;
99
+ };
100
+ time?: { format?: "12h" | "24h"; showSeconds?: boolean };
101
+ cost?: {
102
+ subscriptionDisplay?: "subscription" | "reported-cost" | "both";
103
+ currency?: CostCurrencyCode;
104
+ };
105
+ context?: { format?: "full" | "percent" };
106
+ cache_read?: { format?: "tokens" | "percent" | "both" };
107
+ openPorts?: {
108
+ /** Include UDP listeners (mDNS/DHCP/ephemeral) in the count. Default false. */ includeUdp?: boolean;
109
+ };
110
+ }
111
+
112
+ export type CustomItemPosition = "left" | "right" | "secondary";
113
+
114
+ export interface StatusLineLayout {
115
+ left?: StatusLineSegmentId[];
116
+ right?: StatusLineSegmentId[];
117
+ secondary?: StatusLineSegmentId[];
118
+ }
119
+
120
+ export interface CustomStatusItem {
121
+ id: string;
122
+ statusKey: string;
123
+ position: CustomItemPosition;
124
+ color?: ColorValue;
125
+ prefix?: string;
126
+ hideWhenMissing: boolean;
127
+ excludeFromExtensionStatuses: boolean;
128
+ }
129
+
130
+ // Preset definition
131
+ export interface PresetDef {
132
+ leftSegments: StatusLineSegmentId[];
133
+ rightSegments: StatusLineSegmentId[];
134
+ /** Secondary row segments (shown in footer, above sub bar) */
135
+ secondarySegments?: StatusLineSegmentId[];
136
+ separator: StatusLineSeparatorStyle;
137
+ segmentOptions?: StatusLineSegmentOptions;
138
+ /** Color scheme for this preset */
139
+ colors?: ColorScheme;
140
+ }
141
+
142
+ // Separator definition
143
+ export interface SeparatorDef {
144
+ left: string;
145
+ right: string;
146
+ endCaps?: {
147
+ left: string;
148
+ right: string;
149
+ useBgAsFg: boolean;
150
+ };
151
+ }
152
+
153
+ // Git status data
154
+ export interface GitStatus {
155
+ branch: string | null;
156
+ staged: number;
157
+ unstaged: number;
158
+ untracked: number;
159
+ }
160
+
161
+ // Usage statistics
162
+ export interface QueueSummary {
163
+ queueCount: number;
164
+ ideaCount: number;
165
+ blockedCount: number;
166
+ compacting: boolean;
167
+ leadingText: string | null;
168
+ leadingIntent: "steer" | "follow-up" | "post-compact" | "idea" | null;
169
+ leadingStatus: "queued" | "blocked" | "delivering" | "sent" | "failed" | null;
170
+ }
171
+
172
+ export interface UsageStats {
173
+ input: number;
174
+ output: number;
175
+ cacheRead: number;
176
+ cacheWrite: number;
177
+ cost: number;
178
+ // Cumulative cost of subagent child runs (e.g. /parallel, /worker) launched from this session.
179
+ subagentCost: number;
180
+ }
181
+
182
+ // Context passed to segment render functions
183
+ export interface SegmentContext {
184
+ // From pi-mono
185
+ model:
186
+ | {
187
+ id: string;
188
+ name?: string;
189
+ provider?: string;
190
+ providerId?: string;
191
+ providerName?: string;
192
+ reasoning?: boolean;
193
+ contextWindow?: number;
194
+ }
195
+ | undefined;
196
+ thinkingLevel: string;
197
+ sessionId: string | undefined;
198
+ cwd?: string;
199
+
200
+ // Computed
201
+ usageStats: UsageStats;
202
+ contextTokens: number;
203
+ contextPercent: number;
204
+ contextWindow: number;
205
+ autoCompactEnabled: boolean;
206
+ customCompactionEnabled: boolean;
207
+ usingSubscription: boolean;
208
+ queueSummary: QueueSummary;
209
+ sessionStartTime: number;
210
+ shellModeActive: boolean;
211
+ shellRunning: boolean;
212
+ shellName: string | null;
213
+ shellCwd: string | null;
214
+
215
+ // Git
216
+ git: GitStatus;
217
+
218
+ // Extension statuses
219
+ extensionStatuses: ReadonlyMap<string, string>;
220
+ hiddenExtensionStatusKeys: ReadonlySet<string>;
221
+ customItemsById: ReadonlyMap<string, CustomStatusItem>;
222
+
223
+ // Options
224
+ options: StatusLineSegmentOptions;
225
+ /** Per-segment custom text labels (from powerline.segmentLabels). */
226
+ segmentLabels: ReadonlyMap<string, string>;
227
+
228
+ // Theming
229
+ theme: ThemeLike;
230
+ colors: ColorScheme;
231
+ }
232
+
233
+ // Rendered segment output
234
+ export interface RenderedSegment {
235
+ content: string;
236
+ visible: boolean;
237
+ }
238
+
239
+ // Segment definition
240
+ export interface StatusLineSegment {
241
+ id: StatusLineSegmentId;
242
+ render(ctx: SegmentContext): RenderedSegment;
243
+ }
244
+
245
+ // User-defined computed segment (configured via settings, no TS needed)
246
+ export type CustomSegmentConfig =
247
+ | {
248
+ type: "command";
249
+ command: string;
250
+ prefix?: string;
251
+ color?: ColorValue;
252
+ /** Cache command output for this many ms to avoid re-running every paint */
253
+ cacheMs?: number;
254
+ }
255
+ | {
256
+ type: "env";
257
+ env: string;
258
+ prefix?: string;
259
+ color?: ColorValue;
260
+ fallback?: string;
261
+ }
262
+ | {
263
+ type: "static";
264
+ text: string;
265
+ prefix?: string;
266
+ color?: ColorValue;
267
+ };
268
+
269
+ // User-defined preset (configured via settings, merges with built-ins)
270
+ export interface CustomPresetConfig {
271
+ left?: StatusLineSegmentId[];
272
+ right?: StatusLineSegmentId[];
273
+ secondary?: StatusLineSegmentId[];
274
+ separator?: StatusLineSeparatorStyle;
275
+ colors?: ColorScheme;
276
+ segmentOptions?: StatusLineSegmentOptions;
277
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * frontmatter.ts
3
+ * ------------------------------------------------------------------------
4
+ * Utility for stripping YAML frontmatter from markdown/text files.
5
+ * Used by skills system to extract the actual content from skill files.
6
+ * ------------------------------------------------------------------------
7
+ */
8
+
9
+ /**
10
+ * Strips YAML frontmatter from a text file.
11
+ * Frontmatter is delimited by --- at the start and end.
12
+ *
13
+ * @param content - The full file content including potential frontmatter
14
+ * @returns The content with frontmatter removed
15
+ */
16
+ export function stripFrontmatter(content: string): string {
17
+ const lines = content.split("\n");
18
+
19
+ // Check if file starts with frontmatter delimiter
20
+ if (lines[0]?.trim() !== "---") {
21
+ return content;
22
+ }
23
+
24
+ // Find the closing delimiter
25
+ let endIndex = -1;
26
+ for (let i = 1; i < lines.length; i++) {
27
+ if (lines[i]?.trim() === "---") {
28
+ endIndex = i;
29
+ break;
30
+ }
31
+ }
32
+
33
+ // If no closing delimiter found, return original content
34
+ if (endIndex === -1) {
35
+ return content;
36
+ }
37
+
38
+ // Return everything after the frontmatter without changing user-authored whitespace
39
+ return lines.slice(endIndex + 1).join("\n");
40
+ }
@@ -0,0 +1,41 @@
1
+ import type { AutocompleteProvider } from "@earendil-works/pi-tui";
2
+
3
+ function isObject(val: unknown): val is Record<string, unknown> {
4
+ return val !== null && typeof val === "object";
5
+ }
6
+
7
+ function isProvider(val: unknown): val is AutocompleteProvider {
8
+ if (!isObject(val)) return false;
9
+ return typeof val.getSuggestions === "function" && typeof val.applyCompletion === "function";
10
+ }
11
+
12
+ export function getEditorAutocompleteProvider(sourceEditor: unknown): AutocompleteProvider | undefined {
13
+ if (!isObject(sourceEditor) || !("autocompleteProvider" in sourceEditor)) {
14
+ return undefined;
15
+ }
16
+ const provider = sourceEditor.autocompleteProvider;
17
+ if (isProvider(provider)) {
18
+ return provider;
19
+ }
20
+ return undefined;
21
+ }
22
+
23
+ export function passAutocompleteProviderThroughPreviousEditor(
24
+ provider: AutocompleteProvider,
25
+ previousEditor: unknown,
26
+ ): AutocompleteProvider {
27
+ if (!isObject(previousEditor)) {
28
+ return provider;
29
+ }
30
+
31
+ if ("setAutocompleteProvider" in previousEditor && typeof previousEditor.setAutocompleteProvider === "function") {
32
+ previousEditor.setAutocompleteProvider(provider);
33
+ }
34
+
35
+ const existingProvider = getEditorAutocompleteProvider(previousEditor);
36
+ if (existingProvider !== undefined) {
37
+ return existingProvider;
38
+ }
39
+
40
+ return provider;
41
+ }
@@ -0,0 +1,28 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { parsePowerlineConfig } from "../config/powerline-config.ts";
4
+ import { registerCustomPresets } from "../config/presets.ts";
5
+ import { registerCustomSegments } from "../segments/index.ts";
6
+ import { readSettings } from "./settings-io.ts";
7
+ import { registerSessionLifecycle } from "./session-lifecycle.ts";
8
+ import { registerCommands } from "./commands.ts";
9
+ import { setupInlineInvocation } from "./skills/inline-invocation.ts";
10
+ import {
11
+ config,
12
+ createRuntimeState,
13
+ PRESET_NAMES,
14
+ setConfig,
15
+ } from "./state.ts";
16
+
17
+ export default function powerlineFooter(pi: ExtensionAPI) {
18
+ const startupSettings = readSettings();
19
+ setConfig(parsePowerlineConfig(startupSettings.powerline, PRESET_NAMES));
20
+ registerCustomSegments(config.segments);
21
+ registerCustomPresets(config.presets);
22
+
23
+ const rt = createRuntimeState(startupSettings);
24
+
25
+ registerSessionLifecycle(pi, rt);
26
+ registerCommands(pi, rt);
27
+ setupInlineInvocation(pi, rt);
28
+ }