@sigrun/tui 0.1.0 → 0.1.1
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 +6 -0
- package/dist/actions.d.ts +43 -2
- package/dist/api.d.ts +5 -0
- package/dist/api.js +5 -1
- package/dist/blocks/braille.d.ts +41 -0
- package/dist/blocks/braille.js +131 -0
- package/dist/blocks/chart.d.ts +20 -0
- package/dist/blocks/chart.js +397 -0
- package/dist/blocks/collection/calendar.d.ts +28 -0
- package/dist/blocks/collection/calendar.js +232 -0
- package/dist/blocks/collection/card.d.ts +36 -0
- package/dist/blocks/collection/card.js +128 -0
- package/dist/blocks/collection/collection.d.ts +9 -0
- package/dist/blocks/collection/collection.js +26 -0
- package/dist/blocks/collection/grid.d.ts +18 -0
- package/dist/blocks/collection/grid.js +72 -0
- package/dist/blocks/collection/lanes.d.ts +32 -0
- package/dist/blocks/collection/lanes.js +320 -0
- package/dist/blocks/collection/list.d.ts +30 -0
- package/dist/blocks/collection/list.js +273 -0
- package/dist/blocks/collection/move.d.ts +67 -0
- package/dist/blocks/collection/move.js +96 -0
- package/dist/blocks/collection/shared.d.ts +87 -0
- package/dist/blocks/collection/shared.js +177 -0
- package/dist/blocks/collection/table.d.ts +26 -0
- package/dist/blocks/collection/table.js +221 -0
- package/dist/blocks/content.d.ts +48 -0
- package/dist/blocks/content.js +253 -0
- package/dist/blocks/context.d.ts +96 -0
- package/dist/blocks/context.js +65 -0
- package/dist/blocks/diff.d.ts +21 -0
- package/dist/blocks/diff.js +25 -0
- package/dist/blocks/ext-transport.d.ts +70 -0
- package/dist/blocks/ext-transport.js +165 -0
- package/dist/blocks/form/choice.d.ts +54 -0
- package/dist/blocks/form/choice.js +175 -0
- package/dist/blocks/form/fields.d.ts +56 -0
- package/dist/blocks/form/fields.js +136 -0
- package/dist/blocks/form/file.d.ts +61 -0
- package/dist/blocks/form/file.js +121 -0
- package/dist/blocks/form/form.d.ts +53 -0
- package/dist/blocks/form/form.js +703 -0
- package/dist/blocks/form/list.d.ts +37 -0
- package/dist/blocks/form/list.js +70 -0
- package/dist/blocks/form/values.d.ts +20 -0
- package/dist/blocks/form/values.js +104 -0
- package/dist/blocks/icons.d.ts +14 -0
- package/dist/blocks/icons.js +74 -0
- package/dist/blocks/image.d.ts +39 -0
- package/dist/blocks/image.js +82 -0
- package/dist/blocks/layout.d.ts +31 -0
- package/dist/blocks/layout.js +155 -0
- package/dist/blocks/log.d.ts +24 -0
- package/dist/blocks/log.js +111 -0
- package/dist/blocks/meter.d.ts +16 -0
- package/dist/blocks/meter.js +50 -0
- package/dist/blocks/page.d.ts +54 -0
- package/dist/blocks/page.js +355 -0
- package/dist/blocks/render.d.ts +31 -0
- package/dist/blocks/render.js +145 -0
- package/dist/blocks/surface-view.d.ts +33 -0
- package/dist/blocks/surface-view.js +86 -0
- package/dist/blocks/surface.d.ts +75 -0
- package/dist/blocks/surface.js +468 -0
- package/dist/blocks/tones.d.ts +23 -0
- package/dist/blocks/tones.js +63 -0
- package/dist/blocks/treeview.d.ts +22 -0
- package/dist/blocks/treeview.js +121 -0
- package/dist/blocks/width.d.ts +32 -0
- package/dist/blocks/width.js +70 -0
- package/dist/keymaps/emacs.js +39 -0
- package/dist/keymaps/standard.js +45 -1
- package/dist/keymaps/vim.js +37 -0
- package/dist/launch.js +13 -0
- package/dist/sse.d.ts +8 -0
- package/dist/sse.js +27 -0
- package/dist/theme.d.ts +2 -2
- package/dist/theme.js +16 -2
- package/dist/ui/app.d.ts +36 -0
- package/dist/ui/app.js +84 -0
- package/dist/ui/interactions.js +2 -7
- package/dist/ui/sidebar.d.ts +25 -2
- package/dist/ui/sidebar.js +64 -0
- package/package.json +3 -3
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal width for block rendering (UI-blocks U2 Task 1): the width
|
|
3
|
+
* classes (spec §12.1), the `s`/`m`/`l` token scale (spec §5.2), and the
|
|
4
|
+
* clamp/pad helpers every block renderer funnels through.
|
|
5
|
+
*
|
|
6
|
+
* The display-width measure is pi-tui's `visibleWidth` (grapheme-aware,
|
|
7
|
+
* wcwidth-style, ANSI-safe) — re-exported here so block code has ONE
|
|
8
|
+
* width import site and the Review-Focus-2 invariant ("every rendered
|
|
9
|
+
* line's display width ≤ the pane") is enforced in one place:
|
|
10
|
+
* {@link truncate}.
|
|
11
|
+
*/
|
|
12
|
+
import type { Size } from "@sigrun/shared/blocks";
|
|
13
|
+
/** The container width classes (spec §12.1, TUI column). */
|
|
14
|
+
export type WidthClass = "compact" | "regular" | "wide";
|
|
15
|
+
/** The compact/regular/wide boundary: cells (compact < 80 ≤ regular < 140 ≤ wide). */
|
|
16
|
+
export declare function widthClassOf(cells: number): WidthClass;
|
|
17
|
+
/** Width tokens in cells (spec §5.2: the TUI maps s/m/l to 30/45/60). */
|
|
18
|
+
export declare const SIZE_CELLS: Readonly<Record<Size, number>>;
|
|
19
|
+
/**
|
|
20
|
+
* Clamp one rendered line to `width` cells with an ellipsis (Review
|
|
21
|
+
* Focus 2): ANSI-aware, wide-character-aware, never a mid-sequence cut.
|
|
22
|
+
* Empty lines pass through.
|
|
23
|
+
*/
|
|
24
|
+
export declare function truncate(line: string, width: number): string;
|
|
25
|
+
/** Pad a line to `width` cells by its DISPLAY width (for column joins). */
|
|
26
|
+
export declare function pad(line: string, width: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* Fit a run of source lines into `width` cells: short runs stay one
|
|
29
|
+
* line (truncated); longer word-wraps fold at spaces, each folded line
|
|
30
|
+
* truncated. Used for plain-text values (markdown wraps itself).
|
|
31
|
+
*/
|
|
32
|
+
export declare function wrapPlain(text: string, width: number): string[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal width for block rendering (UI-blocks U2 Task 1): the width
|
|
3
|
+
* classes (spec §12.1), the `s`/`m`/`l` token scale (spec §5.2), and the
|
|
4
|
+
* clamp/pad helpers every block renderer funnels through.
|
|
5
|
+
*
|
|
6
|
+
* The display-width measure is pi-tui's `visibleWidth` (grapheme-aware,
|
|
7
|
+
* wcwidth-style, ANSI-safe) — re-exported here so block code has ONE
|
|
8
|
+
* width import site and the Review-Focus-2 invariant ("every rendered
|
|
9
|
+
* line's display width ≤ the pane") is enforced in one place:
|
|
10
|
+
* {@link truncate}.
|
|
11
|
+
*/
|
|
12
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
13
|
+
/** The compact/regular/wide boundary: cells (compact < 80 ≤ regular < 140 ≤ wide). */
|
|
14
|
+
export function widthClassOf(cells) {
|
|
15
|
+
if (cells < 80)
|
|
16
|
+
return "compact";
|
|
17
|
+
if (cells < 140)
|
|
18
|
+
return "regular";
|
|
19
|
+
return "wide";
|
|
20
|
+
}
|
|
21
|
+
/** Width tokens in cells (spec §5.2: the TUI maps s/m/l to 30/45/60). */
|
|
22
|
+
export const SIZE_CELLS = {
|
|
23
|
+
s: 30,
|
|
24
|
+
m: 45,
|
|
25
|
+
l: 60,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Clamp one rendered line to `width` cells with an ellipsis (Review
|
|
29
|
+
* Focus 2): ANSI-aware, wide-character-aware, never a mid-sequence cut.
|
|
30
|
+
* Empty lines pass through.
|
|
31
|
+
*/
|
|
32
|
+
export function truncate(line, width) {
|
|
33
|
+
if (line === "")
|
|
34
|
+
return "";
|
|
35
|
+
return truncateToWidth(line, width, "…");
|
|
36
|
+
}
|
|
37
|
+
/** Pad a line to `width` cells by its DISPLAY width (for column joins). */
|
|
38
|
+
export function pad(line, width) {
|
|
39
|
+
const rest = width - visibleWidth(line);
|
|
40
|
+
return rest > 0 ? line + " ".repeat(rest) : line;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Fit a run of source lines into `width` cells: short runs stay one
|
|
44
|
+
* line (truncated); longer word-wraps fold at spaces, each folded line
|
|
45
|
+
* truncated. Used for plain-text values (markdown wraps itself).
|
|
46
|
+
*/
|
|
47
|
+
export function wrapPlain(text, width) {
|
|
48
|
+
if (width <= 0)
|
|
49
|
+
return [];
|
|
50
|
+
const words = text.split(/\s+/).filter((w) => w !== "");
|
|
51
|
+
if (words.length === 0)
|
|
52
|
+
return [""];
|
|
53
|
+
const lines = [];
|
|
54
|
+
let current = "";
|
|
55
|
+
for (const word of words) {
|
|
56
|
+
if (current === "") {
|
|
57
|
+
current = word;
|
|
58
|
+
}
|
|
59
|
+
else if (visibleWidth(current) + 1 + visibleWidth(word) <= width) {
|
|
60
|
+
current += ` ${word}`;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
lines.push(current);
|
|
64
|
+
current = word;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (current !== "")
|
|
68
|
+
lines.push(current);
|
|
69
|
+
return lines.map((line) => truncate(line, width));
|
|
70
|
+
}
|
package/dist/keymaps/emacs.js
CHANGED
|
@@ -34,6 +34,14 @@ const BINDINGS = [
|
|
|
34
34
|
{ keys: "C-o", action: "answer interaction" },
|
|
35
35
|
{ keys: "C-l", action: "redraw" },
|
|
36
36
|
{ keys: "C-h", action: "key reference" },
|
|
37
|
+
{ keys: "M-m", action: "pick up (block surface)" },
|
|
38
|
+
{ keys: "enter", action: "drop (block surface)" },
|
|
39
|
+
{ keys: "C-g", action: "cancel move (block surface)" },
|
|
40
|
+
{ keys: "space", action: "select (block surface)" },
|
|
41
|
+
{ keys: ".", action: "action menu (block surface)" },
|
|
42
|
+
{ keys: "x", action: "check (block surface)" },
|
|
43
|
+
{ keys: "[ / ]", action: "previous / next range (calendar)" },
|
|
44
|
+
{ keys: "t", action: "today (calendar)" },
|
|
37
45
|
];
|
|
38
46
|
/** Match `data` against any reserved chord (they always pass). */
|
|
39
47
|
function isReserved(data) {
|
|
@@ -71,6 +79,30 @@ export function createEmacsKeymap() {
|
|
|
71
79
|
}
|
|
72
80
|
return undefined;
|
|
73
81
|
};
|
|
82
|
+
/** Block-surface keys (spec §12.4; emacs picks up with M-m). C-g is
|
|
83
|
+
* the cancel key — it cancels a move on a surface BEFORE the global
|
|
84
|
+
* C-g (cancel-run / focus composer) below sees it. */
|
|
85
|
+
const surface = (data) => {
|
|
86
|
+
if (data === "\x1bm")
|
|
87
|
+
return { type: "blocks.pickUp" }; // M-m
|
|
88
|
+
if (matchesKey(data, Key.enter) || data === "\r")
|
|
89
|
+
return { type: "blocks.drop" };
|
|
90
|
+
if (data === "\x07")
|
|
91
|
+
return { type: "blocks.cancelMove" }; // C-g
|
|
92
|
+
if (data === " ")
|
|
93
|
+
return { type: "blocks.select" };
|
|
94
|
+
if (data === ".")
|
|
95
|
+
return { type: "blocks.actionMenu" };
|
|
96
|
+
if (data === "x")
|
|
97
|
+
return { type: "blocks.check" };
|
|
98
|
+
if (data === "[")
|
|
99
|
+
return { type: "blocks.rangePrev" };
|
|
100
|
+
if (data === "]")
|
|
101
|
+
return { type: "blocks.rangeNext" };
|
|
102
|
+
if (data === "t")
|
|
103
|
+
return { type: "blocks.today" };
|
|
104
|
+
return undefined;
|
|
105
|
+
};
|
|
74
106
|
return {
|
|
75
107
|
name: "emacs",
|
|
76
108
|
modeLabel: () => undefined,
|
|
@@ -89,6 +121,13 @@ export function createEmacsKeymap() {
|
|
|
89
121
|
const chorded = chord(data);
|
|
90
122
|
if (chorded !== undefined)
|
|
91
123
|
return chorded;
|
|
124
|
+
// A block surface owns the §12.4 keys (its C-g is cancel-move,
|
|
125
|
+
// which must win over the global C-g cancel-run).
|
|
126
|
+
if (ctx.focus === "surface") {
|
|
127
|
+
const surfaceResult = surface(data);
|
|
128
|
+
if (surfaceResult !== undefined)
|
|
129
|
+
return surfaceResult;
|
|
130
|
+
}
|
|
92
131
|
if (data === "\x1bx")
|
|
93
132
|
return { type: "picker", kind: "command" };
|
|
94
133
|
if (data === "\x13")
|
package/dist/keymaps/standard.js
CHANGED
|
@@ -35,6 +35,14 @@ const BINDINGS = [
|
|
|
35
35
|
{ keys: "/", action: "search transcript" },
|
|
36
36
|
{ keys: "n / N", action: "next / previous match" },
|
|
37
37
|
{ keys: "?", action: "help" },
|
|
38
|
+
{ keys: "m", action: "pick up (block surface)" },
|
|
39
|
+
{ keys: "enter", action: "drop (block surface)" },
|
|
40
|
+
{ keys: "esc", action: "cancel move (block surface)" },
|
|
41
|
+
{ keys: "space", action: "select (block surface)" },
|
|
42
|
+
{ keys: ".", action: "action menu (block surface)" },
|
|
43
|
+
{ keys: "x", action: "check (block surface)" },
|
|
44
|
+
{ keys: "[ / ]", action: "previous / next range (calendar)" },
|
|
45
|
+
{ keys: "t", action: "today (calendar)" },
|
|
38
46
|
];
|
|
39
47
|
/** Match `data` against any reserved chord (they always pass). */
|
|
40
48
|
function isReserved(data) {
|
|
@@ -64,7 +72,11 @@ export function createStandardKeymap() {
|
|
|
64
72
|
return "pass";
|
|
65
73
|
if (ctx.busy)
|
|
66
74
|
return { type: "cancel-run" };
|
|
67
|
-
|
|
75
|
+
// A block surface owns Esc: it cancels a pick-up
|
|
76
|
+
// (blocks.cancelMove) — the surface branch below handles it.
|
|
77
|
+
if (ctx.focus !== "surface")
|
|
78
|
+
return { type: "focus", target: "composer" };
|
|
79
|
+
return undefined;
|
|
68
80
|
}
|
|
69
81
|
return undefined;
|
|
70
82
|
};
|
|
@@ -99,6 +111,27 @@ export function createStandardKeymap() {
|
|
|
99
111
|
return { type: "help" };
|
|
100
112
|
return undefined;
|
|
101
113
|
};
|
|
114
|
+
const surface = (data) => {
|
|
115
|
+
// Block-surface keys (spec §12.4): routed ONLY here; the app
|
|
116
|
+
// forwards the action to the focused block component.
|
|
117
|
+
if (data === "m")
|
|
118
|
+
return { type: "blocks.pickUp" };
|
|
119
|
+
if (matchesKey(data, Key.enter))
|
|
120
|
+
return { type: "blocks.drop" };
|
|
121
|
+
if (data === " ")
|
|
122
|
+
return { type: "blocks.select" };
|
|
123
|
+
if (data === ".")
|
|
124
|
+
return { type: "blocks.actionMenu" };
|
|
125
|
+
if (data === "x")
|
|
126
|
+
return { type: "blocks.check" };
|
|
127
|
+
if (data === "[")
|
|
128
|
+
return { type: "blocks.rangePrev" };
|
|
129
|
+
if (data === "]")
|
|
130
|
+
return { type: "blocks.rangeNext" };
|
|
131
|
+
if (data === "t")
|
|
132
|
+
return { type: "blocks.today" };
|
|
133
|
+
return undefined;
|
|
134
|
+
};
|
|
102
135
|
return {
|
|
103
136
|
name: "standard",
|
|
104
137
|
modeLabel: () => undefined,
|
|
@@ -113,6 +146,17 @@ export function createStandardKeymap() {
|
|
|
113
146
|
const globalResult = global(data, ctx);
|
|
114
147
|
if (globalResult !== undefined)
|
|
115
148
|
return globalResult;
|
|
149
|
+
if (ctx.focus === "surface") {
|
|
150
|
+
const surfaceResult = surface(data);
|
|
151
|
+
if (surfaceResult !== undefined)
|
|
152
|
+
return surfaceResult;
|
|
153
|
+
// Esc on a surface: cancel a pick-up (§12.4). The host treats
|
|
154
|
+
// a cancel with NO pick-up in flight as focus-composer (the
|
|
155
|
+
// emacs C-g ordering rule, mirrored here).
|
|
156
|
+
if (matchesKey(data, Key.escape))
|
|
157
|
+
return { type: "blocks.cancelMove" };
|
|
158
|
+
return "pass";
|
|
159
|
+
}
|
|
116
160
|
if (ctx.focus !== "composer") {
|
|
117
161
|
const navResult = outsideComposer(data);
|
|
118
162
|
if (navResult !== undefined)
|
package/dist/keymaps/vim.js
CHANGED
|
@@ -51,6 +51,14 @@ const BINDINGS = [
|
|
|
51
51
|
{ keys: "ctrl+c", action: "abort run (busy)" },
|
|
52
52
|
{ keys: "g?", action: "key reference" },
|
|
53
53
|
{ keys: "ZZ / :q", action: "quit" },
|
|
54
|
+
{ keys: "m", action: "pick up (block surface)" },
|
|
55
|
+
{ keys: "enter", action: "drop (block surface)" },
|
|
56
|
+
{ keys: "esc", action: "cancel move (block surface)" },
|
|
57
|
+
{ keys: "space", action: "select (block surface)" },
|
|
58
|
+
{ keys: ".", action: "action menu (block surface)" },
|
|
59
|
+
{ keys: "x", action: "check (block surface)" },
|
|
60
|
+
{ keys: "[ / ]", action: "previous / next range (calendar)" },
|
|
61
|
+
{ keys: "t", action: "today (calendar)" },
|
|
54
62
|
];
|
|
55
63
|
function isReserved(data) {
|
|
56
64
|
return RESERVED_KEYS.some((id) => matchesKey(data, id));
|
|
@@ -117,11 +125,40 @@ export function createVimKeymap(options) {
|
|
|
117
125
|
return { type: "answer-interaction" };
|
|
118
126
|
return "pass";
|
|
119
127
|
};
|
|
128
|
+
/** Block-surface keys (spec §12.4): NORMAL mode over a `surface`
|
|
129
|
+
* focus. The surface wins over transcript/list bindings for the
|
|
130
|
+
* shared keys (o/x/etc). */
|
|
131
|
+
const surfaceNormal = (data) => {
|
|
132
|
+
if (data === "m")
|
|
133
|
+
return { type: "blocks.pickUp" };
|
|
134
|
+
if (matchesKey(data, Key.enter))
|
|
135
|
+
return { type: "blocks.drop" };
|
|
136
|
+
if (matchesKey(data, Key.escape))
|
|
137
|
+
return { type: "blocks.cancelMove" };
|
|
138
|
+
if (data === " ")
|
|
139
|
+
return { type: "blocks.select" };
|
|
140
|
+
if (data === ".")
|
|
141
|
+
return { type: "blocks.actionMenu" };
|
|
142
|
+
if (data === "x")
|
|
143
|
+
return { type: "blocks.check" };
|
|
144
|
+
if (data === "[")
|
|
145
|
+
return { type: "blocks.rangePrev" };
|
|
146
|
+
if (data === "]")
|
|
147
|
+
return { type: "blocks.rangeNext" };
|
|
148
|
+
if (data === "t")
|
|
149
|
+
return { type: "blocks.today" };
|
|
150
|
+
return undefined;
|
|
151
|
+
};
|
|
120
152
|
const normal = (data, ctx) => {
|
|
121
153
|
// ctrl+b / ctrl+f are NOT bound in vim normal — reserved chords pass.
|
|
122
154
|
const chorded = chord(data);
|
|
123
155
|
if (chorded !== undefined)
|
|
124
156
|
return chorded;
|
|
157
|
+
if (ctx.focus === "surface") {
|
|
158
|
+
const surfaceResult = surfaceNormal(data);
|
|
159
|
+
if (surfaceResult !== undefined)
|
|
160
|
+
return surfaceResult;
|
|
161
|
+
}
|
|
125
162
|
if (data === "y")
|
|
126
163
|
return { type: "copy" }; // yy is two copies — same action
|
|
127
164
|
if (data === "j")
|
package/dist/launch.js
CHANGED
|
@@ -204,6 +204,9 @@ export async function launchTuiShell(options) {
|
|
|
204
204
|
]);
|
|
205
205
|
tui.setLayoutRoot(root);
|
|
206
206
|
await app.refresh();
|
|
207
|
+
// U3 Task 5: the sidebar's Views group loads with the conversations
|
|
208
|
+
// (one /v1/extensions GET per boot; failures fail quiet).
|
|
209
|
+
void app.sidebar.refreshViews().catch(() => { });
|
|
207
210
|
// The host events subscription (core hardening W3 Task 4, spec §7.4):
|
|
208
211
|
// ONE stream per process replaces the poll timers — `conversation.*`
|
|
209
212
|
// refetches the sidebar listing, `interaction.*` kicks the prompter,
|
|
@@ -224,8 +227,18 @@ export async function launchTuiShell(options) {
|
|
|
224
227
|
else if (event.type === "ambient.changed") {
|
|
225
228
|
app.ambient.kick(event.conversationId);
|
|
226
229
|
}
|
|
230
|
+
else if (event.type === "surface.changed") {
|
|
231
|
+
// U3 Task 5: fan the surface event out to the open
|
|
232
|
+
// view's transport (the hub matches slug+id+k).
|
|
233
|
+
app.dispatchSurfaceChanged(event);
|
|
234
|
+
}
|
|
227
235
|
},
|
|
228
236
|
onError: (message) => app.pane.showNotice(message),
|
|
237
|
+
// U3 Task 5 (§7.3): the hello frame's streamId is the watcher
|
|
238
|
+
// entry's stream — open ext-view transports send it as
|
|
239
|
+
// X-Sigrun-Stream. A reconnect's new id re-registers on the
|
|
240
|
+
// next fetch.
|
|
241
|
+
onHello: (streamId) => app.setEventsStreamId(streamId),
|
|
229
242
|
}, {
|
|
230
243
|
baseUrl: options.baseUrl,
|
|
231
244
|
...(options.user === undefined ? {} : { user: options.user }),
|
package/dist/sse.d.ts
CHANGED
|
@@ -92,6 +92,14 @@ export declare function watchConversation(conversationId: number, handlers: Watc
|
|
|
92
92
|
export interface EventsHandlers {
|
|
93
93
|
/** A host event arrived (already JSON-parsed). */
|
|
94
94
|
readonly onEvent: (event: HostEventWire) => void;
|
|
95
|
+
/**
|
|
96
|
+
* The hello frame's streamId (UI-blocks U3 Task 4, spec §7.3's W3
|
|
97
|
+
* amendment): fired on EVERY (re)connect — a reconnect's hello
|
|
98
|
+
* carries a NEW id, and the channel re-fetches every visible surface
|
|
99
|
+
* with it (re-registering the watcher entries). Surface fetches send
|
|
100
|
+
* the current id as `X-Sigrun-Stream`.
|
|
101
|
+
*/
|
|
102
|
+
readonly onHello?: (streamId: string) => void;
|
|
95
103
|
/** A failure: malformed frame (stays open) or transport error (reconnects). */
|
|
96
104
|
readonly onError?: (message: string) => void;
|
|
97
105
|
/** The underlying connection (re)opened with an OK response. */
|
package/dist/sse.js
CHANGED
|
@@ -326,6 +326,33 @@ export function watchEvents(handlers, options) {
|
|
|
326
326
|
const parser = createFrameParser((frame) => {
|
|
327
327
|
if (closed)
|
|
328
328
|
return;
|
|
329
|
+
// The hello frame (U3 Task 4) is connection-local: it names
|
|
330
|
+
// THIS stream for the watcher registry — surface fetches send
|
|
331
|
+
// the id as `X-Sigrun-Stream`; a NEW id (reconnect) re-fires
|
|
332
|
+
// so the channel re-registers. It never reaches onEvent (the
|
|
333
|
+
// launch wiring's switch would ignore it anyway — event types
|
|
334
|
+
// are an open set). Parsed loosely: the hello payload carries
|
|
335
|
+
// `streamId`, not a `type` member.
|
|
336
|
+
if (frame.event === "hello") {
|
|
337
|
+
let parsed;
|
|
338
|
+
try {
|
|
339
|
+
parsed = JSON.parse(frame.data);
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
handlers.onError?.("malformed hello frame (not JSON)");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
// SAFETY: the JSON.parse output is unknown; the read below
|
|
346
|
+
// narrows `streamId` by typeof before use.
|
|
347
|
+
const streamId = parsed.streamId;
|
|
348
|
+
if (typeof streamId === "string") {
|
|
349
|
+
handlers.onHello?.(streamId);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
handlers.onError?.("malformed hello frame (no streamId)");
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
329
356
|
const event = parseEvent(frame.data);
|
|
330
357
|
if (event === undefined) {
|
|
331
358
|
handlers.onError?.(`malformed ${frame.event === "" ? "message" : frame.event} frame (not an event object)`);
|
package/dist/theme.d.ts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { type TerminalColorMode } from "@earendil-works/pi-tui";
|
|
20
20
|
import { type AccentName, type ModeName, type PaletteName, type PaletteTokens } from "@sigrun/shared/themes";
|
|
21
|
-
/** A palette token,
|
|
22
|
-
export type TuiToken = keyof PaletteTokens | "accent";
|
|
21
|
+
/** A palette token, the accent colour, or a state colour (warn/bad). */
|
|
22
|
+
export type TuiToken = keyof PaletteTokens | "accent" | "warn" | "bad";
|
|
23
23
|
/**
|
|
24
24
|
* Terminal styling surface: token-named fg/bg plus the plain attribute
|
|
25
25
|
* helpers. `fg`/`bg` return string wrappers so call sites compose
|
package/dist/theme.js
CHANGED
|
@@ -43,10 +43,22 @@ function rgbToHex(r, g, b) {
|
|
|
43
43
|
const two = (n) => n.toString(16).padStart(2, "0");
|
|
44
44
|
return `#${two(r)}${two(g)}${two(b)}`;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* The state colours (warnings + errors), defined per mode like the
|
|
48
|
+
* webui's `--warn`/`--danger` (webui/src/app.css — they are NOT palette
|
|
49
|
+
* members in either channel). The TUI's tone table (blocks/tones.ts)
|
|
50
|
+
* paints `warn`/`bad` tones with them.
|
|
51
|
+
*/
|
|
52
|
+
const STATE_COLORS = {
|
|
53
|
+
warn: { dark: "#d9a35a", light: "#8a6116" },
|
|
54
|
+
bad: { dark: "#e0707a", light: "#b3363f" },
|
|
55
|
+
};
|
|
46
56
|
/** Resolve a token to a parseable colour for the palette+mode. */
|
|
47
|
-
function resolveToken(tokens, accentHex, token) {
|
|
57
|
+
function resolveToken(tokens, accentHex, mode, token) {
|
|
48
58
|
if (token === "accent")
|
|
49
59
|
return accentHex;
|
|
60
|
+
if (token === "warn" || token === "bad")
|
|
61
|
+
return STATE_COLORS[token][mode];
|
|
50
62
|
const value = tokens[token];
|
|
51
63
|
// `code` is the rgba overlay chip; composite over the mode's bg.
|
|
52
64
|
if (value.startsWith("rgba"))
|
|
@@ -75,8 +87,10 @@ export function createTuiTheme(options) {
|
|
|
75
87
|
"ok",
|
|
76
88
|
"code",
|
|
77
89
|
"accent",
|
|
90
|
+
"warn",
|
|
91
|
+
"bad",
|
|
78
92
|
]) {
|
|
79
|
-
const color = parseColor(resolveToken(tokens, accentHex, token));
|
|
93
|
+
const color = parseColor(resolveToken(tokens, accentHex, options.mode, token));
|
|
80
94
|
fgSequences.set(token, foregroundAnsi(color, mode));
|
|
81
95
|
bgSequences.set(token, backgroundAnsi(color, mode));
|
|
82
96
|
}
|
package/dist/ui/app.d.ts
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
*/
|
|
49
49
|
import { Editor, type Component, type TUI } from "@earendil-works/pi-tui";
|
|
50
50
|
import type { SigrunClient } from "../api.js";
|
|
51
|
+
import type { JsonObject } from "@sigrun/shared/blocks";
|
|
51
52
|
import { type Unwatch, type WatchHandlers, type WatchOptions } from "../sse.js";
|
|
52
53
|
import type { TuiTheme } from "../theme.js";
|
|
53
54
|
import type { Action, ActionHost, Focus } from "../actions.js";
|
|
@@ -91,6 +92,41 @@ export declare class TuiApp implements Component, ActionHost {
|
|
|
91
92
|
constructor(client: SigrunClient, options: TuiAppOptions);
|
|
92
93
|
/** The active conversation id (undefined before the first refresh). */
|
|
93
94
|
get activeConversationId(): number | undefined;
|
|
95
|
+
/** The launcher's hello hook (U3 Task 5): the stream id feeds
|
|
96
|
+
* future openExtView transports; an OPEN view's handle gets the
|
|
97
|
+
* new id at once (its next fetch re-registers the watcher entry —
|
|
98
|
+
* spec §7.3's reconnect rule; the webui twin remounts instead). */
|
|
99
|
+
setEventsStreamId(streamId: string): void;
|
|
100
|
+
/** The open extension view's identity (undefined = the conversation
|
|
101
|
+
* pane is the main content). */
|
|
102
|
+
get extView(): {
|
|
103
|
+
readonly slug: string;
|
|
104
|
+
readonly id: string;
|
|
105
|
+
} | undefined;
|
|
106
|
+
/**
|
|
107
|
+
* Open an extension VIEW in the main pane (U3 Task 5, spec §7.1):
|
|
108
|
+
* the sidebar's view selection swaps the pane's content to the
|
|
109
|
+
* surface's transport. The block renderer is U2's; the app holds
|
|
110
|
+
* the transport + identity here so U2's mount is one prop pass.
|
|
111
|
+
*/
|
|
112
|
+
openExtView(slug: string, id: string, params?: JsonObject): Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* Close the open extension view (the Esc/back idiom): the pane
|
|
115
|
+
* returns to the conversation AND the surface's fetches stop — no
|
|
116
|
+
* cadence, no keepalive for a surface nobody is looking at (spec
|
|
117
|
+
* §7.3's stop-when-hidden rule; U2's cadence timer consults this).
|
|
118
|
+
* Returns true when a view was open (the key was consumed).
|
|
119
|
+
*/
|
|
120
|
+
closeExtView(): boolean;
|
|
121
|
+
/** The app-level surface.changed dispatch (the launcher's
|
|
122
|
+
* `watchEvents` onEvent routes events here). */
|
|
123
|
+
dispatchSurfaceChanged(event: {
|
|
124
|
+
slug: string;
|
|
125
|
+
id: string;
|
|
126
|
+
k: string;
|
|
127
|
+
rev: number;
|
|
128
|
+
treeRev: number;
|
|
129
|
+
}): void;
|
|
94
130
|
/** The focused region (the action layer's routing state). */
|
|
95
131
|
get focus(): Focus;
|
|
96
132
|
/** A modal overlay (picker/search) owns the keys. */
|
package/dist/ui/app.js
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
*/
|
|
49
49
|
import { Editor, HStack, Key, matchesKey, VStack, } from "@earendil-works/pi-tui";
|
|
50
50
|
import { SigrunApiError } from "../api.js";
|
|
51
|
+
import { ExtSurfaceHub, surfaceKeyHash, transportKey, } from "../blocks/ext-transport.js";
|
|
51
52
|
import { watchConversation } from "../sse.js";
|
|
52
53
|
import { spawnSync } from "node:child_process";
|
|
53
54
|
import { editInVim, resolveVimEditor } from "../editor-launch.js";
|
|
@@ -79,6 +80,14 @@ function plainEditorTheme() {
|
|
|
79
80
|
}
|
|
80
81
|
export class TuiApp {
|
|
81
82
|
#client;
|
|
83
|
+
/** The extension-surface transport hub (U3 Task 5): one per app —
|
|
84
|
+
* `openExtView`'s transport subscribes through it; the launcher
|
|
85
|
+
* dispatches `surface.changed` events into it. */
|
|
86
|
+
#extHub = new ExtSurfaceHub();
|
|
87
|
+
/** The open extension view (the main pane's surface state). The
|
|
88
|
+
* HANDLE (not the bare transport): a reconnect's new stream id
|
|
89
|
+
* lands via setStreamId without re-creating the transport. */
|
|
90
|
+
#extView;
|
|
82
91
|
#watch;
|
|
83
92
|
#watchOptions;
|
|
84
93
|
/** The sidebar (the app mounts it; tests drive it). */
|
|
@@ -158,6 +167,12 @@ export class TuiApp {
|
|
|
158
167
|
if (selection === "new") {
|
|
159
168
|
void this.createAndSelect();
|
|
160
169
|
}
|
|
170
|
+
else if (typeof selection === "object") {
|
|
171
|
+
// An extension VIEW (U3 Task 5): open it in the main pane.
|
|
172
|
+
void this.openExtView(selection.view.slug, selection.view.id).catch((error) => {
|
|
173
|
+
this.pane.showError(describeCommandError("open view", error));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
161
176
|
else {
|
|
162
177
|
// Caught at the call site (fix wave F1): selectConversation
|
|
163
178
|
// keeps propagating for boot's fail-loud refresh, so the
|
|
@@ -175,6 +190,69 @@ export class TuiApp {
|
|
|
175
190
|
get activeConversationId() {
|
|
176
191
|
return this.#activeId;
|
|
177
192
|
}
|
|
193
|
+
/** The current /v1/events stream id (the hello frame's payload);
|
|
194
|
+
* the ext-view transports send it as X-Sigrun-Stream. */
|
|
195
|
+
#eventsStreamId;
|
|
196
|
+
/** The launcher's hello hook (U3 Task 5): the stream id feeds
|
|
197
|
+
* future openExtView transports; an OPEN view's handle gets the
|
|
198
|
+
* new id at once (its next fetch re-registers the watcher entry —
|
|
199
|
+
* spec §7.3's reconnect rule; the webui twin remounts instead). */
|
|
200
|
+
setEventsStreamId(streamId) {
|
|
201
|
+
this.#eventsStreamId = streamId;
|
|
202
|
+
this.#extView?.handle.setStreamId(streamId);
|
|
203
|
+
}
|
|
204
|
+
/** The open extension view's identity (undefined = the conversation
|
|
205
|
+
* pane is the main content). */
|
|
206
|
+
get extView() {
|
|
207
|
+
return this.#extView === undefined
|
|
208
|
+
? undefined
|
|
209
|
+
: { slug: this.#extView.slug, id: this.#extView.id };
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Open an extension VIEW in the main pane (U3 Task 5, spec §7.1):
|
|
213
|
+
* the sidebar's view selection swaps the pane's content to the
|
|
214
|
+
* surface's transport. The block renderer is U2's; the app holds
|
|
215
|
+
* the transport + identity here so U2's mount is one prop pass.
|
|
216
|
+
*/
|
|
217
|
+
async openExtView(slug, id, params) {
|
|
218
|
+
const key = transportKey({ slug, id, baseUrl: this.#watchOptions.baseUrl, ...(params === undefined ? {} : { params }) });
|
|
219
|
+
const handle = this.#extHub.attach({
|
|
220
|
+
slug,
|
|
221
|
+
id,
|
|
222
|
+
...(params === undefined ? {} : { params }),
|
|
223
|
+
baseUrl: this.#watchOptions.baseUrl,
|
|
224
|
+
...(this.#watchOptions.user === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: { user: this.#watchOptions.user }),
|
|
227
|
+
...(this.#watchOptions.token === undefined
|
|
228
|
+
? {}
|
|
229
|
+
: { token: this.#watchOptions.token }),
|
|
230
|
+
...(this.#eventsStreamId === undefined
|
|
231
|
+
? {}
|
|
232
|
+
: { streamId: this.#eventsStreamId }),
|
|
233
|
+
}, surfaceKeyHash(slug, id, key));
|
|
234
|
+
this.#extView = { slug, id, handle };
|
|
235
|
+
this.#tui.requestRender();
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Close the open extension view (the Esc/back idiom): the pane
|
|
239
|
+
* returns to the conversation AND the surface's fetches stop — no
|
|
240
|
+
* cadence, no keepalive for a surface nobody is looking at (spec
|
|
241
|
+
* §7.3's stop-when-hidden rule; U2's cadence timer consults this).
|
|
242
|
+
* Returns true when a view was open (the key was consumed).
|
|
243
|
+
*/
|
|
244
|
+
closeExtView() {
|
|
245
|
+
if (this.#extView === undefined)
|
|
246
|
+
return false;
|
|
247
|
+
this.#extView = undefined;
|
|
248
|
+
this.#tui.requestRender();
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
/** The app-level surface.changed dispatch (the launcher's
|
|
252
|
+
* `watchEvents` onEvent routes events here). */
|
|
253
|
+
dispatchSurfaceChanged(event) {
|
|
254
|
+
this.#extHub.dispatch(event);
|
|
255
|
+
}
|
|
178
256
|
/** The focused region (the action layer's routing state). */
|
|
179
257
|
get focus() {
|
|
180
258
|
return this.#focus;
|
|
@@ -447,6 +525,12 @@ export class TuiApp {
|
|
|
447
525
|
this.#abortActive();
|
|
448
526
|
return true;
|
|
449
527
|
}
|
|
528
|
+
// Esc closes an open extension view — back to the
|
|
529
|
+
// conversation pane; the surface's fetches stop (the keepalive
|
|
530
|
+
// stops with the view, spec §7.3's stop-when-hidden).
|
|
531
|
+
if (matchesKey(data, Key.escape) && this.#extView !== undefined) {
|
|
532
|
+
return this.closeExtView();
|
|
533
|
+
}
|
|
450
534
|
return false;
|
|
451
535
|
}
|
|
452
536
|
/**
|
package/dist/ui/interactions.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* `[error]` line).
|
|
43
43
|
*/
|
|
44
44
|
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
45
|
+
import { diffLines } from "../blocks/diff.js";
|
|
45
46
|
import { describeCommandError } from "./composer.js";
|
|
46
47
|
/** The answer-mode option pick keys: digits 1-9. */
|
|
47
48
|
const DIGIT = /^[1-9]$/;
|
|
@@ -281,13 +282,7 @@ export class InteractionPrompter {
|
|
|
281
282
|
lines.push(line);
|
|
282
283
|
}
|
|
283
284
|
if (body?.diff !== undefined) {
|
|
284
|
-
lines.push(body.diff
|
|
285
|
-
? "[diff]"
|
|
286
|
-
: `[diff] ${body.diff.path}`);
|
|
287
|
-
for (const line of body.diff.before.split("\n"))
|
|
288
|
-
lines.push(`- ${line}`);
|
|
289
|
-
for (const line of body.diff.after.split("\n"))
|
|
290
|
-
lines.push(`+ ${line}`);
|
|
285
|
+
lines.push(...diffLines(body.diff));
|
|
291
286
|
}
|
|
292
287
|
for (const [index, option] of options.entries()) {
|
|
293
288
|
const marker = this.#answerMode && multi
|
package/dist/ui/sidebar.d.ts
CHANGED
|
@@ -24,8 +24,15 @@
|
|
|
24
24
|
import { type Component } from "@earendil-works/pi-tui";
|
|
25
25
|
import { type TuiTheme } from "../theme.js";
|
|
26
26
|
import type { SigrunClient } from "../api.js";
|
|
27
|
-
/** The sidebar selection: a conversation id,
|
|
28
|
-
|
|
27
|
+
/** The sidebar selection: a conversation id, the `+ new` action, or an
|
|
28
|
+
* extension VIEW (UI-blocks U3 Task 5 — the app opens it in the main
|
|
29
|
+
* pane). */
|
|
30
|
+
export type SidebarSelection = number | "new" | {
|
|
31
|
+
readonly view: {
|
|
32
|
+
readonly slug: string;
|
|
33
|
+
readonly id: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
29
36
|
/** One display row: a conversation, or the trailing `+ New` action. */
|
|
30
37
|
interface SidebarRow {
|
|
31
38
|
/** The conversation id; -1 for the `+ New conversation` action row. */
|
|
@@ -62,6 +69,22 @@ export declare class Sidebar implements Component {
|
|
|
62
69
|
* conversation when it still exists.
|
|
63
70
|
*/
|
|
64
71
|
refresh(): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* The extension views (U3 Task 5): refetch `GET /v1/extensions` and
|
|
74
|
+
* rebuild the VIEWS group (one row per declared view surface, in
|
|
75
|
+
* declaration order, status "ok" only). Panels live in the side pane,
|
|
76
|
+
* not here. Failures keep the last good list (a flaky server must
|
|
77
|
+
* never blank the sidebar).
|
|
78
|
+
*/
|
|
79
|
+
refreshViews(): Promise<void>;
|
|
80
|
+
/** The extension view rows (the app's openExtView reads them). */
|
|
81
|
+
get views(): readonly {
|
|
82
|
+
slug: string;
|
|
83
|
+
id: string;
|
|
84
|
+
title: string;
|
|
85
|
+
}[];
|
|
86
|
+
/** Select the Nth extension view (the action layer's seam). */
|
|
87
|
+
confirmView(index: number): void;
|
|
65
88
|
/** Mark the active conversation (accent fg + bold); pass `undefined` to clear. */
|
|
66
89
|
setActive(id: number | undefined): void;
|
|
67
90
|
/** Move the cursor by `delta` rows, clamped to the list. */
|