@teammates/consolonia 0.2.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/README.md +48 -0
- package/dist/__tests__/ansi.test.d.ts +1 -0
- package/dist/__tests__/ansi.test.js +520 -0
- package/dist/__tests__/chat-view.test.d.ts +4 -0
- package/dist/__tests__/chat-view.test.js +480 -0
- package/dist/__tests__/drawing.test.d.ts +4 -0
- package/dist/__tests__/drawing.test.js +426 -0
- package/dist/__tests__/input.test.d.ts +5 -0
- package/dist/__tests__/input.test.js +911 -0
- package/dist/__tests__/layout.test.d.ts +4 -0
- package/dist/__tests__/layout.test.js +689 -0
- package/dist/__tests__/pixel.test.d.ts +1 -0
- package/dist/__tests__/pixel.test.js +674 -0
- package/dist/__tests__/render.test.d.ts +1 -0
- package/dist/__tests__/render.test.js +400 -0
- package/dist/__tests__/styled.test.d.ts +4 -0
- package/dist/__tests__/styled.test.js +149 -0
- package/dist/__tests__/widgets.test.d.ts +5 -0
- package/dist/__tests__/widgets.test.js +924 -0
- package/dist/ansi/esc.d.ts +61 -0
- package/dist/ansi/esc.js +85 -0
- package/dist/ansi/output.d.ts +66 -0
- package/dist/ansi/output.js +192 -0
- package/dist/ansi/strip.d.ts +16 -0
- package/dist/ansi/strip.js +74 -0
- package/dist/app.d.ts +68 -0
- package/dist/app.js +297 -0
- package/dist/drawing/clip.d.ts +23 -0
- package/dist/drawing/clip.js +67 -0
- package/dist/drawing/context.d.ts +77 -0
- package/dist/drawing/context.js +275 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +63 -0
- package/dist/input/escape-matcher.d.ts +27 -0
- package/dist/input/escape-matcher.js +253 -0
- package/dist/input/events.d.ts +49 -0
- package/dist/input/events.js +17 -0
- package/dist/input/index.d.ts +15 -0
- package/dist/input/index.js +14 -0
- package/dist/input/matcher.d.ts +23 -0
- package/dist/input/matcher.js +14 -0
- package/dist/input/mouse-matcher.d.ts +27 -0
- package/dist/input/mouse-matcher.js +142 -0
- package/dist/input/paste-matcher.d.ts +23 -0
- package/dist/input/paste-matcher.js +104 -0
- package/dist/input/processor.d.ts +51 -0
- package/dist/input/processor.js +145 -0
- package/dist/input/raw-mode.d.ts +13 -0
- package/dist/input/raw-mode.js +24 -0
- package/dist/input/text-matcher.d.ts +14 -0
- package/dist/input/text-matcher.js +32 -0
- package/dist/layout/box.d.ts +33 -0
- package/dist/layout/box.js +92 -0
- package/dist/layout/column.d.ts +21 -0
- package/dist/layout/column.js +90 -0
- package/dist/layout/control.d.ts +73 -0
- package/dist/layout/control.js +215 -0
- package/dist/layout/row.d.ts +21 -0
- package/dist/layout/row.js +95 -0
- package/dist/layout/stack.d.ts +18 -0
- package/dist/layout/stack.js +64 -0
- package/dist/layout/types.d.ts +27 -0
- package/dist/layout/types.js +4 -0
- package/dist/pixel/background.d.ts +16 -0
- package/dist/pixel/background.js +16 -0
- package/dist/pixel/box-pattern.d.ts +38 -0
- package/dist/pixel/box-pattern.js +57 -0
- package/dist/pixel/buffer.d.ts +25 -0
- package/dist/pixel/buffer.js +51 -0
- package/dist/pixel/color.d.ts +48 -0
- package/dist/pixel/color.js +92 -0
- package/dist/pixel/foreground.d.ts +31 -0
- package/dist/pixel/foreground.js +64 -0
- package/dist/pixel/pixel.d.ts +21 -0
- package/dist/pixel/pixel.js +38 -0
- package/dist/pixel/symbol.d.ts +38 -0
- package/dist/pixel/symbol.js +192 -0
- package/dist/render/regions.d.ts +54 -0
- package/dist/render/regions.js +102 -0
- package/dist/render/render-target.d.ts +42 -0
- package/dist/render/render-target.js +118 -0
- package/dist/styled.d.ts +113 -0
- package/dist/styled.js +176 -0
- package/dist/widgets/border.d.ts +34 -0
- package/dist/widgets/border.js +121 -0
- package/dist/widgets/chat-view.d.ts +239 -0
- package/dist/widgets/chat-view.js +993 -0
- package/dist/widgets/interview.d.ts +87 -0
- package/dist/widgets/interview.js +187 -0
- package/dist/widgets/markdown.d.ts +87 -0
- package/dist/widgets/markdown.js +611 -0
- package/dist/widgets/panel.d.ts +19 -0
- package/dist/widgets/panel.js +35 -0
- package/dist/widgets/scroll-view.d.ts +43 -0
- package/dist/widgets/scroll-view.js +182 -0
- package/dist/widgets/styled-text.d.ts +38 -0
- package/dist/widgets/styled-text.js +183 -0
- package/dist/widgets/syntax.d.ts +37 -0
- package/dist/widgets/syntax.js +670 -0
- package/dist/widgets/text-input.d.ts +121 -0
- package/dist/widgets/text-input.js +618 -0
- package/dist/widgets/text.d.ts +34 -0
- package/dist/widgets/text.js +168 -0
- package/package.json +45 -0
package/dist/styled.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Styled — a chalk-like fluent API for building styled text segments.
|
|
3
|
+
*
|
|
4
|
+
* Instead of producing ANSI escape codes (which only work on raw stdout),
|
|
5
|
+
* Styled produces {text, style} segment arrays that can be rendered into
|
|
6
|
+
* a pixel buffer via DrawingContext.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { pen } from "@teammates/consolonia";
|
|
10
|
+
*
|
|
11
|
+
* // Single styled segment
|
|
12
|
+
* pen.cyan("hello") → [{ text: "hello", style: { fg: CYAN } }]
|
|
13
|
+
*
|
|
14
|
+
* // Chained styles
|
|
15
|
+
* pen.bold.red("error") → [{ text: "error", style: { fg: RED, bold: true } }]
|
|
16
|
+
*
|
|
17
|
+
* // Concatenation with +
|
|
18
|
+
* pen.green("✔ ") + pen.white("done")
|
|
19
|
+
* → [{ text: "✔ ", style: { fg: GREEN } }, { text: "done", style: { fg: WHITE } }]
|
|
20
|
+
*
|
|
21
|
+
* // Mixed plain + styled
|
|
22
|
+
* pen("prefix ") + pen.cyan("@name")
|
|
23
|
+
* → [{ text: "prefix ", style: {} }, { text: "@name", style: { fg: CYAN } }]
|
|
24
|
+
*
|
|
25
|
+
* // Gray is dim white (like chalk.gray)
|
|
26
|
+
* pen.gray("muted") → [{ text: "muted", style: { fg: GRAY } }]
|
|
27
|
+
*/
|
|
28
|
+
import { BLACK, BLACK_BRIGHT, BLUE, BLUE_BRIGHT, CYAN, CYAN_BRIGHT, DARK_GRAY, GRAY, GREEN, GREEN_BRIGHT, LIGHT_GRAY, MAGENTA, MAGENTA_BRIGHT, RED, RED_BRIGHT, WHITE, WHITE_BRIGHT, YELLOW, YELLOW_BRIGHT, } from "./pixel/color.js";
|
|
29
|
+
import { charWidth } from "./pixel/symbol.js";
|
|
30
|
+
/** Create a StyledSpan from segments. */
|
|
31
|
+
function span(segments) {
|
|
32
|
+
const arr = segments;
|
|
33
|
+
arr.__brand = "StyledSpan";
|
|
34
|
+
return arr;
|
|
35
|
+
}
|
|
36
|
+
/** Check if a value is a StyledSpan. */
|
|
37
|
+
export function isStyledSpan(v) {
|
|
38
|
+
return Array.isArray(v) && v.__brand === "StyledSpan";
|
|
39
|
+
}
|
|
40
|
+
/** Concatenate styled spans and/or plain strings. */
|
|
41
|
+
export function concat(...parts) {
|
|
42
|
+
const segments = [];
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
if (typeof part === "string") {
|
|
45
|
+
segments.push({ text: part, style: {} });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
segments.push(...part);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return span(segments);
|
|
52
|
+
}
|
|
53
|
+
/** Get the visible (unstyled) text from a StyledSpan. */
|
|
54
|
+
export function spanText(s) {
|
|
55
|
+
return s.map((seg) => seg.text).join("");
|
|
56
|
+
}
|
|
57
|
+
/** Get the visible display width of a StyledSpan (accounts for wide characters). */
|
|
58
|
+
export function spanLength(s) {
|
|
59
|
+
let width = 0;
|
|
60
|
+
for (const seg of s) {
|
|
61
|
+
for (const ch of seg.text) {
|
|
62
|
+
width += charWidth(ch.codePointAt(0));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return width;
|
|
66
|
+
}
|
|
67
|
+
// ── Named colors ─────────────────────────────────────────────────
|
|
68
|
+
//
|
|
69
|
+
// Maps match chalk's naming conventions exactly.
|
|
70
|
+
const FG_COLORS = {
|
|
71
|
+
// Standard (chalk: black, red, green, yellow, blue, magenta, cyan, white)
|
|
72
|
+
black: BLACK,
|
|
73
|
+
red: RED,
|
|
74
|
+
green: GREEN,
|
|
75
|
+
yellow: YELLOW,
|
|
76
|
+
blue: BLUE,
|
|
77
|
+
magenta: MAGENTA,
|
|
78
|
+
cyan: CYAN,
|
|
79
|
+
white: WHITE,
|
|
80
|
+
// Bright (chalk: blackBright … whiteBright)
|
|
81
|
+
blackBright: BLACK_BRIGHT,
|
|
82
|
+
redBright: RED_BRIGHT,
|
|
83
|
+
greenBright: GREEN_BRIGHT,
|
|
84
|
+
yellowBright: YELLOW_BRIGHT,
|
|
85
|
+
blueBright: BLUE_BRIGHT,
|
|
86
|
+
magentaBright: MAGENTA_BRIGHT,
|
|
87
|
+
cyanBright: CYAN_BRIGHT,
|
|
88
|
+
whiteBright: WHITE_BRIGHT,
|
|
89
|
+
// Aliases (chalk: gray = grey = blackBright)
|
|
90
|
+
gray: GRAY,
|
|
91
|
+
grey: GRAY,
|
|
92
|
+
// Extra consolonia aliases
|
|
93
|
+
darkGray: DARK_GRAY,
|
|
94
|
+
lightGray: LIGHT_GRAY,
|
|
95
|
+
};
|
|
96
|
+
const BG_COLORS = {
|
|
97
|
+
// Standard
|
|
98
|
+
bgBlack: BLACK,
|
|
99
|
+
bgRed: RED,
|
|
100
|
+
bgGreen: GREEN,
|
|
101
|
+
bgYellow: YELLOW,
|
|
102
|
+
bgBlue: BLUE,
|
|
103
|
+
bgMagenta: MAGENTA,
|
|
104
|
+
bgCyan: CYAN,
|
|
105
|
+
bgWhite: WHITE,
|
|
106
|
+
// Bright
|
|
107
|
+
bgBlackBright: BLACK_BRIGHT,
|
|
108
|
+
bgRedBright: RED_BRIGHT,
|
|
109
|
+
bgGreenBright: GREEN_BRIGHT,
|
|
110
|
+
bgYellowBright: YELLOW_BRIGHT,
|
|
111
|
+
bgBlueBright: BLUE_BRIGHT,
|
|
112
|
+
bgMagentaBright: MAGENTA_BRIGHT,
|
|
113
|
+
bgCyanBright: CYAN_BRIGHT,
|
|
114
|
+
bgWhiteBright: WHITE_BRIGHT,
|
|
115
|
+
// Aliases (chalk: bgGray = bgGrey = bgBlackBright)
|
|
116
|
+
bgGray: BLACK_BRIGHT,
|
|
117
|
+
bgGrey: BLACK_BRIGHT,
|
|
118
|
+
};
|
|
119
|
+
// Style flag names
|
|
120
|
+
const STYLE_FLAGS = {
|
|
121
|
+
bold: "bold",
|
|
122
|
+
italic: "italic",
|
|
123
|
+
underline: "underline",
|
|
124
|
+
strikethrough: "strikethrough",
|
|
125
|
+
dim: "bold", // dim maps to non-bold (handled specially)
|
|
126
|
+
};
|
|
127
|
+
function createPen(accumulated) {
|
|
128
|
+
const callable = (text) => {
|
|
129
|
+
return span([{ text, style: { ...accumulated } }]);
|
|
130
|
+
};
|
|
131
|
+
return new Proxy(callable, {
|
|
132
|
+
get(_target, prop) {
|
|
133
|
+
// Arbitrary foreground: pen.fg(color)
|
|
134
|
+
if (prop === "fg") {
|
|
135
|
+
return (c) => createPen({ ...accumulated, fg: c });
|
|
136
|
+
}
|
|
137
|
+
// Arbitrary background: pen.bg(color)
|
|
138
|
+
if (prop === "bg") {
|
|
139
|
+
return (c) => createPen({ ...accumulated, bg: c });
|
|
140
|
+
}
|
|
141
|
+
// Named foreground colors
|
|
142
|
+
if (prop in FG_COLORS) {
|
|
143
|
+
return createPen({ ...accumulated, fg: FG_COLORS[prop] });
|
|
144
|
+
}
|
|
145
|
+
// Named background colors
|
|
146
|
+
if (prop in BG_COLORS) {
|
|
147
|
+
return createPen({ ...accumulated, bg: BG_COLORS[prop] });
|
|
148
|
+
}
|
|
149
|
+
// Style flags
|
|
150
|
+
if (prop in STYLE_FLAGS) {
|
|
151
|
+
if (prop === "dim") {
|
|
152
|
+
// dim is represented as gray foreground if no fg set
|
|
153
|
+
return createPen({
|
|
154
|
+
...accumulated,
|
|
155
|
+
fg: accumulated.fg ?? GRAY,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return createPen({
|
|
159
|
+
...accumulated,
|
|
160
|
+
[STYLE_FLAGS[prop]]: true,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The default pen — starting point for building styled text.
|
|
169
|
+
*
|
|
170
|
+
* Usage:
|
|
171
|
+
* pen("plain text")
|
|
172
|
+
* pen.cyan("colored")
|
|
173
|
+
* pen.bold.red("bold red")
|
|
174
|
+
* pen.bgBlue.white("white on blue")
|
|
175
|
+
*/
|
|
176
|
+
export const pen = createPen({});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box-drawing border around a single child control.
|
|
3
|
+
*
|
|
4
|
+
* Draws a Unicode box-drawing rectangle (via DrawingContext.drawBox)
|
|
5
|
+
* and optionally renders a title string embedded in the top border
|
|
6
|
+
* in the form: ┤ Title ├
|
|
7
|
+
*/
|
|
8
|
+
import type { BoxStyle, DrawingContext, TextStyle } from "../drawing/context.js";
|
|
9
|
+
import { Control } from "../layout/control.js";
|
|
10
|
+
import type { Constraint, Rect, Size } from "../layout/types.js";
|
|
11
|
+
export interface BorderOptions {
|
|
12
|
+
child?: Control;
|
|
13
|
+
title?: string;
|
|
14
|
+
style?: BoxStyle;
|
|
15
|
+
titleStyle?: TextStyle;
|
|
16
|
+
}
|
|
17
|
+
export declare class Border extends Control {
|
|
18
|
+
private _child;
|
|
19
|
+
private _title;
|
|
20
|
+
private _style;
|
|
21
|
+
private _titleStyle;
|
|
22
|
+
constructor(options?: BorderOptions);
|
|
23
|
+
get child(): Control | null;
|
|
24
|
+
set child(value: Control | null);
|
|
25
|
+
get title(): string;
|
|
26
|
+
set title(value: string);
|
|
27
|
+
get style(): BoxStyle;
|
|
28
|
+
set style(value: BoxStyle);
|
|
29
|
+
get titleStyle(): TextStyle;
|
|
30
|
+
set titleStyle(value: TextStyle);
|
|
31
|
+
measure(constraint: Constraint): Size;
|
|
32
|
+
arrange(rect: Rect): void;
|
|
33
|
+
render(ctx: DrawingContext): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Box-drawing border around a single child control.
|
|
3
|
+
*
|
|
4
|
+
* Draws a Unicode box-drawing rectangle (via DrawingContext.drawBox)
|
|
5
|
+
* and optionally renders a title string embedded in the top border
|
|
6
|
+
* in the form: ┤ Title ├
|
|
7
|
+
*/
|
|
8
|
+
import { Control } from "../layout/control.js";
|
|
9
|
+
export class Border extends Control {
|
|
10
|
+
_child;
|
|
11
|
+
_title;
|
|
12
|
+
_style;
|
|
13
|
+
_titleStyle;
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
super();
|
|
16
|
+
this._child = options.child ?? null;
|
|
17
|
+
this._title = options.title ?? "";
|
|
18
|
+
this._style = options.style ?? {};
|
|
19
|
+
this._titleStyle = options.titleStyle ?? {};
|
|
20
|
+
if (this._child) {
|
|
21
|
+
this.children.push(this._child);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ── Properties ────────────────────────────────────────────────
|
|
25
|
+
get child() {
|
|
26
|
+
return this._child;
|
|
27
|
+
}
|
|
28
|
+
set child(value) {
|
|
29
|
+
// Remove old child
|
|
30
|
+
if (this._child) {
|
|
31
|
+
const idx = this.children.indexOf(this._child);
|
|
32
|
+
if (idx >= 0)
|
|
33
|
+
this.children.splice(idx, 1);
|
|
34
|
+
}
|
|
35
|
+
this._child = value;
|
|
36
|
+
if (value) {
|
|
37
|
+
this.children.push(value);
|
|
38
|
+
}
|
|
39
|
+
this.invalidate();
|
|
40
|
+
}
|
|
41
|
+
get title() {
|
|
42
|
+
return this._title;
|
|
43
|
+
}
|
|
44
|
+
set title(value) {
|
|
45
|
+
if (this._title !== value) {
|
|
46
|
+
this._title = value;
|
|
47
|
+
this.invalidate();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
get style() {
|
|
51
|
+
return this._style;
|
|
52
|
+
}
|
|
53
|
+
set style(value) {
|
|
54
|
+
this._style = value;
|
|
55
|
+
this.invalidate();
|
|
56
|
+
}
|
|
57
|
+
get titleStyle() {
|
|
58
|
+
return this._titleStyle;
|
|
59
|
+
}
|
|
60
|
+
set titleStyle(value) {
|
|
61
|
+
this._titleStyle = value;
|
|
62
|
+
this.invalidate();
|
|
63
|
+
}
|
|
64
|
+
// ── Layout ────────────────────────────────────────────────────
|
|
65
|
+
measure(constraint) {
|
|
66
|
+
// Border takes 1 cell on each side (2 total per axis)
|
|
67
|
+
const _innerConstraint = {
|
|
68
|
+
minWidth: Math.max(0, constraint.minWidth - 2),
|
|
69
|
+
minHeight: Math.max(0, constraint.minHeight - 2),
|
|
70
|
+
maxWidth: Math.max(0, constraint.maxWidth - 2),
|
|
71
|
+
maxHeight: Math.max(0, constraint.maxHeight - 2),
|
|
72
|
+
};
|
|
73
|
+
if (this._child) {
|
|
74
|
+
// Trigger child measure through its public API
|
|
75
|
+
const childSize = this._child.desiredSize ?? { width: 0, height: 0 };
|
|
76
|
+
// We need to invoke the child's layout — in a typical layout system
|
|
77
|
+
// the parent is responsible for measuring children. We'll call the
|
|
78
|
+
// child's measure indirectly by having the layout system handle it.
|
|
79
|
+
// For now, return border + child desired.
|
|
80
|
+
return {
|
|
81
|
+
width: (childSize.width ?? 0) + 2,
|
|
82
|
+
height: (childSize.height ?? 0) + 2,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// No child: just the border itself (minimum 2x2 for corners)
|
|
86
|
+
return { width: 2, height: 2 };
|
|
87
|
+
}
|
|
88
|
+
arrange(rect) {
|
|
89
|
+
if (this._child) {
|
|
90
|
+
this._child.arrange({
|
|
91
|
+
x: rect.x + 1,
|
|
92
|
+
y: rect.y + 1,
|
|
93
|
+
width: Math.max(0, rect.width - 2),
|
|
94
|
+
height: Math.max(0, rect.height - 2),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
render(ctx) {
|
|
99
|
+
const bounds = this.bounds;
|
|
100
|
+
if (!bounds || bounds.width < 2 || bounds.height < 2)
|
|
101
|
+
return;
|
|
102
|
+
// Draw the box border
|
|
103
|
+
ctx.drawBox(bounds, this._style);
|
|
104
|
+
// Draw title in the top border if present
|
|
105
|
+
if (this._title.length > 0) {
|
|
106
|
+
const maxTitleLen = bounds.width - 4; // room for ┤ and ├ plus spaces
|
|
107
|
+
if (maxTitleLen > 0) {
|
|
108
|
+
const displayTitle = this._title.length > maxTitleLen
|
|
109
|
+
? this._title.slice(0, maxTitleLen)
|
|
110
|
+
: this._title;
|
|
111
|
+
// Draw "┤ Title ├" starting 2 cells from the left edge of top border
|
|
112
|
+
const startX = bounds.x + 2;
|
|
113
|
+
ctx.drawText(startX, bounds.y, `\u2524 ${displayTitle} \u251C`, this._titleStyle);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Render child
|
|
117
|
+
if (this._child) {
|
|
118
|
+
this._child.render(ctx);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatView — full-screen chat widget for terminal REPLs.
|
|
3
|
+
*
|
|
4
|
+
* Layout (top to bottom):
|
|
5
|
+
*
|
|
6
|
+
* ┌─ banner ──────────────────────────────┐
|
|
7
|
+
* │ customizable multi-line header text │
|
|
8
|
+
* ├───────────────────────────────────────-┤
|
|
9
|
+
* │ │
|
|
10
|
+
* │ scrollable feed area │
|
|
11
|
+
* │ (messages, agent output, etc.) │
|
|
12
|
+
* │ │
|
|
13
|
+
* ├───────────────────────────────────────-┤
|
|
14
|
+
* │ progress message (optional) │
|
|
15
|
+
* │ ❯ input box │
|
|
16
|
+
* │ ┌─ dropdown ───────────────────────┐ │
|
|
17
|
+
* │ │ /command1 description │ │
|
|
18
|
+
* │ │ /command2 description │ │
|
|
19
|
+
* │ └──────────────────────────────────-┘ │
|
|
20
|
+
* └────────────────────────────────────────┘
|
|
21
|
+
*
|
|
22
|
+
* The feed is the terminal's own scrollback: new content is appended
|
|
23
|
+
* as Text children to the feed Column. Everything is double-buffered
|
|
24
|
+
* through Consolonia's PixelBuffer so resizing redraws cleanly.
|
|
25
|
+
*
|
|
26
|
+
* Events emitted:
|
|
27
|
+
* "submit" (text: string) — user pressed Enter
|
|
28
|
+
* "change" (text: string) — input value changed
|
|
29
|
+
* "cancel" () — user pressed Escape
|
|
30
|
+
* "tab" () — user pressed Tab (for autocomplete)
|
|
31
|
+
*/
|
|
32
|
+
import type { DrawingContext, TextStyle } from "../drawing/context.js";
|
|
33
|
+
import type { InputEvent } from "../input/events.js";
|
|
34
|
+
import { Control } from "../layout/control.js";
|
|
35
|
+
import type { Constraint, Rect, Size } from "../layout/types.js";
|
|
36
|
+
import type { StyledSpan } from "../styled.js";
|
|
37
|
+
import { type StyledLine } from "./styled-text.js";
|
|
38
|
+
import { type DeleteSizer, type InputColorizer, TextInput } from "./text-input.js";
|
|
39
|
+
export interface DropdownItem {
|
|
40
|
+
/** Display label (left column). */
|
|
41
|
+
label: string;
|
|
42
|
+
/** Description (right column). */
|
|
43
|
+
description: string;
|
|
44
|
+
/** Full text to insert on accept. */
|
|
45
|
+
completion: string;
|
|
46
|
+
}
|
|
47
|
+
/** A single action item within an action line. */
|
|
48
|
+
export interface FeedActionItem {
|
|
49
|
+
id: string;
|
|
50
|
+
normalStyle: StyledLine;
|
|
51
|
+
hoverStyle: StyledLine;
|
|
52
|
+
}
|
|
53
|
+
export interface ChatViewOptions {
|
|
54
|
+
/** Banner text shown at the top of the chat area. */
|
|
55
|
+
banner?: string;
|
|
56
|
+
/** Style for the banner text. */
|
|
57
|
+
bannerStyle?: TextStyle;
|
|
58
|
+
/** Custom widget to use as the banner instead of the built-in Text. */
|
|
59
|
+
bannerWidget?: Control;
|
|
60
|
+
/** Prompt string for the input box (default "❯ "). */
|
|
61
|
+
prompt?: string;
|
|
62
|
+
/** Style for the prompt. */
|
|
63
|
+
promptStyle?: TextStyle;
|
|
64
|
+
/** Style for input text. */
|
|
65
|
+
inputStyle?: TextStyle;
|
|
66
|
+
/** Style for the cursor. */
|
|
67
|
+
cursorStyle?: TextStyle;
|
|
68
|
+
/** Per-character colorizer for input text. */
|
|
69
|
+
inputColorize?: InputColorizer;
|
|
70
|
+
/** Callback to determine delete size for backspace/delete in input. */
|
|
71
|
+
inputDeleteSize?: DeleteSizer;
|
|
72
|
+
/** Hint callback — returns dim text shown after the cursor (e.g. param placeholders). */
|
|
73
|
+
inputHint?: (value: string) => string | null;
|
|
74
|
+
/** Style for input hint text (default: dim). */
|
|
75
|
+
inputHintStyle?: TextStyle;
|
|
76
|
+
/** Placeholder when input is empty. */
|
|
77
|
+
placeholder?: string;
|
|
78
|
+
/** Style for placeholder text. */
|
|
79
|
+
placeholderStyle?: TextStyle;
|
|
80
|
+
/** Style for feed text (default messages). */
|
|
81
|
+
feedStyle?: TextStyle;
|
|
82
|
+
/** Style for progress message text. */
|
|
83
|
+
progressStyle?: TextStyle;
|
|
84
|
+
/** Style for the separator lines between banner/feed/input. */
|
|
85
|
+
separatorStyle?: TextStyle;
|
|
86
|
+
/** Character used for separator lines (default "─"). */
|
|
87
|
+
separatorChar?: string;
|
|
88
|
+
/** Style for highlighted dropdown item. */
|
|
89
|
+
dropdownHighlightStyle?: TextStyle;
|
|
90
|
+
/** Style for normal dropdown item description. */
|
|
91
|
+
dropdownStyle?: TextStyle;
|
|
92
|
+
/** Style for non-highlighted dropdown item label (/command, @name). */
|
|
93
|
+
dropdownLabelStyle?: TextStyle;
|
|
94
|
+
/** Maximum number of lines the input box can grow to (default 1). */
|
|
95
|
+
maxInputHeight?: number;
|
|
96
|
+
/** Footer content shown below the input (StyledLine for mixed colors). */
|
|
97
|
+
footer?: StyledLine;
|
|
98
|
+
/** Style for footer text (used as default when footer is a plain string). */
|
|
99
|
+
footerStyle?: TextStyle;
|
|
100
|
+
/** Command history entries. */
|
|
101
|
+
history?: string[];
|
|
102
|
+
}
|
|
103
|
+
export declare class ChatView extends Control {
|
|
104
|
+
private _banner;
|
|
105
|
+
private _topSeparator;
|
|
106
|
+
private _feedLines;
|
|
107
|
+
/** Maps feed line index → action(s) for clickable lines. */
|
|
108
|
+
private _feedActions;
|
|
109
|
+
/** Feed line index currently hovered (-1 if none). */
|
|
110
|
+
private _hoveredAction;
|
|
111
|
+
/** Maps screen Y → feed line index (rebuilt each render). */
|
|
112
|
+
private _screenToFeedLine;
|
|
113
|
+
/** Maps screen Y → row offset within the feed line (for multi-row wrapped lines). */
|
|
114
|
+
private _screenToFeedRow;
|
|
115
|
+
private _bottomSeparator;
|
|
116
|
+
private _progressText;
|
|
117
|
+
private _input;
|
|
118
|
+
private _inputSeparator;
|
|
119
|
+
private _footer;
|
|
120
|
+
private _dropdownItems;
|
|
121
|
+
private _dropdownIndex;
|
|
122
|
+
private _feedStyle;
|
|
123
|
+
private _progressStyle;
|
|
124
|
+
private _separatorStyle;
|
|
125
|
+
private _separatorChar;
|
|
126
|
+
private _dropdownHighlightStyle;
|
|
127
|
+
private _dropdownStyle;
|
|
128
|
+
private _footerStyle;
|
|
129
|
+
private _maxInputH;
|
|
130
|
+
private _feedScrollOffset;
|
|
131
|
+
private _feedX;
|
|
132
|
+
private _contentWidth;
|
|
133
|
+
/** Cached from last render for hit-testing. */
|
|
134
|
+
private _scrollbarX;
|
|
135
|
+
private _feedY;
|
|
136
|
+
private _feedH;
|
|
137
|
+
private _thumbPos;
|
|
138
|
+
private _thumbSize;
|
|
139
|
+
private _maxScroll;
|
|
140
|
+
private _scrollbarVisible;
|
|
141
|
+
/** True while the user is dragging the scrollbar thumb. */
|
|
142
|
+
private _dragging;
|
|
143
|
+
/** The Y offset within the thumb where the drag started. */
|
|
144
|
+
private _dragOffsetY;
|
|
145
|
+
/** Optional widget that replaces the input area (e.g. Interview). */
|
|
146
|
+
private _inputOverride;
|
|
147
|
+
constructor(options?: ChatViewOptions);
|
|
148
|
+
/** Get the banner text (only works when using the built-in Text banner). */
|
|
149
|
+
get banner(): string;
|
|
150
|
+
/** Set the banner text (only works when using the built-in Text banner). */
|
|
151
|
+
set banner(text: string);
|
|
152
|
+
/** Get the banner style (only works when using the built-in Text banner). */
|
|
153
|
+
get bannerStyle(): TextStyle;
|
|
154
|
+
/** Set the banner style (only works when using the built-in Text banner). */
|
|
155
|
+
set bannerStyle(style: TextStyle);
|
|
156
|
+
/** Replace the banner with a custom widget. */
|
|
157
|
+
set bannerWidget(widget: Control);
|
|
158
|
+
/** Get the current banner widget. */
|
|
159
|
+
get bannerWidget(): Control;
|
|
160
|
+
/** Set footer content (plain string or StyledSpan for mixed colors). */
|
|
161
|
+
setFooter(content: StyledLine): void;
|
|
162
|
+
/** Append a line of plain text to the feed. Auto-scrolls to bottom. */
|
|
163
|
+
appendToFeed(text: string, style?: TextStyle): void;
|
|
164
|
+
/** Append a styled line (StyledSpan) to the feed. */
|
|
165
|
+
appendStyledToFeed(styledLine: StyledSpan): void;
|
|
166
|
+
/** Append a clickable action line to the feed. Emits "action" on click. */
|
|
167
|
+
appendAction(id: string, normalContent: StyledLine, hoverContent: StyledLine): void;
|
|
168
|
+
/** Append a line with multiple side-by-side clickable actions. */
|
|
169
|
+
appendActionList(actions: FeedActionItem[]): void;
|
|
170
|
+
/** Concatenate multiple StyledLine arrays into one. */
|
|
171
|
+
private _concatSpans;
|
|
172
|
+
/** Append multiple plain lines to the feed. */
|
|
173
|
+
appendLines(lines: string[], style?: TextStyle): void;
|
|
174
|
+
/** Clear everything between the banner and the input box. */
|
|
175
|
+
clear(): void;
|
|
176
|
+
/** Total number of feed lines. */
|
|
177
|
+
get feedLineCount(): number;
|
|
178
|
+
/** Update the content of an existing feed line by index. Also removes its action if any. */
|
|
179
|
+
updateFeedLine(index: number, content: StyledLine): void;
|
|
180
|
+
/** Scroll the feed to the bottom. */
|
|
181
|
+
scrollToBottom(): void;
|
|
182
|
+
/** Scroll the feed by a delta (positive = down, negative = up). */
|
|
183
|
+
scrollFeed(delta: number): void;
|
|
184
|
+
/** Get current input value. */
|
|
185
|
+
get inputValue(): string;
|
|
186
|
+
/** Set the input value and move cursor to end. */
|
|
187
|
+
set inputValue(text: string);
|
|
188
|
+
/** Get the underlying TextInput for advanced use. */
|
|
189
|
+
get input(): TextInput;
|
|
190
|
+
/** Get input history. */
|
|
191
|
+
get history(): string[];
|
|
192
|
+
/** Set the input prompt text. */
|
|
193
|
+
set prompt(text: string);
|
|
194
|
+
get prompt(): string;
|
|
195
|
+
/** Show a progress/status message just above the separator. */
|
|
196
|
+
setProgress(content: StyledLine | null): void;
|
|
197
|
+
/** Show dropdown items below the input box. */
|
|
198
|
+
showDropdown(items: DropdownItem[]): void;
|
|
199
|
+
/** Hide the dropdown. */
|
|
200
|
+
hideDropdown(): void;
|
|
201
|
+
/** Move dropdown selection down. */
|
|
202
|
+
dropdownDown(): boolean;
|
|
203
|
+
/** Move dropdown selection up. */
|
|
204
|
+
dropdownUp(): boolean;
|
|
205
|
+
/** Accept the currently highlighted dropdown item. Returns it, or null. */
|
|
206
|
+
acceptDropdownItem(): DropdownItem | null;
|
|
207
|
+
/** Get current dropdown items. */
|
|
208
|
+
get dropdownItems(): DropdownItem[];
|
|
209
|
+
/** Get current dropdown selection index. */
|
|
210
|
+
get dropdownIndex(): number;
|
|
211
|
+
/**
|
|
212
|
+
* Replace the normal input/footer area with a custom widget
|
|
213
|
+
* (e.g. an Interview). While an override is active the normal
|
|
214
|
+
* input, separator, footer and dropdown are hidden and input
|
|
215
|
+
* events are routed to the override widget.
|
|
216
|
+
*
|
|
217
|
+
* Pass `null` to remove the override and restore normal input.
|
|
218
|
+
*/
|
|
219
|
+
setInputOverride(widget: Control | null): void;
|
|
220
|
+
/** Get the current input override widget, or null. */
|
|
221
|
+
get inputOverride(): Control | null;
|
|
222
|
+
handleInput(event: InputEvent): boolean;
|
|
223
|
+
/** Extract the plain text content of a feed line. */
|
|
224
|
+
private _extractFeedLineText;
|
|
225
|
+
/** Find the URL at the given character offset, if any. */
|
|
226
|
+
private _findUrlAtOffset;
|
|
227
|
+
/** Resolve which action item the mouse x-position falls on. */
|
|
228
|
+
private _resolveActionItem;
|
|
229
|
+
/** Build a hover line: highlight only the target item, keep others normal. */
|
|
230
|
+
private _buildHoverLine;
|
|
231
|
+
/** Get the plain text length of a StyledLine. */
|
|
232
|
+
private _spanTextLength;
|
|
233
|
+
measure(constraint: Constraint): Size;
|
|
234
|
+
arrange(rect: Rect): void;
|
|
235
|
+
render(ctx: DrawingContext): void;
|
|
236
|
+
private _renderFeed;
|
|
237
|
+
private _renderDropdown;
|
|
238
|
+
private _autoScrollToBottom;
|
|
239
|
+
}
|