@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,64 @@
|
|
|
1
|
+
# Stash & shortcuts
|
|
2
|
+
|
|
3
|
+
## Editor stash
|
|
4
|
+
|
|
5
|
+
Use `Alt+S` / `Option+S` as a quick stash toggle while drafting. It keeps one active stash and clears the editor when stashing. Powerline listens for unambiguous Alt/Meta-S escape encodings by default. If your old terminal setup only emits the printable German sharp-S character for Option+S and you still want that to trigger stash, set `"stashSharpSShortcut": true` under `powerline`.
|
|
6
|
+
|
|
7
|
+
| Editor | Stash | `Alt+S` result |
|
|
8
|
+
|--------|-------|----------------|
|
|
9
|
+
| Has text | Empty | Stash current text, clear editor |
|
|
10
|
+
| Empty | Has stash | Restore stash into editor |
|
|
11
|
+
| Has text | Has stash | Update stash with current text, clear editor |
|
|
12
|
+
| Empty | Empty | Show "Nothing to stash" |
|
|
13
|
+
|
|
14
|
+
Auto-restore after an agent run only happens when the editor is still empty. If you typed meanwhile, the stash is preserved.
|
|
15
|
+
|
|
16
|
+
The `stash` indicator appears in the powerline bar (on presets with `extension_statuses`). Active stash is still session-local and resets on session switch / disable, but stash history is persisted to the agent dir at `powerline-footer/stash-history.json` so it survives restarts. By default the agent dir is `~/.pi/agent`; set `PI_CODING_AGENT_DIR` to move global powerline settings, stash history, sessions, vibes, skills, commands, and extension discovery with Pi.
|
|
17
|
+
|
|
18
|
+
## Stash history
|
|
19
|
+
|
|
20
|
+
Open prompt history with either:
|
|
21
|
+
|
|
22
|
+
- `ctrl+alt+h`
|
|
23
|
+
- `/stash-history`
|
|
24
|
+
|
|
25
|
+
Prompt history now has two sources:
|
|
26
|
+
|
|
27
|
+
- stashed prompts: up to 12 recent stashed prompts (newest first)
|
|
28
|
+
- recent project prompts: up to 50 recent user-submitted prompts pulled from pi sessions in the current project folder
|
|
29
|
+
|
|
30
|
+
Selecting a stashed entry lets you insert it or promote it to an idea. Project prompt history entries insert into the editor. If the editor already has text, you can choose `Replace`, `Append`, or `Cancel`.
|
|
31
|
+
|
|
32
|
+
## Editor clipboard and navigation shortcuts
|
|
33
|
+
|
|
34
|
+
- `ctrl+alt+c`: copy full editor content
|
|
35
|
+
- `ctrl+alt+x`: cut full editor content (copy, then clear)
|
|
36
|
+
- `ctrl+alt+q`: open the queued-prompt picker
|
|
37
|
+
- `cmd+shift+up`: move the editor cursor to the start of the first line
|
|
38
|
+
- `cmd+shift+down`: move the editor cursor to the end of the last line
|
|
39
|
+
|
|
40
|
+
Copy/cut actions do not modify stash state or stash history. Dragging files, folders, images, or screenshots from Finder into the custom editor inserts their path strings. Pi owns chat scrolling, selection, and fixed input behavior natively.
|
|
41
|
+
|
|
42
|
+
## Shortcut configuration
|
|
43
|
+
|
|
44
|
+
You can override shortcut keys in the agent settings file:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"powerlineShortcuts": {
|
|
49
|
+
"stashHistory": "ctrl+alt+h",
|
|
50
|
+
"copyEditor": "ctrl+alt+c",
|
|
51
|
+
"cutEditor": "ctrl+alt+x",
|
|
52
|
+
"ideaCapture": null,
|
|
53
|
+
"queueOpen": "ctrl+alt+q",
|
|
54
|
+
"editorStart": "cmd+shift+up",
|
|
55
|
+
"editorEnd": "cmd+shift+down"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
After changing bindings, run `/reload`. Invalid bindings, reserved key conflicts like `Alt+S`, or duplicate conflicts fall back to safe defaults. Set a binding to `null` or `""` to disable that action. `cmd` and `command` are accepted aliases for Pi's `super` modifier for the documented Command navigation keys.
|
|
61
|
+
|
|
62
|
+
## Editor autocomplete composition
|
|
63
|
+
|
|
64
|
+
Powerline wraps Pi's autocomplete provider so bash mode can add shell-aware suggestions. When another editor extension was already installed, powerline now passes Pi's provider through that previous editor's `setAutocompleteProvider()` first and then wraps the resulting provider. This preserves prior autocomplete-provider wrappers where possible, but it is not full render/input composition between custom editors.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Working vibes
|
|
2
|
+
|
|
3
|
+
Transform boring "Working..." messages into themed phrases that match your style:
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
/vibe star trek → "Running diagnostics...", "Engaging warp drive..."
|
|
7
|
+
/vibe pirate → "Hoisting the sails...", "Charting course..."
|
|
8
|
+
/vibe zen → "Breathing deeply...", "Finding balance..."
|
|
9
|
+
/vibe noir → "Following the trail...", "Checking the angles..."
|
|
10
|
+
/vibe → Shows current theme, mode, and model
|
|
11
|
+
/vibe off → Disables (back to "Working...")
|
|
12
|
+
/vibe model → Shows current model
|
|
13
|
+
/vibe model openai/gpt-4o-mini → Use a different model
|
|
14
|
+
/vibe mode → Shows current mode (generate or file)
|
|
15
|
+
/vibe mode file → Switch to file-based mode (instant, no API calls)
|
|
16
|
+
/vibe mode generate → Switch to on-demand generation (contextual)
|
|
17
|
+
/vibe generate mafia 200 → Pre-generate 200 vibes and save to file
|
|
18
|
+
/vibe test mafia → Preview 3 sample vibes without committing the theme
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
In the agent settings file:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"workingVibe": "star trek", // Theme phrase
|
|
28
|
+
"workingVibeMode": "generate", // "generate" (on-demand) or "file" (pre-generated)
|
|
29
|
+
"workingVibeModel": "openai-codex/gpt-5.4-mini", // Optional: model to use (default)
|
|
30
|
+
"workingVibeFallback": "Working", // Optional: fallback message
|
|
31
|
+
"workingVibeRefreshInterval": 30, // Optional: seconds between refreshes (default 30)
|
|
32
|
+
"workingVibePrompt": "Generate a {theme} loading message for: {task}", // Optional: custom prompt template
|
|
33
|
+
"workingVibeMaxLength": 65 // Optional: max message length (default 65)
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Modes
|
|
38
|
+
|
|
39
|
+
| Mode | Description | Pros | Cons |
|
|
40
|
+
|------|-------------|------|------|
|
|
41
|
+
| `generate` | On-demand AI generation (default) | Contextual, hints at actual task | Model-dependent cost and latency |
|
|
42
|
+
| `file` | Pull from pre-generated file | Instant, zero cost, works offline | Not contextual |
|
|
43
|
+
|
|
44
|
+
**File mode setup:**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
/vibe generate mafia 200 # Generate 200 vibes, save to the agent dir
|
|
48
|
+
/vibe mode file # Switch to file mode
|
|
49
|
+
/vibe mafia # Now uses the file
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**How file mode works:**
|
|
53
|
+
|
|
54
|
+
1. Vibes are loaded from `vibes/{theme}.txt` in the agent dir into memory
|
|
55
|
+
2. Uses seeded shuffle (Mulberry32 PRNG): cycles through all vibes before repeating
|
|
56
|
+
3. New seed each session: different order every time you restart pi
|
|
57
|
+
4. Zero latency, zero cost, works offline
|
|
58
|
+
|
|
59
|
+
**Prompt template variables (generate mode only):**
|
|
60
|
+
|
|
61
|
+
- `{theme}`: the current vibe theme (e.g., "star trek", "mafia")
|
|
62
|
+
- `{task}`: context hint (user prompt initially, then agent's response text or tool info on refresh)
|
|
63
|
+
- `{exclude}`: recent vibes to avoid (auto-populated, e.g., "Don't use: vibe1, vibe2...")
|
|
64
|
+
|
|
65
|
+
**How it works:**
|
|
66
|
+
|
|
67
|
+
1. When you send a message, shows "Channeling {theme}..." placeholder
|
|
68
|
+
2. AI generates a themed message in the background (3s timeout)
|
|
69
|
+
3. Message updates to the themed version (e.g., "Engaging warp drive...")
|
|
70
|
+
4. During long tasks, refreshes on tool calls (rate-limited, default 30s)
|
|
71
|
+
5. Cost and latency depend on your configured `workingVibeModel`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Wishcraft cockpit for the pi coding agent — powerline status, vibes, idea inbox, bash mode, and full customization.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"*.md",
|
|
9
9
|
"*.json",
|
|
10
10
|
"banner.png",
|
|
11
|
+
"docs/**/*.md",
|
|
11
12
|
"src/**/*.ts",
|
|
12
13
|
"src/**/*.txt",
|
|
13
14
|
"bash-mode/**/*.ts",
|
package/queue/store.ts
CHANGED
|
@@ -23,7 +23,9 @@ import { ACTIVE_QUEUE_STATUSES } from "./types.ts";
|
|
|
23
23
|
|
|
24
24
|
const STORE_DIR = "powerline-footer";
|
|
25
25
|
const INBOX_FILE = "inbox.jsonl";
|
|
26
|
+
const ARCHIVE_FILE = "inbox.archive.jsonl";
|
|
26
27
|
const ALIASES_FILE = "projects.json";
|
|
28
|
+
const DEFAULT_SENT_RETENTION_MS = 24 * 60 * 60 * 1000;
|
|
27
29
|
const LOCK_RETRY_MS = 25;
|
|
28
30
|
const LOCK_TIMEOUT_MS = 2000;
|
|
29
31
|
|
|
@@ -126,10 +128,12 @@ function sleepSync(ms: number): void {
|
|
|
126
128
|
export function getQueueStorePaths(): {
|
|
127
129
|
inboxPath: string;
|
|
128
130
|
aliasesPath: string;
|
|
131
|
+
archivePath: string;
|
|
129
132
|
} {
|
|
130
133
|
return {
|
|
131
134
|
inboxPath: getAgentPath(STORE_DIR, INBOX_FILE),
|
|
132
135
|
aliasesPath: getAgentPath(STORE_DIR, ALIASES_FILE),
|
|
136
|
+
archivePath: getAgentPath(STORE_DIR, ARCHIVE_FILE),
|
|
133
137
|
};
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -155,13 +159,23 @@ export function createQueueItem(
|
|
|
155
159
|
export class PowerlineQueueStore {
|
|
156
160
|
private readonly inboxPath: string;
|
|
157
161
|
private readonly aliasesPath: string;
|
|
162
|
+
private readonly archivePath: string;
|
|
163
|
+
/** How long completed (sent) items stay in the inbox before pruning/archiving. */
|
|
164
|
+
private sentRetentionMs: number = DEFAULT_SENT_RETENTION_MS;
|
|
158
165
|
|
|
159
166
|
constructor(
|
|
160
167
|
inboxPath: string = getQueueStorePaths().inboxPath,
|
|
161
168
|
aliasesPath: string = getQueueStorePaths().aliasesPath,
|
|
169
|
+
archivePath: string = getQueueStorePaths().archivePath,
|
|
162
170
|
) {
|
|
163
171
|
this.inboxPath = inboxPath;
|
|
164
172
|
this.aliasesPath = aliasesPath;
|
|
173
|
+
this.archivePath = archivePath;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Configure the retention window for completed items (clamped to >= 1 hour). */
|
|
177
|
+
setSentRetentionMs(ms: number): void {
|
|
178
|
+
this.sentRetentionMs = Math.max(60 * 60 * 1000, Number.isFinite(ms) ? ms : DEFAULT_SENT_RETENTION_MS);
|
|
165
179
|
}
|
|
166
180
|
|
|
167
181
|
list(): PowerlineQueueItem[] {
|
|
@@ -323,13 +337,16 @@ export class PowerlineQueueStore {
|
|
|
323
337
|
}
|
|
324
338
|
}
|
|
325
339
|
|
|
326
|
-
private writeItems(
|
|
340
|
+
private writeItems(
|
|
341
|
+
items: readonly PowerlineQueueItem[],
|
|
342
|
+
now: number = Date.now(),
|
|
343
|
+
): void {
|
|
327
344
|
mkdirSync(dirname(this.inboxPath), { recursive: true });
|
|
345
|
+
const retentionCutoff = now - this.sentRetentionMs;
|
|
328
346
|
const activeOrRecent = items
|
|
329
347
|
.filter(
|
|
330
348
|
(item) =>
|
|
331
|
-
item.status !== "sent" ||
|
|
332
|
-
Date.now() - item.updatedAt < 24 * 60 * 60 * 1000,
|
|
349
|
+
item.status !== "sent" || item.updatedAt >= retentionCutoff,
|
|
333
350
|
)
|
|
334
351
|
.map((item) => JSON.stringify(item))
|
|
335
352
|
.join("\n");
|
|
@@ -339,6 +356,58 @@ export class PowerlineQueueStore {
|
|
|
339
356
|
);
|
|
340
357
|
}
|
|
341
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Move completed (sent) items older than the retention window out of the
|
|
361
|
+
* inbox into the archive file, keeping the inbox lean. Returns how many
|
|
362
|
+
* items were archived and how many sent items remain in the inbox.
|
|
363
|
+
*
|
|
364
|
+
* `archiveSentItems` and `writeItems` share one `now` snapshot so a sent
|
|
365
|
+
* item counted as remaining cannot be pruned on the same write.
|
|
366
|
+
*/
|
|
367
|
+
archiveSentItems(olderThanMs: number = this.sentRetentionMs): {
|
|
368
|
+
archived: number;
|
|
369
|
+
remainingSent: number;
|
|
370
|
+
} {
|
|
371
|
+
return this.withStoreLock(() => {
|
|
372
|
+
const now = Date.now();
|
|
373
|
+
const items = this.list();
|
|
374
|
+
// writeItems prunes sent items older than sentRetentionMs on every write.
|
|
375
|
+
// If the requested archive window were wider than that, items in the gap
|
|
376
|
+
// would be silently dropped (neither archived nor kept) while still being
|
|
377
|
+
// reported as "remaining". Clamp the cutoff so anything past the inbox
|
|
378
|
+
// retention window is archived rather than lost.
|
|
379
|
+
const effectiveOlderThanMs = Math.min(olderThanMs, this.sentRetentionMs);
|
|
380
|
+
const cutoff = now - effectiveOlderThanMs;
|
|
381
|
+
const archived: PowerlineQueueItem[] = [];
|
|
382
|
+
const kept: PowerlineQueueItem[] = [];
|
|
383
|
+
let remainingSent = 0;
|
|
384
|
+
|
|
385
|
+
for (const item of items) {
|
|
386
|
+
if (item.status === "sent" && item.updatedAt < cutoff) {
|
|
387
|
+
archived.push(item);
|
|
388
|
+
} else {
|
|
389
|
+
kept.push(item);
|
|
390
|
+
if (item.status === "sent") remainingSent += 1;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (archived.length > 0) {
|
|
395
|
+
mkdirSync(dirname(this.archivePath), { recursive: true });
|
|
396
|
+
const existing = existsSync(this.archivePath)
|
|
397
|
+
? readFileSync(this.archivePath, "utf-8")
|
|
398
|
+
: "";
|
|
399
|
+
const lines = archived.map((item) => JSON.stringify(item)).join("\n");
|
|
400
|
+
this.writeAtomic(
|
|
401
|
+
this.archivePath,
|
|
402
|
+
`${existing.trimEnd() ? `${existing.trimEnd()}\n` : ""}${lines}\n`,
|
|
403
|
+
);
|
|
404
|
+
this.writeItems(kept, now);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return { archived: archived.length, remainingSent };
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
342
411
|
private writeJson(path: string, value: unknown): void {
|
|
343
412
|
mkdirSync(dirname(path), { recursive: true });
|
|
344
413
|
this.writeAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "./primitives.ts";
|
|
10
10
|
import { normalizeSegmentOptions } from "./segment-options.ts";
|
|
11
11
|
import { normalizeStatusLineSegmentId } from "./segment-ids.ts";
|
|
12
|
+
import { isNotificationExtensionStatus } from "./extension-statuses.ts";
|
|
12
13
|
import type {
|
|
13
14
|
ColorScheme,
|
|
14
15
|
CustomSegmentConfig,
|
|
@@ -180,3 +181,47 @@ export function normalizeCustomItems(raw: unknown): CustomStatusItem[] {
|
|
|
180
181
|
|
|
181
182
|
return [...deduped.values()];
|
|
182
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* `powerline.customItems.auto` — when `customItems` is an object/record with
|
|
187
|
+
* an `auto: true` key, offer live extension status keys as segments without
|
|
188
|
+
* listing each one explicitly (the ChefBar status bridge).
|
|
189
|
+
*/
|
|
190
|
+
export function normalizeCustomItemsAuto(raw: unknown): boolean {
|
|
191
|
+
if (isRecord(raw)) return raw.auto === true;
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Build the effective custom item list at render time. When `enabled`, every
|
|
197
|
+
* non-notification extension status key becomes an implicit right-aligned
|
|
198
|
+
* custom item unless it is already claimed by an explicit item, is an
|
|
199
|
+
* excluded internal key, or is not a valid segment id. Auto items are marked
|
|
200
|
+
* `excludeFromExtensionStatuses` so each status renders exactly once.
|
|
201
|
+
*/
|
|
202
|
+
export function deriveAutoCustomItems(
|
|
203
|
+
customItems: readonly CustomStatusItem[],
|
|
204
|
+
extensionStatuses: ReadonlyMap<string, string>,
|
|
205
|
+
enabled: boolean,
|
|
206
|
+
excludedStatusKeys: ReadonlySet<string>,
|
|
207
|
+
): CustomStatusItem[] {
|
|
208
|
+
if (!enabled) return [...customItems];
|
|
209
|
+
|
|
210
|
+
const claimed = new Set(customItems.map((item) => item.statusKey));
|
|
211
|
+
const auto: CustomStatusItem[] = [];
|
|
212
|
+
for (const [statusKey, value] of extensionStatuses) {
|
|
213
|
+
if (claimed.has(statusKey) || excludedStatusKeys.has(statusKey)) continue;
|
|
214
|
+
if (!normalizeCustomItemId(statusKey)) continue;
|
|
215
|
+
if (isNotificationExtensionStatus(value)) continue;
|
|
216
|
+
|
|
217
|
+
auto.push({
|
|
218
|
+
id: statusKey,
|
|
219
|
+
statusKey,
|
|
220
|
+
position: "right",
|
|
221
|
+
hideWhenMissing: true,
|
|
222
|
+
excludeFromExtensionStatuses: true,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return [...customItems, ...auto];
|
|
227
|
+
}
|
package/src/config/parse.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeCustomItems,
|
|
3
|
+
normalizeCustomItemsAuto,
|
|
3
4
|
normalizeCustomPresets,
|
|
4
5
|
normalizeCustomSegments,
|
|
5
6
|
} from "./custom-items.ts";
|
|
6
7
|
import {
|
|
7
8
|
isRecord,
|
|
8
9
|
normalizeCaptureSigil,
|
|
10
|
+
normalizeCostAlert,
|
|
9
11
|
normalizePlacement,
|
|
10
12
|
normalizePreset,
|
|
13
|
+
normalizeRetentionHours,
|
|
11
14
|
normalizeSegmentLabels,
|
|
12
15
|
normalizeSeparator,
|
|
13
16
|
} from "./primitives.ts";
|
|
@@ -37,7 +40,11 @@ export interface PowerlineConfig {
|
|
|
37
40
|
invalidPlacement: string | null;
|
|
38
41
|
welcome: boolean;
|
|
39
42
|
stashSharpSShortcut: boolean;
|
|
40
|
-
|
|
43
|
+
/** Session cost threshold (USD) for a once-per-session warning. */
|
|
44
|
+
costAlert: number | null;
|
|
45
|
+
/** Auto-promote live extension status keys into `custom:<key>` segments. */
|
|
46
|
+
customItemsAuto: boolean;
|
|
47
|
+
queue: { captureSigil: string | false; retentionHours: number };
|
|
41
48
|
/** User-defined computed segments (command/env/static), keyed by id */
|
|
42
49
|
segments: Record<string, CustomSegmentConfig>;
|
|
43
50
|
/** User-defined presets, keyed by name */
|
|
@@ -63,7 +70,9 @@ export function parsePowerlineConfig(
|
|
|
63
70
|
invalidPlacement: null,
|
|
64
71
|
welcome: true,
|
|
65
72
|
stashSharpSShortcut: false,
|
|
66
|
-
|
|
73
|
+
costAlert: null,
|
|
74
|
+
customItemsAuto: false,
|
|
75
|
+
queue: { captureSigil: "#", retentionHours: 24 },
|
|
67
76
|
segments: {},
|
|
68
77
|
presets: {},
|
|
69
78
|
segmentLabels: {},
|
|
@@ -90,7 +99,10 @@ export function parsePowerlineConfig(
|
|
|
90
99
|
);
|
|
91
100
|
const { placement, invalidPlacement } = normalizePlacement(value.placement);
|
|
92
101
|
const queue = isRecord(value.queue)
|
|
93
|
-
? {
|
|
102
|
+
? {
|
|
103
|
+
captureSigil: normalizeCaptureSigil(value.queue.captureSigil),
|
|
104
|
+
retentionHours: normalizeRetentionHours(value.queue.retentionHours),
|
|
105
|
+
}
|
|
94
106
|
: defaultConfig.queue;
|
|
95
107
|
const customItemIds = new Set(customItems.map((item) => item.id));
|
|
96
108
|
const customPresetDefs = normalizeCustomPresets(
|
|
@@ -119,6 +131,10 @@ export function parsePowerlineConfig(
|
|
|
119
131
|
invalidPlacement,
|
|
120
132
|
welcome: value.welcome !== false,
|
|
121
133
|
stashSharpSShortcut: value.stashSharpSShortcut === true,
|
|
134
|
+
costAlert: normalizeCostAlert(value.costAlert),
|
|
135
|
+
customItemsAuto:
|
|
136
|
+
normalizeCustomItemsAuto(value.customItems) ||
|
|
137
|
+
value.customItemsAuto === true,
|
|
122
138
|
queue,
|
|
123
139
|
segments: customSegments,
|
|
124
140
|
presets: customPresetDefs,
|
|
@@ -6,6 +6,11 @@ export { parsePowerlineConfig } from "./parse.ts";
|
|
|
6
6
|
export { mergeSegmentOptions } from "./segment-options.ts";
|
|
7
7
|
export { mergeSegmentsWithCustomItems } from "./layout.ts";
|
|
8
8
|
export {
|
|
9
|
+
deriveAutoCustomItems,
|
|
10
|
+
normalizeCustomItemsAuto,
|
|
11
|
+
} from "./custom-items.ts";
|
|
12
|
+
export {
|
|
13
|
+
nextPowerlineSettingWithCustomPreset,
|
|
9
14
|
nextPowerlineSettingWithPreset,
|
|
10
15
|
nextPowerlineSettingWithOptions,
|
|
11
16
|
} from "./settings-patch.ts";
|
package/src/config/primitives.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function normalizePlacement(value: unknown): {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const SEPARATOR_STYLES = [
|
|
43
|
+
export const SEPARATOR_STYLES = [
|
|
44
44
|
"powerline",
|
|
45
45
|
"powerline-thin",
|
|
46
46
|
"slash",
|
|
@@ -97,6 +97,18 @@ export function normalizeCaptureSigil(value: unknown): string | false {
|
|
|
97
97
|
return normalized && !/\s/.test(normalized) ? normalized : "#";
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export function normalizeRetentionHours(value: unknown): number {
|
|
101
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 24;
|
|
102
|
+
return Math.min(24 * 365, Math.max(1, Math.floor(value)));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function normalizeCostAlert(value: unknown): number | null {
|
|
106
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return Math.round(value * 100) / 100;
|
|
110
|
+
}
|
|
111
|
+
|
|
100
112
|
export function normalizeCustomSegmentType(
|
|
101
113
|
value: unknown,
|
|
102
114
|
): "command" | "env" | "static" | null {
|
|
@@ -115,41 +115,46 @@ export function normalizeSegmentOptions(
|
|
|
115
115
|
...(typeof raw.openPorts.includeUdp === "boolean"
|
|
116
116
|
? { includeUdp: raw.openPorts.includeUdp }
|
|
117
117
|
: {}),
|
|
118
|
+
...(typeof raw.openPorts.host === "string" && raw.openPorts.host.trim()
|
|
119
|
+
? { host: raw.openPorts.host.trim() }
|
|
120
|
+
: {}),
|
|
118
121
|
};
|
|
119
122
|
}
|
|
120
123
|
|
|
121
124
|
if (isRecord(raw.tps)) {
|
|
122
125
|
options.tps = {
|
|
123
|
-
// De lookback gebruikt [0.5·windowMs, 2·windowMs] binnen een 5s-ring, dus
|
|
124
|
-
// windowMs boven 2500 zou nooit een sample vinden. Afkappen op 2500.
|
|
125
126
|
...(typeof raw.tps.windowMs === "number" &&
|
|
126
|
-
Number.isFinite(raw.tps.windowMs)
|
|
127
|
-
|
|
128
|
-
? { windowMs: Math.min(raw.tps.windowMs, 2500) }
|
|
129
|
-
: {}),
|
|
130
|
-
...(raw.tps.mode === "both" ||
|
|
131
|
-
raw.tps.mode === "out" ||
|
|
132
|
-
raw.tps.mode === "in"
|
|
133
|
-
? { mode: raw.tps.mode }
|
|
134
|
-
: {}),
|
|
135
|
-
...(typeof raw.tps.hideIdle === "boolean"
|
|
136
|
-
? { hideIdle: raw.tps.hideIdle }
|
|
127
|
+
Number.isFinite(raw.tps.windowMs)
|
|
128
|
+
? { windowMs: Math.min(5000, Math.max(500, Math.floor(raw.tps.windowMs))) }
|
|
137
129
|
: {}),
|
|
138
130
|
};
|
|
139
131
|
}
|
|
140
132
|
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
// Generic `template` override for every segment option group:
|
|
134
|
+
// segmentOptions.<id>.template: "{value} tok/s" replaces the value text.
|
|
135
|
+
const TEMPLATE_OPTION_KEYS = [
|
|
136
|
+
"model",
|
|
137
|
+
"path",
|
|
138
|
+
"git",
|
|
139
|
+
"time",
|
|
140
|
+
"cost",
|
|
141
|
+
"context",
|
|
142
|
+
"cache_read",
|
|
143
|
+
"openPorts",
|
|
144
|
+
"tps",
|
|
145
|
+
] as const;
|
|
146
|
+
const genericOptions = options as Record<
|
|
147
|
+
(typeof TEMPLATE_OPTION_KEYS)[number],
|
|
148
|
+
{ template?: string } | undefined
|
|
147
149
|
>;
|
|
148
|
-
for (const
|
|
149
|
-
|
|
150
|
-
if (typeof
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
for (const key of TEMPLATE_OPTION_KEYS) {
|
|
151
|
+
const rawOpt = raw[key];
|
|
152
|
+
if (isRecord(rawOpt) && typeof rawOpt.template === "string") {
|
|
153
|
+
const template = rawOpt.template.trim();
|
|
154
|
+
if (template) {
|
|
155
|
+
genericOptions[key] = { ...genericOptions[key], template };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
return options;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isRecord } from "./primitives.ts";
|
|
2
2
|
import type { PowerlineConfig } from "./parse.ts";
|
|
3
|
-
import type { StatusLinePreset } from "./types.ts";
|
|
3
|
+
import type { CustomPresetConfig, StatusLinePreset } from "./types.ts";
|
|
4
4
|
|
|
5
5
|
export function nextPowerlineSettingWithPreset(
|
|
6
6
|
existingPowerlineSetting: unknown,
|
|
@@ -12,10 +12,31 @@ export function nextPowerlineSettingWithPreset(
|
|
|
12
12
|
return { ...existingPowerlineSetting, preset };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export function nextPowerlineSettingWithCustomPreset(
|
|
16
|
+
existingPowerlineSetting: unknown,
|
|
17
|
+
name: string,
|
|
18
|
+
presetDef: CustomPresetConfig,
|
|
19
|
+
): unknown {
|
|
20
|
+
if (!isRecord(existingPowerlineSetting)) {
|
|
21
|
+
return { preset: name, presets: { [name]: presetDef } };
|
|
22
|
+
}
|
|
23
|
+
const existingPresets = isRecord(existingPowerlineSetting.presets)
|
|
24
|
+
? existingPowerlineSetting.presets
|
|
25
|
+
: {};
|
|
26
|
+
return {
|
|
27
|
+
...existingPowerlineSetting,
|
|
28
|
+
preset: name,
|
|
29
|
+
presets: { ...existingPresets, [name]: presetDef },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
export function nextPowerlineSettingWithOptions(
|
|
16
34
|
existingPowerlineSetting: unknown,
|
|
17
35
|
updates: Partial<
|
|
18
|
-
Pick<
|
|
36
|
+
Pick<
|
|
37
|
+
PowerlineConfig,
|
|
38
|
+
"welcome" | "stashSharpSShortcut" | "placement" | "disabledSegments"
|
|
39
|
+
>
|
|
19
40
|
>,
|
|
20
41
|
currentPreset: StatusLinePreset,
|
|
21
42
|
): unknown {
|
package/src/config/types.ts
CHANGED
|
@@ -80,14 +80,23 @@ export type PowerlinePlacement = "above" | "below";
|
|
|
80
80
|
export type StatusLinePreset =
|
|
81
81
|
"default" | "minimal" | "compact" | "full" | "nerd" | "ascii" | "chef";
|
|
82
82
|
|
|
83
|
+
// Optional `{value}` template override shared by all segment option groups.
|
|
84
|
+
// Replaces `{value}` in the rendered value text (e.g. "{value} tok/s").
|
|
85
|
+
export interface SegmentFormatOption {
|
|
86
|
+
template?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
83
89
|
// Per-segment options
|
|
84
90
|
export interface StatusLineSegmentOptions {
|
|
85
|
-
model?:
|
|
86
|
-
|
|
91
|
+
model?: SegmentFormatOption & {
|
|
92
|
+
showThinkingLevel?: boolean;
|
|
93
|
+
display?: "name" | "qualified";
|
|
94
|
+
};
|
|
95
|
+
path?: SegmentFormatOption & {
|
|
87
96
|
mode?: "basename" | "abbreviated" | "full";
|
|
88
97
|
maxLength?: number;
|
|
89
98
|
};
|
|
90
|
-
git?: {
|
|
99
|
+
git?: SegmentFormatOption & {
|
|
91
100
|
showBranch?: boolean;
|
|
92
101
|
showStaged?: boolean;
|
|
93
102
|
showUnstaged?: boolean;
|
|
@@ -103,25 +112,26 @@ export interface StatusLineSegmentOptions {
|
|
|
103
112
|
/** Max length of the commit subject before truncation. Default 24. */
|
|
104
113
|
maxCommitSubjectLength?: number;
|
|
105
114
|
};
|
|
106
|
-
time?: { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
107
|
-
cost?: {
|
|
115
|
+
time?: SegmentFormatOption & { format?: "12h" | "24h"; showSeconds?: boolean };
|
|
116
|
+
cost?: SegmentFormatOption & {
|
|
108
117
|
subscriptionDisplay?: "subscription" | "reported-cost" | "both";
|
|
109
118
|
currency?: CostCurrencyCode;
|
|
110
119
|
};
|
|
111
|
-
context?: { format?: "full" | "percent" };
|
|
112
|
-
cache_read?: { format?: "tokens" | "percent" | "both" };
|
|
113
|
-
openPorts?: {
|
|
114
|
-
/** Include UDP listeners (mDNS/DHCP/ephemeral) in the count. Default false. */
|
|
120
|
+
context?: SegmentFormatOption & { format?: "full" | "percent" };
|
|
121
|
+
cache_read?: SegmentFormatOption & { format?: "tokens" | "percent" | "both" };
|
|
122
|
+
openPorts?: SegmentFormatOption & {
|
|
123
|
+
/** Include UDP listeners (mDNS/DHCP/ephemeral) in the count. Default false. */
|
|
124
|
+
includeUdp?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* SSH host to probe instead of the local machine (fleet open-ports).
|
|
127
|
+
* Best-effort, opt-in: requires passwordless/agent SSH to that host and
|
|
128
|
+
* falls back to `?` when the probe cannot run.
|
|
129
|
+
*/
|
|
130
|
+
host?: string;
|
|
115
131
|
};
|
|
116
|
-
tps?: {
|
|
117
|
-
/**
|
|
132
|
+
tps?: SegmentFormatOption & {
|
|
133
|
+
/** Sliding rate window length in ms (default 1000; wider = smoother, e.g. 2000 for fast models). */
|
|
118
134
|
windowMs?: number;
|
|
119
|
-
/** Which rates to show. Default "both". */
|
|
120
|
-
mode?: "both" | "out" | "in";
|
|
121
|
-
/** Hide the segment entirely when idle (both rates 0). Default false (show dim "0"). */
|
|
122
|
-
hideIdle?: boolean;
|
|
123
|
-
/** Render template applied centrally; {value} is the plain segment content. Overrides segmentLabels. */
|
|
124
|
-
template?: string;
|
|
125
135
|
};
|
|
126
136
|
}
|
|
127
137
|
|
|
@@ -241,12 +251,17 @@ export interface SegmentContext {
|
|
|
241
251
|
extensionStatuses: ReadonlyMap<string, string>;
|
|
242
252
|
hiddenExtensionStatusKeys: ReadonlySet<string>;
|
|
243
253
|
customItemsById: ReadonlyMap<string, CustomStatusItem>;
|
|
254
|
+
/** Explicit custom items + auto-promoted status items (customItems.auto). */
|
|
255
|
+
effectiveCustomItems: readonly CustomStatusItem[];
|
|
244
256
|
|
|
245
257
|
// Options
|
|
246
258
|
options: StatusLineSegmentOptions;
|
|
247
259
|
/** Per-segment custom text labels (from powerline.segmentLabels). */
|
|
248
260
|
segmentLabels: ReadonlyMap<string, string>;
|
|
249
261
|
|
|
262
|
+
/** Daily token budget progress (wishcraft.tokenBudget.daily). */
|
|
263
|
+
tokenBudget?: { dailyLimit: number | null; dailyUsed: number };
|
|
264
|
+
|
|
250
265
|
// Theming
|
|
251
266
|
theme: ThemeLike;
|
|
252
267
|
colors: ColorScheme;
|