@groeponline/pi-wishcraft 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +83 -0
- package/ROADMAP.md +18 -20
- package/bash-mode/editor-ghost.ts +63 -0
- package/bash-mode/editor-history.ts +81 -0
- package/bash-mode/editor-input.ts +35 -0
- package/bash-mode/editor.ts +16 -134
- package/bash-mode/shell-session.ts +12 -15
- package/bash-mode/types.ts +2 -0
- package/docs/bash-mode.md +46 -0
- package/docs/commands.md +122 -0
- package/docs/configuration.md +307 -0
- package/docs/index.md +18 -0
- package/docs/segments.md +168 -0
- package/docs/skill-manager.md +7 -0
- package/docs/stash-and-shortcuts.md +64 -0
- package/docs/working-vibes.md +71 -0
- package/package.json +2 -1
- package/queue/store.ts +72 -3
- package/src/config/custom-items.ts +45 -0
- package/src/config/parse.ts +19 -3
- package/src/config/powerline-config.ts +5 -0
- package/src/config/primitives.ts +13 -1
- package/src/config/segment-options.ts +29 -24
- package/src/config/settings-patch.ts +23 -2
- package/src/config/types.ts +32 -17
- package/src/extension/commands/bash-mode-actions.ts +1 -0
- package/src/extension/commands/commands.ts +36 -18
- package/src/extension/commands/powerline-completions.ts +8 -0
- package/src/extension/commands/powerline-doctor.ts +247 -0
- package/src/extension/commands/powerline-export.ts +94 -0
- package/src/extension/commands/queue-commands.ts +29 -1
- package/src/extension/commands/vibe-command.ts +49 -1
- package/src/extension/core/constants.ts +1 -0
- package/src/extension/core/segment-context.ts +38 -2
- package/src/extension/core/state.ts +5 -1
- package/src/extension/core/status-export.ts +68 -0
- package/src/extension/core/types.ts +4 -0
- package/src/extension/hooks/repairs.ts +89 -16
- package/src/extension/session/activate.ts +1 -2
- package/src/extension/session/cost-alert.ts +36 -0
- package/src/extension/session/session-lifecycle.ts +82 -1
- package/src/extension/settings/settings-io.ts +23 -17
- package/src/extension/settings/wishcraft-config.ts +2 -1
- package/src/extension/shortcuts/shortcuts-config.ts +5 -0
- package/src/extension/ui/layout.ts +1 -1
- package/src/extension/ui/menu-items.ts +379 -0
- package/src/extension/ui/menu-views.ts +377 -191
- package/src/extension/ui/overlay-chrome.ts +163 -0
- package/src/extension/ui/powerline-menu-view.ts +2 -4
- package/src/extension/ui/token-overlays.ts +82 -0
- package/src/extension/welcome/welcome-integration.ts +5 -9
- package/src/segments/registry.ts +100 -39
- package/src/segments/system.ts +204 -64
- package/src/segments/usage.ts +10 -4
- package/src/usage/ledger.ts +6 -0
- package/src/usage/token-budget.ts +57 -0
- package/src/usage/tps-ring.ts +151 -0
- package/src/usage/usage-store.ts +351 -0
- package/src/welcome/banner.ts +2 -1
- package/src/welcome/index.ts +3 -1
- package/src/welcome/overlay.ts +2 -0
- package/src/welcome/renderer.ts +52 -10
- package/src/welcome/types.ts +1 -2
- package/src/welcome/whats-new.ts +156 -0
- package/src/welcome/widgets/index.ts +3 -0
- package/src/welcome/widgets/whats-new-widget.ts +19 -0
- package/src/working-vibes/generate.ts +144 -58
- package/src/working-vibes/index.ts +10 -2
- package/src/working-vibes/storage.ts +5 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type { SelectItem } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import type { PowerlineConfig } from "../../config/powerline-config.ts";
|
|
4
|
+
import { mergeSegmentsWithCustomItems } from "../../config/powerline-config.ts";
|
|
5
|
+
import { getPreset } from "../../config/presets.ts";
|
|
6
|
+
import {
|
|
7
|
+
BUILTIN_STATUS_LINE_SEGMENT_IDS,
|
|
8
|
+
type CustomPresetConfig,
|
|
9
|
+
type CustomStatusItem,
|
|
10
|
+
type PresetDef,
|
|
11
|
+
type SegmentContext,
|
|
12
|
+
type StatusLineSegmentId,
|
|
13
|
+
type StatusLineSeparatorStyle,
|
|
14
|
+
} from "../../config/types.ts";
|
|
15
|
+
import type { OpenPortProcess } from "../../segments/system.ts";
|
|
16
|
+
import { formatUsdCost } from "../../usage/rates.ts";
|
|
17
|
+
import { renderSegmentWithWidth } from "./layout.ts";
|
|
18
|
+
|
|
19
|
+
const stripAnsi = (text: string) => text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
20
|
+
|
|
21
|
+
export interface SegmentMenuItem {
|
|
22
|
+
id: StatusLineSegmentId;
|
|
23
|
+
/** ANSI-stripped rendered value shown next to the id in the navigator. */
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DetailLine {
|
|
28
|
+
label: string;
|
|
29
|
+
value: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Segment ids in the active preset/layout order (custom items merged in).
|
|
34
|
+
* Disabled segments are intentionally kept so the toggle view can re-enable
|
|
35
|
+
* them; callers that render the live bar must filter those themselves.
|
|
36
|
+
*/
|
|
37
|
+
export function collectSegmentIds(
|
|
38
|
+
config: PowerlineConfig,
|
|
39
|
+
customItems: readonly CustomStatusItem[] = config.customItems,
|
|
40
|
+
): StatusLineSegmentId[] {
|
|
41
|
+
const presetDef = getPreset(config.preset);
|
|
42
|
+
const merged = mergeSegmentsWithCustomItems(presetDef, customItems, {
|
|
43
|
+
layout: config.layout,
|
|
44
|
+
});
|
|
45
|
+
return [
|
|
46
|
+
...merged.leftSegments,
|
|
47
|
+
...merged.rightSegments,
|
|
48
|
+
...merged.secondarySegments,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build navigator entries from the live segment context. Pure: given the same
|
|
54
|
+
* context + config it returns the same items, so it can be unit-tested.
|
|
55
|
+
* Disabled and currently-hidden segments are skipped (the bar never shows
|
|
56
|
+
* them, so there is nothing to navigate into).
|
|
57
|
+
*/
|
|
58
|
+
export function buildSegmentItems(
|
|
59
|
+
segCtx: SegmentContext,
|
|
60
|
+
config: PowerlineConfig,
|
|
61
|
+
): SegmentMenuItem[] {
|
|
62
|
+
const disabled = new Set(config.disabledSegments);
|
|
63
|
+
const items: SegmentMenuItem[] = [];
|
|
64
|
+
for (const id of collectSegmentIds(config, segCtx.effectiveCustomItems)) {
|
|
65
|
+
if (disabled.has(id)) continue;
|
|
66
|
+
const rendered = renderSegmentWithWidth(id, segCtx);
|
|
67
|
+
if (!rendered.visible) continue;
|
|
68
|
+
const value = stripAnsi(rendered.content).trim() || "(empty)";
|
|
69
|
+
items.push({ id, value });
|
|
70
|
+
}
|
|
71
|
+
return items;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function segmentItemsToSelectItems(
|
|
75
|
+
items: SegmentMenuItem[],
|
|
76
|
+
): SelectItem[] {
|
|
77
|
+
const selectItems: SelectItem[] = items.map((item) => ({
|
|
78
|
+
label: `● ${item.id} ${item.value}`,
|
|
79
|
+
value: item.id,
|
|
80
|
+
}));
|
|
81
|
+
if (selectItems.length === 0) {
|
|
82
|
+
selectItems.push({ label: "(no visible segments)", value: "__none__" });
|
|
83
|
+
}
|
|
84
|
+
return selectItems;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Index into `items` to keep the selection stable across a live refresh. The
|
|
89
|
+
* navigator rebuilds its item list on a timer; restoring by segment id (value)
|
|
90
|
+
* keeps the highlight on the same segment even when labels shift. Falls back to
|
|
91
|
+
* the first item when the previous selection no longer exists.
|
|
92
|
+
*/
|
|
93
|
+
export function selectIndexForValue(
|
|
94
|
+
items: SelectItem[],
|
|
95
|
+
previousValue: string | null,
|
|
96
|
+
): number {
|
|
97
|
+
if (previousValue === null) return 0;
|
|
98
|
+
const index = items.findIndex((item) => item.value === previousValue);
|
|
99
|
+
return index >= 0 ? index : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Configure sub-menu choices as plain data (label only; order is the menu). */
|
|
103
|
+
export function buildConfigureItems(): string[] {
|
|
104
|
+
return [
|
|
105
|
+
"Change preset",
|
|
106
|
+
"Set TPS value (POWERLINE_TPS)",
|
|
107
|
+
"Clear TPS override (use live)",
|
|
108
|
+
"Toggle UDP in open-ports",
|
|
109
|
+
"Toggle segment visibility…",
|
|
110
|
+
"Set segment label…",
|
|
111
|
+
"Build custom preset…",
|
|
112
|
+
"Show current config",
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const CONFIGURE_ITEM_DESCRIPTIONS: Record<string, string> = {
|
|
117
|
+
"Change preset": "Switch left/right segment layout preset",
|
|
118
|
+
"Set TPS value (POWERLINE_TPS)": "Override tokens/sec display with a fixed value",
|
|
119
|
+
"Clear TPS override (use live)": "Remove POWERLINE_TPS and use the live rate",
|
|
120
|
+
"Toggle UDP in open-ports": "Include or exclude UDP listeners in the port count",
|
|
121
|
+
"Toggle segment visibility…": "Enable or disable individual segments",
|
|
122
|
+
"Set segment label…": "Override the label prefix for one segment",
|
|
123
|
+
"Build custom preset…": "Compose a new preset from segment groups",
|
|
124
|
+
"Show current config": "Notify a one-line summary of active settings",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Configure sub-menu as SelectList items (CHE-42 overlay). */
|
|
128
|
+
export function configureItemsToSelectItems(): SelectItem[] {
|
|
129
|
+
return buildConfigureItems().map((label) => ({
|
|
130
|
+
label,
|
|
131
|
+
value: label,
|
|
132
|
+
description: CONFIGURE_ITEM_DESCRIPTIONS[label],
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Every segment id a user can place while building a custom preset: built-ins
|
|
138
|
+
* plus `custom:<id>` for explicit custom items and user-defined computed
|
|
139
|
+
* segments (command/env/static).
|
|
140
|
+
*/
|
|
141
|
+
export function buildCustomPresetSegmentIds(
|
|
142
|
+
config: PowerlineConfig,
|
|
143
|
+
): StatusLineSegmentId[] {
|
|
144
|
+
const ids: StatusLineSegmentId[] = [...BUILTIN_STATUS_LINE_SEGMENT_IDS];
|
|
145
|
+
const seen = new Set<string>(ids);
|
|
146
|
+
for (const item of config.customItems) {
|
|
147
|
+
const id = `custom:${item.id}` as StatusLineSegmentId;
|
|
148
|
+
if (!seen.has(id)) {
|
|
149
|
+
seen.add(id);
|
|
150
|
+
ids.push(id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const id of Object.keys(config.segments)) {
|
|
154
|
+
const segmentId = `custom:${id}` as StatusLineSegmentId;
|
|
155
|
+
if (!seen.has(segmentId)) {
|
|
156
|
+
seen.add(segmentId);
|
|
157
|
+
ids.push(segmentId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return ids;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Normalize a user-entered preset name to a lowercase safe id, or null. */
|
|
164
|
+
export function validatePresetName(name: string): string | null {
|
|
165
|
+
const trimmed = name.trim().toLowerCase();
|
|
166
|
+
return /^[a-z0-9_-]+$/.test(trimmed) && trimmed.length > 0 ? trimmed : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Choices for one "add segment to this preset group" picker: a done sentinel
|
|
171
|
+
* first, then every not-yet-chosen id. Pure so the picker can be unit-tested.
|
|
172
|
+
*/
|
|
173
|
+
export function buildPresetEditorAddItems(
|
|
174
|
+
all: readonly StatusLineSegmentId[],
|
|
175
|
+
chosen: readonly StatusLineSegmentId[],
|
|
176
|
+
): SelectItem[] {
|
|
177
|
+
const chosenSet = new Set(chosen);
|
|
178
|
+
const items: SelectItem[] = [
|
|
179
|
+
{
|
|
180
|
+
label:
|
|
181
|
+
chosen.length > 0 ? `— done — (${chosen.length} selected)` : "— done —",
|
|
182
|
+
value: "__done__",
|
|
183
|
+
},
|
|
184
|
+
];
|
|
185
|
+
for (const id of all) {
|
|
186
|
+
if (chosenSet.has(id)) continue;
|
|
187
|
+
items.push({ label: `+ ${id}`, value: id });
|
|
188
|
+
}
|
|
189
|
+
return items;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Assemble a `CustomPresetConfig` from a base preset (colors + segment options
|
|
194
|
+
* are inherited) plus the user's chosen segment groups and separator.
|
|
195
|
+
*/
|
|
196
|
+
export function buildCustomPresetDef(
|
|
197
|
+
base: PresetDef,
|
|
198
|
+
left: readonly StatusLineSegmentId[],
|
|
199
|
+
right: readonly StatusLineSegmentId[],
|
|
200
|
+
secondary: readonly StatusLineSegmentId[],
|
|
201
|
+
separator: StatusLineSeparatorStyle,
|
|
202
|
+
): CustomPresetConfig {
|
|
203
|
+
const def: CustomPresetConfig = {
|
|
204
|
+
left: [...left],
|
|
205
|
+
right: [...right],
|
|
206
|
+
separator,
|
|
207
|
+
};
|
|
208
|
+
if (secondary.length > 0) def.secondary = [...secondary];
|
|
209
|
+
if (base.colors) def.colors = { ...base.colors };
|
|
210
|
+
if (base.segmentOptions) def.segmentOptions = { ...base.segmentOptions };
|
|
211
|
+
return def;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Key/value detail rows for the per-segment detail view (`→`/`tab`).
|
|
216
|
+
* `openPortProcesses` is the live (already cached) `listOpenPortProcesses()`
|
|
217
|
+
* result, passed in so this builder stays pure and unit-testable.
|
|
218
|
+
*/
|
|
219
|
+
export function buildSegmentDetailLines(
|
|
220
|
+
id: StatusLineSegmentId,
|
|
221
|
+
segCtx: SegmentContext,
|
|
222
|
+
openPortProcesses: readonly OpenPortProcess[] = [],
|
|
223
|
+
): DetailLine[] {
|
|
224
|
+
const usage = segCtx.usageStats;
|
|
225
|
+
const label = segCtx.segmentLabels.get(id);
|
|
226
|
+
const common: DetailLine[] = [{ label: "segment", value: id }];
|
|
227
|
+
if (label) common.push({ label: "label", value: label });
|
|
228
|
+
|
|
229
|
+
switch (id) {
|
|
230
|
+
case "tps": {
|
|
231
|
+
const override = process.env.POWERLINE_TPS;
|
|
232
|
+
const windowMs = segCtx.options.tps?.windowMs;
|
|
233
|
+
const lines: DetailLine[] = [
|
|
234
|
+
...common,
|
|
235
|
+
{
|
|
236
|
+
label: "value",
|
|
237
|
+
value: override ? `${override} (override)` : "live 1s window",
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
if (windowMs) lines.push({ label: "window", value: `${windowMs} ms` });
|
|
241
|
+
return lines;
|
|
242
|
+
}
|
|
243
|
+
case "open_ports": {
|
|
244
|
+
const host = segCtx.options.openPorts?.host;
|
|
245
|
+
const lines: DetailLine[] = [
|
|
246
|
+
...common,
|
|
247
|
+
{
|
|
248
|
+
label: "UDP",
|
|
249
|
+
value: segCtx.options.openPorts?.includeUdp ? "on" : "off",
|
|
250
|
+
},
|
|
251
|
+
];
|
|
252
|
+
if (host) lines.push({ label: "host", value: host });
|
|
253
|
+
if (openPortProcesses.length > 0) {
|
|
254
|
+
for (const p of openPortProcesses) {
|
|
255
|
+
lines.push({
|
|
256
|
+
label: `${p.proto}:${p.port}`,
|
|
257
|
+
value: p.process ?? "(unknown)",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
lines.push({ label: "processes", value: "(none / ss -p unavailable)" });
|
|
262
|
+
}
|
|
263
|
+
return lines;
|
|
264
|
+
}
|
|
265
|
+
case "git": {
|
|
266
|
+
const git = segCtx.git;
|
|
267
|
+
const lines: DetailLine[] = [...common];
|
|
268
|
+
if (git.branch) {
|
|
269
|
+
lines.push({ label: "branch", value: git.branch });
|
|
270
|
+
lines.push({
|
|
271
|
+
label: "changes",
|
|
272
|
+
value: `+${git.staged} ~${git.unstaged} ?${git.untracked}`,
|
|
273
|
+
});
|
|
274
|
+
lines.push({
|
|
275
|
+
label: "upstream",
|
|
276
|
+
value: `↑${git.ahead} ↓${git.behind}`,
|
|
277
|
+
});
|
|
278
|
+
if (git.commit) {
|
|
279
|
+
lines.push({
|
|
280
|
+
label: "head",
|
|
281
|
+
value: `${git.commit.short} ${git.commit.subject}`,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
lines.push({ label: "branch", value: "(no repo)" });
|
|
286
|
+
}
|
|
287
|
+
return lines;
|
|
288
|
+
}
|
|
289
|
+
case "cost": {
|
|
290
|
+
const currency = segCtx.options.cost?.currency ?? "USD";
|
|
291
|
+
const total = usage.cost + usage.subagentCost;
|
|
292
|
+
return [
|
|
293
|
+
...common,
|
|
294
|
+
{
|
|
295
|
+
label: "cost",
|
|
296
|
+
value: formatUsdCost(usage.cost, currency) ?? `$${usage.cost.toFixed(2)}`,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
label: "subagents",
|
|
300
|
+
value:
|
|
301
|
+
formatUsdCost(usage.subagentCost, currency) ??
|
|
302
|
+
`$${usage.subagentCost.toFixed(2)}`,
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
label: "total",
|
|
306
|
+
value: formatUsdCost(total, currency) ?? `$${total.toFixed(2)}`,
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
case "context_pct":
|
|
311
|
+
case "context_total":
|
|
312
|
+
return [
|
|
313
|
+
...common,
|
|
314
|
+
{ label: "tokens", value: String(segCtx.contextTokens) },
|
|
315
|
+
{ label: "window", value: String(segCtx.contextWindow) },
|
|
316
|
+
{ label: "percent", value: `${segCtx.contextPercent.toFixed(1)}%` },
|
|
317
|
+
{
|
|
318
|
+
label: "auto-compact",
|
|
319
|
+
value: segCtx.autoCompactEnabled ? "on" : "off",
|
|
320
|
+
},
|
|
321
|
+
];
|
|
322
|
+
case "model":
|
|
323
|
+
return [
|
|
324
|
+
...common,
|
|
325
|
+
{
|
|
326
|
+
label: "model",
|
|
327
|
+
value: segCtx.model?.name ?? segCtx.model?.id ?? "(unknown)",
|
|
328
|
+
},
|
|
329
|
+
{ label: "thinking", value: segCtx.thinkingLevel },
|
|
330
|
+
];
|
|
331
|
+
case "queue": {
|
|
332
|
+
const queue = segCtx.queueSummary;
|
|
333
|
+
const lines: DetailLine[] = [
|
|
334
|
+
...common,
|
|
335
|
+
{ label: "queued", value: String(queue.queueCount) },
|
|
336
|
+
{ label: "ideas", value: String(queue.ideaCount) },
|
|
337
|
+
{ label: "blocked", value: String(queue.blockedCount) },
|
|
338
|
+
];
|
|
339
|
+
if (queue.leadingText) lines.push({ label: "next", value: queue.leadingText });
|
|
340
|
+
return lines;
|
|
341
|
+
}
|
|
342
|
+
case "token_in":
|
|
343
|
+
return [...common, { label: "input", value: String(usage.input) }];
|
|
344
|
+
case "token_out":
|
|
345
|
+
return [...common, { label: "output", value: String(usage.output) }];
|
|
346
|
+
case "token_total":
|
|
347
|
+
return [
|
|
348
|
+
...common,
|
|
349
|
+
{
|
|
350
|
+
label: "total",
|
|
351
|
+
value: String(
|
|
352
|
+
usage.input + usage.output + usage.cacheRead + usage.cacheWrite,
|
|
353
|
+
),
|
|
354
|
+
},
|
|
355
|
+
];
|
|
356
|
+
case "cache_read":
|
|
357
|
+
return [...common, { label: "cache read", value: String(usage.cacheRead) }];
|
|
358
|
+
case "cache_write":
|
|
359
|
+
return [
|
|
360
|
+
...common,
|
|
361
|
+
{ label: "cache write", value: String(usage.cacheWrite) },
|
|
362
|
+
];
|
|
363
|
+
case "time_spent":
|
|
364
|
+
return [
|
|
365
|
+
...common,
|
|
366
|
+
{
|
|
367
|
+
label: "session start",
|
|
368
|
+
value: new Date(segCtx.sessionStartTime).toLocaleTimeString(),
|
|
369
|
+
},
|
|
370
|
+
];
|
|
371
|
+
default: {
|
|
372
|
+
const rendered = renderSegmentWithWidth(id, segCtx);
|
|
373
|
+
const value = rendered.visible
|
|
374
|
+
? stripAnsi(rendered.content).trim() || "(empty)"
|
|
375
|
+
: "(hidden)";
|
|
376
|
+
return [...common, { label: "value", value }];
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|