@groeponline/pi-wishcraft 1.4.16 → 1.4.17
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 +11 -0
- package/bash-mode/editor.ts +61 -6
- package/bash-mode/pty-session.ts +45 -3
- package/bash-mode/transcript.ts +67 -2
- package/docs/bash-mode.md +2 -0
- package/package.json +1 -1
- package/src/config/segment-options.ts +95 -83
- package/src/config/structural-preset-data.ts +69 -0
- package/src/config/structural-preset-table.ts +445 -0
- package/src/config/structural-presets.ts +9 -499
- package/src/extension/ui/custom-editor.ts +22 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.4.17] - 2026-09-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Bash forward-mode privacy notice (issue #71): the first keystroke forwarded to a running command raises an info notice that typed input may echo into the transcript; documented in `docs/bash-mode.md`.
|
|
9
|
+
- Managed PTY suite is now script-gated (issue #73): PTY-core tests skip when `script(1)` is missing instead of silently passing in degraded pipe mode, the basic run asserts a real PTY transport, and the explicit pipe-mode tests still cover the fallback.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Bash paste-while-running (issue #72): a bracketed paste performed while a command runs is stripped of its markers and forwarded to the child stdin — including split delivery across input chunks — instead of silently queuing in the editor buffer behind the command.
|
|
13
|
+
- PTY/transcript memory bounds (from jan `fix/runtime-pty-bounds`): a single unterminated output line is capped at a 64 KiB UTF-8 tail with a one-time notice, the partial-escape tail is bounded, and the active transcript command trims head lines/bytes within limits instead of growing unbounded.
|
|
14
|
+
- Segment-options spread hygiene (CodeFactor #86–89): `normalizeSegmentOptions` assigns fields imperatively instead of spreading conditional empty objects.
|
|
15
|
+
|
|
5
16
|
## [1.4.16] - 2026-09-05
|
|
6
17
|
|
|
7
18
|
### Fixed
|
package/bash-mode/editor.ts
CHANGED
|
@@ -31,6 +31,24 @@ import type {
|
|
|
31
31
|
GhostSuggestion,
|
|
32
32
|
} from "./types.ts";
|
|
33
33
|
|
|
34
|
+
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
35
|
+
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
36
|
+
|
|
37
|
+
function splitTrailingPasteMarkerPrefix(value: string): [string, string] {
|
|
38
|
+
let tail = "";
|
|
39
|
+
for (const marker of [BRACKETED_PASTE_START, BRACKETED_PASTE_END]) {
|
|
40
|
+
const max = Math.min(marker.length - 1, value.length);
|
|
41
|
+
for (let length = max; length > tail.length; length -= 1) {
|
|
42
|
+
const candidate = value.slice(-length);
|
|
43
|
+
if (marker.startsWith(candidate)) {
|
|
44
|
+
tail = candidate;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return tail ? [value.slice(0, -tail.length), tail] : [value, ""];
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
export class BashModeEditor extends CustomEditor {
|
|
35
53
|
private readonly keybindingsRef: KeybindingsManager;
|
|
36
54
|
private readonly optionsRef: BashModeEditorOptions;
|
|
@@ -43,6 +61,10 @@ export class BashModeEditor extends CustomEditor {
|
|
|
43
61
|
private ghostAbort: AbortController | null = null;
|
|
44
62
|
private ghostToken = 0;
|
|
45
63
|
private ghostSchedule: ReturnType<typeof setTimeout> | null = null;
|
|
64
|
+
/** A bracketed paste was split across input chunks while forwarding. */
|
|
65
|
+
private forwardPasteOpen = false;
|
|
66
|
+
/** Trailing prefix of a bracketed-paste delimiter awaiting the next input chunk. */
|
|
67
|
+
private forwardPasteMarkerTail = "";
|
|
46
68
|
|
|
47
69
|
constructor(
|
|
48
70
|
tui: any,
|
|
@@ -112,6 +134,25 @@ export class BashModeEditor extends CustomEditor {
|
|
|
112
134
|
}
|
|
113
135
|
|
|
114
136
|
handleInput(data: string): void {
|
|
137
|
+
// v2 forward-mode gate, shared by the paste branch (issue #72) and the
|
|
138
|
+
// per-keystroke forward further down: while a command runs, input
|
|
139
|
+
// belongs to the child stdin, never to the editor buffer behind it.
|
|
140
|
+
const forwardActive =
|
|
141
|
+
this.optionsRef.isBashModeActive() &&
|
|
142
|
+
this.optionsRef.isShellRunning() &&
|
|
143
|
+
(this.optionsRef.forwardWhileRunning?.() ?? false) &&
|
|
144
|
+
this.optionsRef.onForwardInput != null;
|
|
145
|
+
if (!forwardActive) {
|
|
146
|
+
this.forwardPasteOpen = false;
|
|
147
|
+
this.forwardPasteMarkerTail = "";
|
|
148
|
+
} else {
|
|
149
|
+
data = this.forwardPasteMarkerTail + data;
|
|
150
|
+
const [complete, tail] = splitTrailingPasteMarkerPrefix(data);
|
|
151
|
+
data = complete;
|
|
152
|
+
this.forwardPasteMarkerTail = tail;
|
|
153
|
+
if (!data) return;
|
|
154
|
+
}
|
|
155
|
+
|
|
115
156
|
const droppedPathText = droppedPathTextFromInput(data);
|
|
116
157
|
if (droppedPathText !== null) {
|
|
117
158
|
this.insertTextAtCursor(droppedPathText);
|
|
@@ -125,8 +166,23 @@ export class BashModeEditor extends CustomEditor {
|
|
|
125
166
|
}
|
|
126
167
|
|
|
127
168
|
const pasteInProgress =
|
|
128
|
-
data.includes(
|
|
169
|
+
data.includes(BRACKETED_PASTE_START) ||
|
|
170
|
+
data.includes(BRACKETED_PASTE_END) ||
|
|
171
|
+
Reflect.get(this, "isInPaste") === true ||
|
|
172
|
+
(forwardActive && this.forwardPasteOpen);
|
|
129
173
|
if (pasteInProgress) {
|
|
174
|
+
if (forwardActive) {
|
|
175
|
+
const startIndex = data.lastIndexOf(BRACKETED_PASTE_START);
|
|
176
|
+
const endIndex = data.lastIndexOf(BRACKETED_PASTE_END);
|
|
177
|
+
if (startIndex !== -1 || endIndex !== -1) {
|
|
178
|
+
this.forwardPasteOpen = startIndex > endIndex;
|
|
179
|
+
}
|
|
180
|
+
const payload = data
|
|
181
|
+
.replaceAll(BRACKETED_PASTE_START, "")
|
|
182
|
+
.replaceAll(BRACKETED_PASTE_END, "");
|
|
183
|
+
if (payload) this.optionsRef.onForwardInput?.(payload);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
130
186
|
super.handleInput(data);
|
|
131
187
|
if (Reflect.get(this, "isInPaste") === true) {
|
|
132
188
|
return;
|
|
@@ -169,15 +225,14 @@ export class BashModeEditor extends CustomEditor {
|
|
|
169
225
|
// (\x04) must also forward — line-oriented stdin programs (read,
|
|
170
226
|
// sudo, git rebase -i) cannot proceed without a line terminator.
|
|
171
227
|
// Opt-in via forwardWhileRunning so v1 run-blocked behavior unchanged.
|
|
228
|
+
// (forwardActive is computed above; bracketed pastes take the paste
|
|
229
|
+
// branch, which forwards the stripped payload too.)
|
|
172
230
|
if (
|
|
173
|
-
|
|
174
|
-
this.optionsRef.isShellRunning() &&
|
|
175
|
-
(this.optionsRef.forwardWhileRunning?.() ?? false) &&
|
|
176
|
-
this.optionsRef.onForwardInput != null &&
|
|
231
|
+
forwardActive &&
|
|
177
232
|
!isKeyRelease(data) &&
|
|
178
233
|
(isPrintableInput(data) || data === "\r" || data === "\n" || data === "\x04")
|
|
179
234
|
) {
|
|
180
|
-
this.optionsRef.onForwardInput(data);
|
|
235
|
+
this.optionsRef.onForwardInput?.(data);
|
|
181
236
|
return;
|
|
182
237
|
}
|
|
183
238
|
|
package/bash-mode/pty-session.ts
CHANGED
|
@@ -22,6 +22,38 @@ import { basename, join } from "node:path";
|
|
|
22
22
|
import { randomBytes } from "node:crypto";
|
|
23
23
|
import { spawn, spawnSync, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
24
24
|
|
|
25
|
+
const MAX_PENDING_LINE_BYTES = 64 * 1024;
|
|
26
|
+
const MAX_ESCAPE_TAIL_CHARS = 4096;
|
|
27
|
+
const TRUNCATED_LINE_NOTICE = "[wishcraft] output line truncated; keeping tail";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Restricts a string to its trailing UTF-8 bytes without splitting a multibyte character.
|
|
31
|
+
*
|
|
32
|
+
* @param value - The string to restrict
|
|
33
|
+
* @param maxBytes - The maximum number of UTF-8 bytes to retain
|
|
34
|
+
* @returns The UTF-8-safe trailing portion of `value`
|
|
35
|
+
*/
|
|
36
|
+
function utf8Tail(value: string, maxBytes: number): string {
|
|
37
|
+
if (maxBytes <= 0) return "";
|
|
38
|
+
const bytes = Buffer.from(value, "utf8");
|
|
39
|
+
if (bytes.length <= maxBytes) return value;
|
|
40
|
+
let start = bytes.length - maxBytes;
|
|
41
|
+
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
|
|
42
|
+
return bytes.subarray(start).toString("utf8");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Limits a partial terminal escape sequence to the configured character bound while preserving its introducer.
|
|
47
|
+
*
|
|
48
|
+
* @param value - The partial escape sequence to limit
|
|
49
|
+
* @returns The original value or a bounded value with its escape introducer preserved
|
|
50
|
+
*/
|
|
51
|
+
function boundedEscapeTail(value: string): string {
|
|
52
|
+
if (value.length <= MAX_ESCAPE_TAIL_CHARS) return value;
|
|
53
|
+
const prefixLength = value.startsWith("\x1b") && value.length > 1 ? 2 : 1;
|
|
54
|
+
return value.slice(0, prefixLength) + value.slice(-(MAX_ESCAPE_TAIL_CHARS - prefixLength));
|
|
55
|
+
}
|
|
56
|
+
|
|
25
57
|
/**
|
|
26
58
|
* Fresh per-command completion delimiter. The wrapper prints it after the
|
|
27
59
|
* sourced script ends; because it is unpredictable, a command cannot forge
|
|
@@ -121,6 +153,8 @@ interface RunningCommand {
|
|
|
121
153
|
escapeTail: string;
|
|
122
154
|
/** Wrapper result observed on stdout; publication waits for child close. */
|
|
123
155
|
pendingResult: PtyRunResult | null;
|
|
156
|
+
/** Emit at most one notice when a single output line exceeds the pending cap. */
|
|
157
|
+
lineTruncated: boolean;
|
|
124
158
|
resolve: (result: PtyRunResult) => void;
|
|
125
159
|
settled: boolean;
|
|
126
160
|
}
|
|
@@ -182,6 +216,7 @@ export class PtyShellSession {
|
|
|
182
216
|
buffer: "",
|
|
183
217
|
escapeTail: "",
|
|
184
218
|
pendingResult: null,
|
|
219
|
+
lineTruncated: false,
|
|
185
220
|
resolve: (result) => {
|
|
186
221
|
if (running.settled) return;
|
|
187
222
|
running.settled = true;
|
|
@@ -308,8 +343,9 @@ export class PtyShellSession {
|
|
|
308
343
|
let work = merged;
|
|
309
344
|
const partial = /(?:\x1b(?:\[[0-9;?]*[ -/]*|\][^\x07\x1b]*|[P^_][^\x07\x1b]*))$|\x1b$/.exec(work);
|
|
310
345
|
if (partial) {
|
|
311
|
-
|
|
312
|
-
|
|
346
|
+
const rawTail = partial[0];
|
|
347
|
+
running.escapeTail = boundedEscapeTail(rawTail);
|
|
348
|
+
work = work.slice(0, work.length - rawTail.length);
|
|
313
349
|
}
|
|
314
350
|
|
|
315
351
|
const color = this.color && this.state.mode === "pty";
|
|
@@ -317,7 +353,13 @@ export class PtyShellSession {
|
|
|
317
353
|
|
|
318
354
|
running.buffer += filtered;
|
|
319
355
|
const parts = running.buffer.split("\n");
|
|
320
|
-
|
|
356
|
+
const remainder = parts.pop() ?? "";
|
|
357
|
+
const boundedRemainder = utf8Tail(remainder, MAX_PENDING_LINE_BYTES);
|
|
358
|
+
if (boundedRemainder !== remainder && !running.lineTruncated) {
|
|
359
|
+
running.lineTruncated = true;
|
|
360
|
+
this.onOutput(TRUNCATED_LINE_NOTICE);
|
|
361
|
+
}
|
|
362
|
+
running.buffer = boundedRemainder;
|
|
321
363
|
|
|
322
364
|
for (const rawLine of parts) {
|
|
323
365
|
const line = rawLine.replace(/\r$/, "");
|
package/bash-mode/transcript.ts
CHANGED
|
@@ -4,6 +4,29 @@ function byteLength(value: string): number {
|
|
|
4
4
|
return Buffer.byteLength(value, "utf8");
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Extracts the end of a string within a UTF-8 byte limit.
|
|
9
|
+
*
|
|
10
|
+
* @param value - The source string
|
|
11
|
+
* @param maxBytes - The maximum number of UTF-8 bytes to include
|
|
12
|
+
* @returns The UTF-8-safe tail of `value`
|
|
13
|
+
*/
|
|
14
|
+
function utf8Tail(value: string, maxBytes: number): string {
|
|
15
|
+
if (maxBytes <= 0) return "";
|
|
16
|
+
const bytes = Buffer.from(value, "utf8");
|
|
17
|
+
if (bytes.length <= maxBytes) return value;
|
|
18
|
+
|
|
19
|
+
let start = bytes.length - maxBytes;
|
|
20
|
+
while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start += 1;
|
|
21
|
+
return bytes.subarray(start).toString("utf8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalizes lines by removing carriage returns and splitting embedded newline characters.
|
|
26
|
+
*
|
|
27
|
+
* @param lines - The lines to normalize
|
|
28
|
+
* @returns The normalized lines
|
|
29
|
+
*/
|
|
7
30
|
function compactLines(lines: string[]): string[] {
|
|
8
31
|
const normalized: string[] = [];
|
|
9
32
|
for (const line of lines) {
|
|
@@ -119,8 +142,50 @@ export class BashTranscriptStore {
|
|
|
119
142
|
this.commandIndex.delete(removed.id);
|
|
120
143
|
this.totalLines = Math.max(0, this.totalLines - removed.output.length);
|
|
121
144
|
this.totalBytes = Math.max(0, this.totalBytes - removed.outputBytes);
|
|
122
|
-
this.
|
|
123
|
-
|
|
145
|
+
this.markTruncated(removed);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// A single active command used to bypass both global limits because there
|
|
149
|
+
// was no older command to evict. Trim that command from the head instead:
|
|
150
|
+
// bash mode is a tail-oriented live view, so the newest output is the
|
|
151
|
+
// useful part and memory remains bounded even for multi-megabyte logs.
|
|
152
|
+
const oldest = this.commands[0];
|
|
153
|
+
if (!oldest) return;
|
|
154
|
+
|
|
155
|
+
while (oldest.output.length > 0 && this.totalLines > this.settings.transcriptMaxLines) {
|
|
156
|
+
this.dropOldestLine(oldest);
|
|
157
|
+
}
|
|
158
|
+
while (oldest.output.length > 1 && this.totalBytes > this.settings.transcriptMaxBytes) {
|
|
159
|
+
this.dropOldestLine(oldest);
|
|
124
160
|
}
|
|
161
|
+
|
|
162
|
+
if (oldest.output.length === 1 && this.totalBytes > this.settings.transcriptMaxBytes) {
|
|
163
|
+
const previous = oldest.output[0]!;
|
|
164
|
+
const previousBytes = byteLength(previous) + 1;
|
|
165
|
+
const bytesOutsideLine = Math.max(0, this.totalBytes - previousBytes);
|
|
166
|
+
const lineBudget = Math.max(0, this.settings.transcriptMaxBytes - bytesOutsideLine - 1);
|
|
167
|
+
const tail = utf8Tail(previous, lineBudget);
|
|
168
|
+
const tailBytes = byteLength(tail) + 1;
|
|
169
|
+
oldest.output[0] = tail;
|
|
170
|
+
oldest.outputBytes = Math.max(0, oldest.outputBytes - previousBytes + tailBytes);
|
|
171
|
+
this.totalBytes = Math.max(0, this.totalBytes - previousBytes + tailBytes);
|
|
172
|
+
this.markTruncated(oldest);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private dropOldestLine(command: BashCommandRecord): void {
|
|
177
|
+
const removed = command.output.shift();
|
|
178
|
+
if (removed === undefined) return;
|
|
179
|
+
const removedBytes = byteLength(removed) + 1;
|
|
180
|
+
command.outputBytes = Math.max(0, command.outputBytes - removedBytes);
|
|
181
|
+
this.totalLines = Math.max(0, this.totalLines - 1);
|
|
182
|
+
this.totalBytes = Math.max(0, this.totalBytes - removedBytes);
|
|
183
|
+
this.markTruncated(command);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private markTruncated(command: BashCommandRecord): void {
|
|
187
|
+
if (command.truncated) return;
|
|
188
|
+
command.truncated = true;
|
|
189
|
+
this.truncatedCommands += 1;
|
|
125
190
|
}
|
|
126
191
|
}
|
package/docs/bash-mode.md
CHANGED
|
@@ -24,6 +24,8 @@ The managed shell is persistent for the current pi session. Command output appea
|
|
|
24
24
|
|
|
25
25
|
Commands run under a real PTY via `script(1)` (no native dependency), so programs that read stdin work: printable input typed while a command runs is forwarded to the process, and `ctrl+c` interrupts it. SGR color survives into the transcript when the terminal supports it; `NO_COLOR` renders plain text. When `script(1)` is missing, each command degrades to plain pipe execution with a one-time warning (no color, no interactive stdin).
|
|
26
26
|
|
|
27
|
+
Privacy: forwarded keystrokes echo back through the PTY into the transcript (issue #71), so typing into a password-style prompt persists it in the transcript store. The first forward per run raises an info notice; avoid secrets at interactive prompts or clear the transcript afterwards. Pasted text while a command runs goes to the child stdin too (issue #72) — it never queues silently in the editor.
|
|
28
|
+
|
|
27
29
|
## Shell ghost suggestions
|
|
28
30
|
|
|
29
31
|
Bash mode is ghost-first. Successful per-project shell history is the primary source, while deterministic path and git continuations can still extend an existing command. Shell-native completion probes are disabled so `!command` predictions never spawn interactive shell completion subprocesses.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groeponline/pi-wishcraft",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.17",
|
|
4
4
|
"description": "Operator cockpit for Pi: live powerline status, searchable skills, idea queue, sticky Bash, hooks, policy controls, and session UX.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -2,132 +2,144 @@ import { normalizeCostCurrency } from "../usage/rates.ts";
|
|
|
2
2
|
import { isRecord } from "./primitives.ts";
|
|
3
3
|
import type { StatusLineSegmentOptions } from "./types.ts";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes raw segment configuration into supported status-line segment options.
|
|
7
|
+
*
|
|
8
|
+
* Invalid values are ignored, numeric limits are normalized, string values are trimmed,
|
|
9
|
+
* and non-empty template overrides are preserved for supported option groups.
|
|
10
|
+
*
|
|
11
|
+
* @param raw - Raw segment configuration to normalize
|
|
12
|
+
* @returns The validated and normalized segment options
|
|
13
|
+
*/
|
|
5
14
|
export function normalizeSegmentOptions(
|
|
6
15
|
raw: Record<string, unknown>,
|
|
7
16
|
): StatusLineSegmentOptions {
|
|
8
17
|
const options: StatusLineSegmentOptions = {};
|
|
9
18
|
|
|
10
19
|
if (isRecord(raw.model)) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
const model: NonNullable<StatusLineSegmentOptions["model"]> = {};
|
|
21
|
+
if (typeof raw.model.showThinkingLevel === "boolean") {
|
|
22
|
+
model.showThinkingLevel = raw.model.showThinkingLevel;
|
|
23
|
+
}
|
|
24
|
+
if (raw.model.display === "name" || raw.model.display === "qualified") {
|
|
25
|
+
model.display = raw.model.display;
|
|
26
|
+
}
|
|
27
|
+
options.model = model;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
30
|
if (isRecord(raw.path)) {
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
const path: NonNullable<StatusLineSegmentOptions["path"]> = {};
|
|
32
|
+
if (
|
|
33
|
+
raw.path.mode === "basename" ||
|
|
24
34
|
raw.path.mode === "abbreviated" ||
|
|
25
35
|
raw.path.mode === "full"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
) {
|
|
37
|
+
path.mode = raw.path.mode;
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
typeof raw.path.maxLength === "number" &&
|
|
29
41
|
Number.isFinite(raw.path.maxLength) &&
|
|
30
42
|
raw.path.maxLength > 0
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
43
|
+
) {
|
|
44
|
+
path.maxLength = Math.floor(raw.path.maxLength);
|
|
45
|
+
}
|
|
46
|
+
options.path = path;
|
|
34
47
|
}
|
|
35
48
|
|
|
36
49
|
if (isRecord(raw.git)) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
? { showUntracked: raw.git.showUntracked }
|
|
49
|
-
: {}),
|
|
50
|
-
...(raw.git.polling === "full" ||
|
|
50
|
+
const git: NonNullable<StatusLineSegmentOptions["git"]> = {};
|
|
51
|
+
if (typeof raw.git.showBranch === "boolean") git.showBranch = raw.git.showBranch;
|
|
52
|
+
if (typeof raw.git.showStaged === "boolean") git.showStaged = raw.git.showStaged;
|
|
53
|
+
if (typeof raw.git.showUnstaged === "boolean") {
|
|
54
|
+
git.showUnstaged = raw.git.showUnstaged;
|
|
55
|
+
}
|
|
56
|
+
if (typeof raw.git.showUntracked === "boolean") {
|
|
57
|
+
git.showUntracked = raw.git.showUntracked;
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
raw.git.polling === "full" ||
|
|
51
61
|
raw.git.polling === "branch" ||
|
|
52
62
|
raw.git.polling === "off"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: {}),
|
|
64
|
-
...(typeof raw.git.maxCommitSubjectLength === "number" &&
|
|
63
|
+
) {
|
|
64
|
+
git.polling = raw.git.polling;
|
|
65
|
+
}
|
|
66
|
+
if (typeof raw.git.hostIcon === "boolean") git.hostIcon = raw.git.hostIcon;
|
|
67
|
+
if (typeof raw.git.showAheadBehind === "boolean") {
|
|
68
|
+
git.showAheadBehind = raw.git.showAheadBehind;
|
|
69
|
+
}
|
|
70
|
+
if (typeof raw.git.showCommit === "boolean") git.showCommit = raw.git.showCommit;
|
|
71
|
+
if (
|
|
72
|
+
typeof raw.git.maxCommitSubjectLength === "number" &&
|
|
65
73
|
Number.isFinite(raw.git.maxCommitSubjectLength) &&
|
|
66
74
|
raw.git.maxCommitSubjectLength > 0
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
75
|
+
) {
|
|
76
|
+
git.maxCommitSubjectLength = Math.floor(raw.git.maxCommitSubjectLength);
|
|
77
|
+
}
|
|
78
|
+
options.git = git;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
if (isRecord(raw.time)) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
const time: NonNullable<StatusLineSegmentOptions["time"]> = {};
|
|
83
|
+
if (raw.time.format === "12h" || raw.time.format === "24h") {
|
|
84
|
+
time.format = raw.time.format;
|
|
85
|
+
}
|
|
86
|
+
if (typeof raw.time.showSeconds === "boolean") {
|
|
87
|
+
time.showSeconds = raw.time.showSeconds;
|
|
88
|
+
}
|
|
89
|
+
options.time = time;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
if (isRecord(raw.cost)) {
|
|
84
93
|
const currency = normalizeCostCurrency(raw.cost.currency);
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
const cost: NonNullable<StatusLineSegmentOptions["cost"]> = {};
|
|
95
|
+
if (
|
|
96
|
+
raw.cost.subscriptionDisplay === "subscription" ||
|
|
87
97
|
raw.cost.subscriptionDisplay === "reported-cost" ||
|
|
88
98
|
raw.cost.subscriptionDisplay === "both"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
) {
|
|
100
|
+
cost.subscriptionDisplay = raw.cost.subscriptionDisplay;
|
|
101
|
+
}
|
|
102
|
+
if (currency) cost.currency = currency;
|
|
103
|
+
options.cost = cost;
|
|
93
104
|
}
|
|
94
105
|
|
|
95
106
|
if (isRecord(raw.context)) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
const context: NonNullable<StatusLineSegmentOptions["context"]> = {};
|
|
108
|
+
if (raw.context.format === "full" || raw.context.format === "percent") {
|
|
109
|
+
context.format = raw.context.format;
|
|
110
|
+
}
|
|
111
|
+
options.context = context;
|
|
101
112
|
}
|
|
102
113
|
|
|
103
114
|
if (isRecord(raw.cache_read)) {
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
const cacheRead: NonNullable<StatusLineSegmentOptions["cache_read"]> = {};
|
|
116
|
+
if (
|
|
117
|
+
raw.cache_read.format === "tokens" ||
|
|
106
118
|
raw.cache_read.format === "percent" ||
|
|
107
119
|
raw.cache_read.format === "both"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
120
|
+
) {
|
|
121
|
+
cacheRead.format = raw.cache_read.format;
|
|
122
|
+
}
|
|
123
|
+
options.cache_read = cacheRead;
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
if (isRecord(raw.openPorts)) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
const openPorts: NonNullable<StatusLineSegmentOptions["openPorts"]> = {};
|
|
128
|
+
if (typeof raw.openPorts.includeUdp === "boolean") {
|
|
129
|
+
openPorts.includeUdp = raw.openPorts.includeUdp;
|
|
130
|
+
}
|
|
131
|
+
if (typeof raw.openPorts.host === "string" && raw.openPorts.host.trim()) {
|
|
132
|
+
openPorts.host = raw.openPorts.host.trim();
|
|
133
|
+
}
|
|
134
|
+
options.openPorts = openPorts;
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
if (isRecord(raw.tps)) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
138
|
+
const tps: NonNullable<StatusLineSegmentOptions["tps"]> = {};
|
|
139
|
+
if (typeof raw.tps.windowMs === "number" && Number.isFinite(raw.tps.windowMs)) {
|
|
140
|
+
tps.windowMs = Math.min(5000, Math.max(500, Math.floor(raw.tps.windowMs)));
|
|
141
|
+
}
|
|
142
|
+
options.tps = tps;
|
|
131
143
|
}
|
|
132
144
|
|
|
133
145
|
// Generic `template` override for every segment option group:
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* structural-preset-data.ts
|
|
3
|
+
* ---------------------------------------------------------------------------
|
|
4
|
+
* Shared helpers and chrome/deck/welcome constants for the structural presets.
|
|
5
|
+
* The preset definitions live in structural-preset-table.ts; the public API
|
|
6
|
+
* (getStructuralPreset, etc.) stays in structural-presets.ts.
|
|
7
|
+
* ---------------------------------------------------------------------------
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ChromeSpec, DeckSpec, MotionRef, WelcomeSpec, WishcraftTokens } from "./types.ts";
|
|
11
|
+
import type { MotionEvent } from "../motion/types.ts";
|
|
12
|
+
|
|
13
|
+
export function tokens(partial: Partial<WishcraftTokens>): Partial<WishcraftTokens> {
|
|
14
|
+
return partial;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function motion(
|
|
18
|
+
signature: MotionRef,
|
|
19
|
+
overrides?: Partial<Record<MotionEvent, MotionRef>>,
|
|
20
|
+
): Partial<Record<MotionEvent, MotionRef>> {
|
|
21
|
+
const base: Partial<Record<MotionEvent, MotionRef>> = {
|
|
22
|
+
idle: "wisp",
|
|
23
|
+
thinking: signature,
|
|
24
|
+
streaming: signature,
|
|
25
|
+
"tool.start": signature,
|
|
26
|
+
"tool.end": "rune-bloom",
|
|
27
|
+
"idea.capture": "rune-bloom",
|
|
28
|
+
"skill.insert": "rune-bloom",
|
|
29
|
+
"policy.deny": "rune-bloom",
|
|
30
|
+
repair: signature,
|
|
31
|
+
compact: "bar",
|
|
32
|
+
success: "rune-bloom",
|
|
33
|
+
warning: "rune-bloom",
|
|
34
|
+
error: "rune-bloom",
|
|
35
|
+
};
|
|
36
|
+
return { ...base, ...overrides };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const ROUNDED_CHROME: ChromeSpec = {
|
|
40
|
+
frame: "rounded",
|
|
41
|
+
corners: { tl: "╭", tr: "╮", bl: "╰", br: "╯" },
|
|
42
|
+
dividers: { horizontal: "─", vertical: "│", cross: "┼" },
|
|
43
|
+
density: "medium",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const MINIMAL_CHROME: ChromeSpec = {
|
|
47
|
+
frame: "minimal",
|
|
48
|
+
corners: { tl: " ", tr: " ", bl: " ", br: " " },
|
|
49
|
+
dividers: { horizontal: "─", vertical: " ", cross: " " },
|
|
50
|
+
density: "spacious",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const BORDERLESS_CHROME: ChromeSpec = {
|
|
54
|
+
frame: "borderless",
|
|
55
|
+
corners: { tl: "", tr: "", bl: "", br: "" },
|
|
56
|
+
dividers: { horizontal: "─", vertical: " ", cross: " " },
|
|
57
|
+
density: "spacious",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const DEFAULT_DECK: DeckSpec = {
|
|
61
|
+
navigation: "tabs",
|
|
62
|
+
panelStyle: "framed",
|
|
63
|
+
activityStyle: "pulse",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const DEFAULT_WELCOME: WelcomeSpec = {
|
|
67
|
+
lantern: true,
|
|
68
|
+
ambient: true,
|
|
69
|
+
};
|