@groeponline/pi-wishcraft 1.4.0 → 1.4.5
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/AGENTS.md +28 -0
- package/CHANGELOG.md +36 -0
- package/README.md +22 -18
- package/ROADMAP.md +3 -3
- package/STRATEGY.md +54 -0
- package/bash-mode/editor-input.ts +3 -1
- package/bash-mode/editor.ts +44 -7
- package/bash-mode/forward.ts +64 -0
- package/bash-mode/pty-session.ts +356 -0
- package/bash-mode/ptyshell-managed.ts +145 -0
- package/bash-mode/session-factory.ts +64 -0
- package/bash-mode/transcript-v2.ts +77 -0
- package/bash-mode/transcript.ts +18 -0
- package/bash-mode/types.ts +27 -0
- package/docs/bash-mode.md +4 -0
- package/docs/configuration.md +45 -1
- package/docs/index.md +4 -1
- package/package.json +1 -1
- package/src/config/effective.ts +122 -0
- package/src/config/settings-registry.ts +2 -0
- package/src/config/structural-presets.ts +1 -1
- package/src/extension/commands/bash-mode-actions.ts +11 -10
- package/src/extension/commands/commands.ts +8 -0
- package/src/extension/core/state.ts +0 -1
- package/src/extension/core/types.ts +2 -2
- package/src/extension/session/session-lifecycle.ts +21 -0
- package/src/extension/skills/skill-templates.ts +12 -3
- package/src/extension/ui/custom-editor.ts +4 -0
- package/src/extension/ui/status-line-renderers.ts +12 -9
- package/src/extension/welcome/welcome-integration.ts +23 -1
- package/src/motion/catalog.ts +4 -0
- package/src/motion/frames.ts +19 -9
- package/src/motion/scheduler.ts +7 -0
- package/src/render/layout.ts +137 -0
- package/src/render/motion-candidates.ts +52 -0
- package/src/render/motion-rail.ts +99 -0
- package/src/render/paint.ts +64 -0
- package/src/render/v2-adapter.ts +70 -0
- package/src/render/v2-entry.ts +132 -0
- package/src/segments/pipeline.ts +164 -0
- package/src/segments/types.ts +36 -0
- package/src/segments/usage.ts +42 -3
- package/src/signal/controller.ts +37 -21
- package/src/studio/actions.ts +105 -0
- package/src/studio/advice-pane.ts +73 -0
- package/src/studio/advise/context.ts +89 -0
- package/src/studio/advise/engine.ts +117 -0
- package/src/studio/advise/prompts.ts +68 -0
- package/src/studio/component.ts +105 -0
- package/src/studio/deepwiki/cache.ts +74 -0
- package/src/studio/deepwiki/client.ts +73 -0
- package/src/studio/deepwiki/extract.ts +66 -0
- package/src/studio/inspect.ts +38 -0
- package/src/studio/list.ts +48 -0
- package/src/studio/open.ts +44 -0
- package/src/studio/state.ts +100 -0
- package/src/studio/types.ts +30 -0
- package/src/welcome/banner.ts +6 -0
- package/src/welcome/index.ts +7 -0
- package/src/welcome/overlay.ts +6 -0
- package/src/welcome/renderer.ts +6 -39
- package/src/welcome/types.ts +6 -0
- package/src/welcome/welcome-art.ts +148 -0
- package/src/working-vibes/frames.ts +25 -0
- package/src/working-vibes/index.ts +2 -0
- package/src/working-vibes/storage.ts +11 -0
- package/bash-mode/shell-session.ts +0 -294
- package/src/signal/render.ts +0 -221
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Pure layout for the v2 powerline. No fs, no git, no theme reload.
|
|
2
|
+
// ponytail: this is a refactor target. The legacy getResponsiveLayout stays
|
|
3
|
+
// in place until U12 cutover; the v2 entry point is the only public surface
|
|
4
|
+
// for the new pipeline. Width classes follow the existing renderer
|
|
5
|
+
// (status-line-renderers.ts): small <60, medium <120, wide otherwise.
|
|
6
|
+
|
|
7
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
export type WidthClass = "small" | "medium" | "wide";
|
|
10
|
+
|
|
11
|
+
export interface LayoutSegment {
|
|
12
|
+
id: string;
|
|
13
|
+
text: string;
|
|
14
|
+
priority: number;
|
|
15
|
+
/**
|
|
16
|
+
* Optional visual height (rows). When > 1 the segment's `text` is split
|
|
17
|
+
* on `\n` and rendered across that many rows; other segments on the same
|
|
18
|
+
* lane pad empty rows below their content. The lane's rowCount is the
|
|
19
|
+
* max height across its rendered segments, so a tall rail creates a
|
|
20
|
+
* 3-row block beneath the rest of the line without disturbing their
|
|
21
|
+
* own row(s).
|
|
22
|
+
*/
|
|
23
|
+
height?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LayoutConfig {
|
|
27
|
+
primary: string[];
|
|
28
|
+
secondary: string[];
|
|
29
|
+
separator: string;
|
|
30
|
+
maxWidth: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LayoutResult {
|
|
34
|
+
primary: LayoutSegment[];
|
|
35
|
+
secondary: LayoutSegment[];
|
|
36
|
+
dropped: LayoutSegment[];
|
|
37
|
+
widthClass: WidthClass;
|
|
38
|
+
/** Max visual row count across rendered primary segments (>=1). */
|
|
39
|
+
primaryRowCount: number;
|
|
40
|
+
/** Max visual row count across rendered secondary segments (>=1). */
|
|
41
|
+
secondaryRowCount: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function widthClassFor(width: number): WidthClass {
|
|
45
|
+
if (width < 60) return "small";
|
|
46
|
+
if (width < 120) return "medium";
|
|
47
|
+
return "wide";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function segmentLineWidth(seg: LayoutSegment): number {
|
|
51
|
+
let max = 0;
|
|
52
|
+
for (const line of seg.text.split("\n")) {
|
|
53
|
+
const w = visibleWidth(line);
|
|
54
|
+
if (w > max) max = w;
|
|
55
|
+
}
|
|
56
|
+
return max;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function segmentHeight(seg: LayoutSegment): number {
|
|
60
|
+
if (seg.height && seg.height > 1) return seg.height;
|
|
61
|
+
// ponytail: derive from text so a 3-line rail without an explicit
|
|
62
|
+
// height still renders as 3 rows. Trailing empty lines are noise from
|
|
63
|
+
// upstream trims — don't promote them to extra rows.
|
|
64
|
+
const nonEmpty = seg.text.split("\n").filter((line) => line.length > 0);
|
|
65
|
+
return Math.max(1, nonEmpty.length);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function laneFor(seg: LayoutSegment, config: LayoutConfig): "primary" | "secondary" | null {
|
|
69
|
+
if (config.primary.includes(seg.id)) return "primary";
|
|
70
|
+
if (config.secondary.includes(seg.id)) return "secondary";
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function computeLaneLayout(
|
|
75
|
+
segments: LayoutSegment[],
|
|
76
|
+
width: number,
|
|
77
|
+
config: LayoutConfig,
|
|
78
|
+
): LayoutResult {
|
|
79
|
+
const cap = Math.max(0, Math.min(width, config.maxWidth));
|
|
80
|
+
const separatorWidth = visibleWidth(config.separator);
|
|
81
|
+
const primary: LayoutSegment[] = [];
|
|
82
|
+
const secondary: LayoutSegment[] = [];
|
|
83
|
+
const dropped: LayoutSegment[] = [];
|
|
84
|
+
// Sort by priority desc once; then assign by lane membership and width.
|
|
85
|
+
const sorted = [...segments].sort((a, b) => b.priority - a.priority);
|
|
86
|
+
let primaryUsed = 0;
|
|
87
|
+
let secondaryUsed = 0;
|
|
88
|
+
for (const seg of sorted) {
|
|
89
|
+
const lane = laneFor(seg, config);
|
|
90
|
+
if (lane === null) {
|
|
91
|
+
dropped.push(seg);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (segmentLineWidth(seg) === 0) {
|
|
95
|
+
dropped.push(seg);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const w = segmentLineWidth(seg);
|
|
99
|
+
// Each lane owns its own budget: a primary segment must not push a
|
|
100
|
+
// secondary segment off the line. Separator width is charged only when
|
|
101
|
+
// a segment follows another in the same lane.
|
|
102
|
+
const used = lane === "primary" ? primaryUsed : secondaryUsed;
|
|
103
|
+
const addedWidth = used === 0 ? w : separatorWidth + w;
|
|
104
|
+
if (used + addedWidth > cap) {
|
|
105
|
+
dropped.push(seg);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
(lane === "primary" ? primary : secondary).push(seg);
|
|
109
|
+
if (lane === "primary") primaryUsed += addedWidth;
|
|
110
|
+
else secondaryUsed += addedWidth;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Honor the original primary/secondary order in the config so callers
|
|
114
|
+
// expect model->context->git->queue rather than priority-sorted.
|
|
115
|
+
const primaryOrder = new Map(config.primary.map((id, i) => [id, i]));
|
|
116
|
+
primary.sort((a, b) => (primaryOrder.get(a.id) ?? 0) - (primaryOrder.get(b.id) ?? 0));
|
|
117
|
+
const secondaryOrder = new Map(config.secondary.map((id, i) => [id, i]));
|
|
118
|
+
secondary.sort((a, b) => (secondaryOrder.get(a.id) ?? 0) - (secondaryOrder.get(b.id) ?? 0));
|
|
119
|
+
|
|
120
|
+
const primaryRowCount = primary.reduce(
|
|
121
|
+
(m, s) => Math.max(m, segmentHeight(s)),
|
|
122
|
+
1,
|
|
123
|
+
);
|
|
124
|
+
const secondaryRowCount = secondary.reduce(
|
|
125
|
+
(m, s) => Math.max(m, segmentHeight(s)),
|
|
126
|
+
1,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
primary,
|
|
131
|
+
secondary,
|
|
132
|
+
dropped,
|
|
133
|
+
widthClass: widthClassFor(width),
|
|
134
|
+
primaryRowCount,
|
|
135
|
+
secondaryRowCount,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* motion-candidates — tick-driven rail renderers (the autoresearch lab).
|
|
3
|
+
* Only `lanternSigil` is wired as the active streaming rail; the rest
|
|
4
|
+
* were evaluated and discarded (see `.auto/log.jsonl`).
|
|
5
|
+
*
|
|
6
|
+
* Each returns a single 12-col line, or a 3-line string when it spans
|
|
7
|
+
* multiple rows (the layout's `height` + paint's row loop honour it).
|
|
8
|
+
* Joep: "denk is aan wat OMP als intro heeft maar dan anders" — a
|
|
9
|
+
* centered, breathing lantern sigil is the anders-than-OMP step.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const W = 12;
|
|
13
|
+
|
|
14
|
+
/** lantern sigil — 3-row animated identity, the OMP-intro step-up.
|
|
15
|
+
* Centered `#`-block lantern, sways and breathes. No braille, no shade
|
|
16
|
+
* blocks, no font assumptions — works on every terminal, every font.
|
|
17
|
+
* " ## "
|
|
18
|
+
* " ###### "
|
|
19
|
+
* " #### "
|
|
20
|
+
* (left/right columns trimmed to 12 cols; breathing adds a denser row).
|
|
21
|
+
*/
|
|
22
|
+
const LANTERN_ROWS: readonly string[] = [
|
|
23
|
+
" ## ",
|
|
24
|
+
" ###### ",
|
|
25
|
+
" #### ",
|
|
26
|
+
];
|
|
27
|
+
const LANTERN_ROWS_BREATHE: readonly string[] = [
|
|
28
|
+
" ## ",
|
|
29
|
+
" ####### ",
|
|
30
|
+
" #### ",
|
|
31
|
+
];
|
|
32
|
+
function center(s: string, w = W): string {
|
|
33
|
+
if (s.length === w) return s;
|
|
34
|
+
if (s.length > w) return s.slice(0, w);
|
|
35
|
+
const pad = Math.floor((w - s.length) / 2);
|
|
36
|
+
return " ".repeat(pad) + s + " ".repeat(w - s.length - pad);
|
|
37
|
+
}
|
|
38
|
+
function sway(s: string, n: number): string {
|
|
39
|
+
if (n === 0) return s;
|
|
40
|
+
if (n > 0) return (" ".repeat(n) + s).slice(0, W).padEnd(W, " ");
|
|
41
|
+
return (s + " ".repeat(-n)).slice(0, W).padEnd(W, " ");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function lanternSigil(tick: number, _ascii: boolean): string[] {
|
|
45
|
+
// Pure `#`-lantern — no braille, no shade blocks, no font
|
|
46
|
+
// assumptions. Works on every terminal, every font. The lantern
|
|
47
|
+
// identity comes from the shape, not the glyph set.
|
|
48
|
+
const s = Math.round(Math.sin(tick * 0.3) * 1);
|
|
49
|
+
const inhale = (tick % 8) < 5;
|
|
50
|
+
const rows = inhale ? LANTERN_ROWS : LANTERN_ROWS_BREATHE;
|
|
51
|
+
return rows.map((r) => center(sway(r, s)));
|
|
52
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Motion rail (v2). Renders the Signal activity rail — the animated
|
|
3
|
+
* sweep/compact rail plus activity label — as a first-class layout segment.
|
|
4
|
+
* Extracted from the legacy v1 Signal renderer (src/signal/render.ts,
|
|
5
|
+
* deleted in the U12 cutover); rail semantics are unchanged: idle = flat
|
|
6
|
+
* rail, streaming = travelling head + trail, compacting = inward heads.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { sweepPosition, trailGlyph } from "../motion/index.ts";
|
|
10
|
+
import type { SignalRuntime } from "../signal/controller.ts";
|
|
11
|
+
import type { SignalSpec } from "../config/types.ts";
|
|
12
|
+
import { ansi, colorEnabled, getFgAnsiCode } from "../theme/colors.ts";
|
|
13
|
+
import { lanternSigil } from "./motion-candidates.ts";
|
|
14
|
+
|
|
15
|
+
export function renderActivity(
|
|
16
|
+
runtime: SignalRuntime,
|
|
17
|
+
spec: SignalSpec,
|
|
18
|
+
ascii = false,
|
|
19
|
+
): string {
|
|
20
|
+
const label = runtime.activity || "ready";
|
|
21
|
+
const open = spec.caps.leftOpen ?? "";
|
|
22
|
+
const close = spec.caps.leftClose ?? "";
|
|
23
|
+
const dim = getFgAnsiCode("sep");
|
|
24
|
+
const hot = getFgAnsiCode("accent");
|
|
25
|
+
const reset = colorEnabled() ? ansi.reset : "";
|
|
26
|
+
// One glyph family, directional comet: light `─` track, fixed solid
|
|
27
|
+
// head, and a short box-drawing trail ONLY behind the head. Idle is a
|
|
28
|
+
// calm flat rail (no cycling glyphs — the old shade-block cloud read
|
|
29
|
+
// muddy across three unrelated glyph families).
|
|
30
|
+
const RAIL_WIDTH = 12;
|
|
31
|
+
const track = ascii ? "-" : "─";
|
|
32
|
+
const head = ascii ? "o" : "●";
|
|
33
|
+
const railColor = runtime.active ? hot : dim;
|
|
34
|
+
let railBlock: string;
|
|
35
|
+
if (!runtime.active) {
|
|
36
|
+
railBlock = track.repeat(RAIL_WIDTH);
|
|
37
|
+
} else if (runtime.activity === "compacting") {
|
|
38
|
+
// Compact state: two heads travel inward and compress a heavy core —
|
|
39
|
+
// visually distinct from the sweep so compaction reads at a glance.
|
|
40
|
+
railBlock = renderCompactRail(runtime.tick, RAIL_WIDTH, ascii);
|
|
41
|
+
} else {
|
|
42
|
+
const pos = sweepPosition(runtime.tick, RAIL_WIDTH, true);
|
|
43
|
+
const built: string[] = [];
|
|
44
|
+
for (let i = 0; i < RAIL_WIDTH; i++) {
|
|
45
|
+
if (i === pos) built.push(head);
|
|
46
|
+
else if (i < pos) built.push(trailGlyph(Math.min(pos - i, 4), ascii));
|
|
47
|
+
else built.push(track);
|
|
48
|
+
}
|
|
49
|
+
railBlock = built.join("");
|
|
50
|
+
}
|
|
51
|
+
// Active state: 3-row lantern sigil ONLY for the streaming/working
|
|
52
|
+
// activity. Each row is wrapped individually with the lane color
|
|
53
|
+
// so rows 1+ don't inherit an empty ANSI state from row 0.
|
|
54
|
+
if (runtime.active && !ascii && runtime.activity === "streaming") {
|
|
55
|
+
const sigilRows = lanternSigil(runtime.tick, false);
|
|
56
|
+
const [r0, ...rest] = sigilRows;
|
|
57
|
+
const framed = `${spec.separators.left}${r0} ${label}${spec.separators.right}`;
|
|
58
|
+
const allRows = [framed, ...rest];
|
|
59
|
+
const colored = (row: string) => `${railColor}${row}${reset}`;
|
|
60
|
+
// open brackets on the first row, close on the last row, color on
|
|
61
|
+
// every row so the whole sigil reads as one amber block.
|
|
62
|
+
const rows = allRows.map((row, i) => {
|
|
63
|
+
const prefix = i === 0 ? open : "";
|
|
64
|
+
const suffix = i === allRows.length - 1 ? close : "";
|
|
65
|
+
return `${prefix}${colored(row)}${suffix}`;
|
|
66
|
+
});
|
|
67
|
+
railBlock = rows.join("\n");
|
|
68
|
+
return railBlock;
|
|
69
|
+
}
|
|
70
|
+
const rail = `${spec.separators.left}${railBlock}${spec.separators.right}`;
|
|
71
|
+
return `${railColor}${open}${rail}${close}${reset} ${label}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compact rail: two heads travel inward toward center, compressing the rail.
|
|
76
|
+
* Gives compaction a distinct visual signature vs the generic sweep.
|
|
77
|
+
*/
|
|
78
|
+
function renderCompactRail(
|
|
79
|
+
tick: number,
|
|
80
|
+
width: number,
|
|
81
|
+
ascii: boolean,
|
|
82
|
+
): string {
|
|
83
|
+
const half = Math.floor(width / 2);
|
|
84
|
+
// Heads oscillate from the edges toward center and back.
|
|
85
|
+
const span = half;
|
|
86
|
+
const phase = tick % (span * 2);
|
|
87
|
+
const inward = phase < span ? phase : span * 2 - phase;
|
|
88
|
+
const leftPos = inward;
|
|
89
|
+
const rightPos = width - 1 - inward;
|
|
90
|
+
const head = ascii ? "*" : "●";
|
|
91
|
+
const core = ascii ? "=" : "━";
|
|
92
|
+
const track = ascii ? "-" : "─";
|
|
93
|
+
let built = "";
|
|
94
|
+
for (let i = 0; i < width; i++) {
|
|
95
|
+
if (i === leftPos || i === rightPos) built += head;
|
|
96
|
+
else built += i > leftPos && i < rightPos ? core : track;
|
|
97
|
+
}
|
|
98
|
+
return built;
|
|
99
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Cheap paint assembly. Consumes a layout and produces terminal-ready
|
|
2
|
+
// strings. No fs, no git, no theme reload — those are upstream of layout.
|
|
3
|
+
|
|
4
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
export interface PaintedLane {
|
|
7
|
+
id: string;
|
|
8
|
+
text: string;
|
|
9
|
+
bg: string;
|
|
10
|
+
fg: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PaintedLayout {
|
|
14
|
+
primary: PaintedLane[];
|
|
15
|
+
secondary: PaintedLane[];
|
|
16
|
+
dropped: PaintedLane[];
|
|
17
|
+
widthClass: "small" | "medium" | "wide";
|
|
18
|
+
primaryRowCount?: number;
|
|
19
|
+
secondaryRowCount?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const RESET = "\x1b[0m";
|
|
23
|
+
|
|
24
|
+
export function paintLane(lane: PaintedLane): string {
|
|
25
|
+
return `${lane.bg}${lane.fg}${lane.text}${RESET}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function paintLayout(layout: PaintedLayout, separator: string): string {
|
|
29
|
+
const rows = layout.primaryRowCount ?? 1;
|
|
30
|
+
const sepPad = " ".repeat(visibleWidth(separator));
|
|
31
|
+
const out: string[] = [];
|
|
32
|
+
for (let r = 0; r < rows; r++) {
|
|
33
|
+
const rowOut: string[] = [];
|
|
34
|
+
for (let i = 0; i < layout.primary.length; i++) {
|
|
35
|
+
if (i > 0 && r === 0) rowOut.push(separator);
|
|
36
|
+
if (i > 0 && r > 0) rowOut.push(sepPad);
|
|
37
|
+
rowOut.push(paintLaneRow(layout.primary[i]!, r));
|
|
38
|
+
}
|
|
39
|
+
out.push(rowOut.join(""));
|
|
40
|
+
}
|
|
41
|
+
return out.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function paintSecondary(layout: PaintedLayout, separator: string): string {
|
|
45
|
+
const rows = layout.secondaryRowCount ?? 1;
|
|
46
|
+
const sepPad = " ".repeat(visibleWidth(separator));
|
|
47
|
+
const out: string[] = [];
|
|
48
|
+
for (let r = 0; r < rows; r++) {
|
|
49
|
+
const rowOut: string[] = [];
|
|
50
|
+
for (let i = 0; i < layout.secondary.length; i++) {
|
|
51
|
+
if (i > 0 && r === 0) rowOut.push(separator);
|
|
52
|
+
if (i > 0 && r > 0) rowOut.push(sepPad);
|
|
53
|
+
rowOut.push(paintLaneRow(layout.secondary[i]!, r));
|
|
54
|
+
}
|
|
55
|
+
out.push(rowOut.join(""));
|
|
56
|
+
}
|
|
57
|
+
return out.join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function paintLaneRow(lane: PaintedLane, row: number): string {
|
|
61
|
+
const lines = lane.text.split("\n");
|
|
62
|
+
const line = lines[row] ?? "";
|
|
63
|
+
return `${lane.bg}${lane.fg}${line}${RESET}`;
|
|
64
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// v2 render adapter: wires the pure layout + paint into a top/secondary
|
|
2
|
+
// string pair matching the shape that status-line-renderers.ts consumers
|
|
3
|
+
// (renderPowerlinePrimaryLines / renderPowerlineSecondaryLines) already
|
|
4
|
+
// expect. This is the U12 cutover entry point. v1 paths remain in place
|
|
5
|
+
// until a follow-up PR deletes them after live-golden verification.
|
|
6
|
+
//
|
|
7
|
+
// ponytail: this is a delegating adapter, not a replacement. The
|
|
8
|
+
// status-line-renderers.ts consumers stay on v1 until v2 is exercised
|
|
9
|
+
// against real presets; the adapter exists so consumers can opt in via a
|
|
10
|
+
// single import swap once that is done.
|
|
11
|
+
|
|
12
|
+
import { computeLaneLayout, type LayoutSegment, type WidthClass } from "./layout.ts";
|
|
13
|
+
import { paintLayout, paintSecondary, type PaintedLane } from "./paint.ts";
|
|
14
|
+
|
|
15
|
+
export interface V2Segment extends LayoutSegment {}
|
|
16
|
+
|
|
17
|
+
export interface V2LaneConfig {
|
|
18
|
+
primary: string[];
|
|
19
|
+
secondary: string[];
|
|
20
|
+
/** Separator between painted lanes; styled separators carry ANSI. */
|
|
21
|
+
separator?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface V2RenderResult {
|
|
25
|
+
topContent: string;
|
|
26
|
+
secondaryContent: string;
|
|
27
|
+
dropped: LayoutSegment[];
|
|
28
|
+
widthClass: WidthClass;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const SEPARATOR = " | ";
|
|
32
|
+
|
|
33
|
+
function toPainted(seg: LayoutSegment): PaintedLane {
|
|
34
|
+
return {
|
|
35
|
+
id: seg.id,
|
|
36
|
+
text: seg.text,
|
|
37
|
+
bg: "",
|
|
38
|
+
fg: "",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function renderPowerlineV2(
|
|
43
|
+
segments: V2Segment[],
|
|
44
|
+
width: number,
|
|
45
|
+
config: V2LaneConfig,
|
|
46
|
+
): V2RenderResult {
|
|
47
|
+
const separator = config.separator ?? SEPARATOR;
|
|
48
|
+
const layout = computeLaneLayout(segments, width, {
|
|
49
|
+
primary: config.primary,
|
|
50
|
+
secondary: config.secondary,
|
|
51
|
+
separator,
|
|
52
|
+
maxWidth: width,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const painted = {
|
|
56
|
+
primary: layout.primary.map(toPainted),
|
|
57
|
+
secondary: layout.secondary.map(toPainted),
|
|
58
|
+
dropped: [] as PaintedLane[],
|
|
59
|
+
widthClass: layout.widthClass,
|
|
60
|
+
primaryRowCount: layout.primaryRowCount,
|
|
61
|
+
secondaryRowCount: layout.secondaryRowCount,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
topContent: paintLayout(painted, separator),
|
|
66
|
+
secondaryContent: paintSecondary(painted, separator),
|
|
67
|
+
dropped: layout.dropped,
|
|
68
|
+
widthClass: layout.widthClass,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 status-line entry (U12 cutover). Composes the single render path:
|
|
3
|
+
* segments render through the per-segment fault-isolated `renderSegment`
|
|
4
|
+
* boundary, the motion rail renders as a first-class layout segment, and
|
|
5
|
+
* lanes flow through `computeLaneLayout` + `paintLayout`. The legacy v1
|
|
6
|
+
* three-lane renderer (src/signal/render.ts) is deleted; rail semantics
|
|
7
|
+
* live on in ./motion-rail.ts.
|
|
8
|
+
*
|
|
9
|
+
* Priority scheme preserves the v1 reading order left -> rail -> right:
|
|
10
|
+
* left segments 10_000-i, rail 5_000, right segments 1_000-i. Under width
|
|
11
|
+
* pressure computeLaneLayout drops the lowest priorities first, so the
|
|
12
|
+
* right lane yields before the rail, and the rail before the left —
|
|
13
|
+
* matching v1's "preserve model/git first, then context/queue" squeeze.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
PresetDef,
|
|
18
|
+
SegmentContext,
|
|
19
|
+
SignalSpec,
|
|
20
|
+
StatusLineLayout,
|
|
21
|
+
StatusLineSegmentId,
|
|
22
|
+
StatusLineSeparatorStyle,
|
|
23
|
+
} from "../config/types.ts";
|
|
24
|
+
import { mergeSegmentsWithCustomItems } from "../config/powerline-config.ts";
|
|
25
|
+
import { renderSegment } from "../segments/index.ts";
|
|
26
|
+
import { getContributedSignalSources } from "../extension/contrib/registry.ts";
|
|
27
|
+
import { getSeparator } from "../theme/separators.ts";
|
|
28
|
+
import { ansi, colorEnabled, getFgAnsiCode } from "../theme/colors.ts";
|
|
29
|
+
import { renderActivity } from "./motion-rail.ts";
|
|
30
|
+
import { renderPowerlineV2, type V2Segment } from "./v2-adapter.ts";
|
|
31
|
+
import type { SignalRuntime } from "../signal/controller.ts";
|
|
32
|
+
|
|
33
|
+
export interface StatusLineV2Options {
|
|
34
|
+
separatorStyle: StatusLineSeparatorStyle;
|
|
35
|
+
signal: SignalSpec;
|
|
36
|
+
ascii?: boolean;
|
|
37
|
+
layout?: StatusLineLayout | null;
|
|
38
|
+
disabledSegments?: StatusLineSegmentId[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const ANSI_CONTROL_RE =
|
|
42
|
+
/\x1B(?:\][^\x07]*(?:\u0007|\x1B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])|\x9B[0-?]*[ -/]*[@-~]|\x9D[^\x07]*(?:\u0007|\x1B\\)/g;
|
|
43
|
+
|
|
44
|
+
function contributedTexts(ctx: SegmentContext): string[] {
|
|
45
|
+
const out: string[] = [];
|
|
46
|
+
for (const src of getContributedSignalSources()) {
|
|
47
|
+
try {
|
|
48
|
+
const raw = src.render?.(ctx);
|
|
49
|
+
if (!raw || typeof raw !== "string") continue;
|
|
50
|
+
const content = raw.trim();
|
|
51
|
+
if (!content.replace(ANSI_CONTROL_RE, "").trim()) continue;
|
|
52
|
+
// ponytail: fault-isolated — a throwing or empty source never takes the line down
|
|
53
|
+
out.push(content);
|
|
54
|
+
} catch {
|
|
55
|
+
// skip failing contribution
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function styledSeparator(separator: string): string {
|
|
62
|
+
const color = getFgAnsiCode("sep");
|
|
63
|
+
const reset = colorEnabled() ? ansi.reset : "";
|
|
64
|
+
return ` ${color}${separator}${reset} `;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderStatusLineV2(
|
|
68
|
+
ctx: SegmentContext,
|
|
69
|
+
preset: PresetDef,
|
|
70
|
+
runtime: SignalRuntime,
|
|
71
|
+
width: number,
|
|
72
|
+
options: StatusLineV2Options,
|
|
73
|
+
): { topContent: string; secondaryContent: string } {
|
|
74
|
+
const merged = mergeSegmentsWithCustomItems(
|
|
75
|
+
preset,
|
|
76
|
+
ctx.effectiveCustomItems,
|
|
77
|
+
{
|
|
78
|
+
layout: options.layout ?? null,
|
|
79
|
+
disabledSegments: options.disabledSegments ?? [],
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const segments: V2Segment[] = [];
|
|
84
|
+
const primary: string[] = [];
|
|
85
|
+
|
|
86
|
+
const pushSegment = (id: StatusLineSegmentId, priority: number): void => {
|
|
87
|
+
try {
|
|
88
|
+
const rendered = renderSegment(id, ctx);
|
|
89
|
+
if (!rendered.visible || !rendered.content) return;
|
|
90
|
+
segments.push({ id, text: rendered.content, priority });
|
|
91
|
+
primary.push(id);
|
|
92
|
+
} catch {
|
|
93
|
+
// renderSegment is the established per-segment error boundary; a
|
|
94
|
+
// throwing segment never blanks the line.
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
merged.leftSegments.forEach((id, i) => pushSegment(id, 10_000 - i));
|
|
99
|
+
|
|
100
|
+
const rail = renderActivity(runtime, options.signal, options.ascii);
|
|
101
|
+
segments.push({ id: "signal", text: rail, priority: 5_000 });
|
|
102
|
+
primary.push("signal");
|
|
103
|
+
|
|
104
|
+
merged.rightSegments.forEach((id, i) => pushSegment(id, 1_000 - i));
|
|
105
|
+
contributedTexts(ctx).forEach((text, i) => {
|
|
106
|
+
const id = `contrib:${i}`;
|
|
107
|
+
segments.push({ id, text, priority: 900 - i });
|
|
108
|
+
primary.push(id);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
merged.secondarySegments.forEach((id, i) => {
|
|
112
|
+
try {
|
|
113
|
+
const rendered = renderSegment(id, ctx);
|
|
114
|
+
if (!rendered.visible || !rendered.content) return;
|
|
115
|
+
segments.push({ id, text: rendered.content, priority: 500 - i });
|
|
116
|
+
} catch {
|
|
117
|
+
// Keep secondary segments behind the same fault boundary as primary.
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const separatorGlyph = getSeparator(options.separatorStyle).left;
|
|
122
|
+
const result = renderPowerlineV2(segments, width, {
|
|
123
|
+
primary,
|
|
124
|
+
secondary: [...merged.secondarySegments],
|
|
125
|
+
separator: styledSeparator(separatorGlyph),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
topContent: result.topContent,
|
|
130
|
+
secondaryContent: result.secondaryContent,
|
|
131
|
+
};
|
|
132
|
+
}
|