@fx-/chat-ink 0.0.0-stage → 0.1.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.
@@ -0,0 +1,383 @@
1
+ import { CustomBlock, DetailLevel, RenderableMessage } from "@fx-/chat-core";
2
+
3
+ //#region src/lines/measure.d.ts
4
+ type Measure = (text: string) => number;
5
+ /**
6
+ * `width`'s results (default: `string-width`, which Ink measures with), with
7
+ * each distinct grapheme cluster measured once. The package sums independent
8
+ * per-cluster widths, so caching changes no result; on Bun its emoji check is
9
+ * pathologically slow and the same few clusters are measured over and over.
10
+ * Text with ANSI escapes goes to `width` as it is (stripping spans clusters).
11
+ * The cache is bounded and starts over when full.
12
+ */
13
+ declare function cachedMeasure(width?: Measure, limit?: number): Measure;
14
+ declare const defaultMeasure: Measure;
15
+ /** Grapheme clusters of `text` (so a wide or combined character is never split). */
16
+ declare function graphemes(text: string): string[];
17
+ /** Cut `text` to at most `cells`, ending with `…` when cut. */
18
+ declare function truncate(text: string, cells: number, measure?: Measure): string;
19
+ /** Split `text` into pieces of at most `cells` each, never inside a grapheme. */
20
+ declare function hardSplit(text: string, cells: number, measure?: Measure): string[];
21
+ /** One wrapped row and how it joins the previous row when copied. */
22
+ interface WrappedRow {
23
+ text: string;
24
+ wrap?: 'soft' | 'hard';
25
+ }
26
+ /**
27
+ * Wrap one paragraph (no newlines) to `width` cells at spaces. A word longer
28
+ * than the line is split with a `hard` join; other continuation rows are
29
+ * `soft` (a copy rejoins them with a space).
30
+ */
31
+ declare function wrapText(text: string, width: number, measure?: Measure): WrappedRow[];
32
+ /** Wrap text with newlines: each line wrapped on its own (hard line breaks stay). */
33
+ declare function wrapLines(text: string, width: number, measure?: Measure): WrappedRow[];
34
+
35
+ //#endregion
36
+ //#region src/theme.d.ts
37
+ /**
38
+ * The injected look of the terminal renderer.
39
+ *
40
+ * Like `@fx-/chat`'s `classNames`, the package decides structure and semantics
41
+ * and the host decides appearance: every piece of text names its ROLE and the
42
+ * theme answers with an appearance. Glyphs (icons, disclosure markers, spinner
43
+ * frames) are injected the same way. The defaults are greyscale and plain, so
44
+ * an un-themed transcript reads correctly on any terminal.
45
+ */
46
+ /** Text attributes for one role. An absent attribute is not applied. */
47
+ interface InkAppearance {
48
+ /** Any Ink/chalk colour name or hex string. */
49
+ color?: string;
50
+ dim?: boolean;
51
+ bold?: boolean;
52
+ italic?: boolean;
53
+ inverse?: boolean;
54
+ underline?: boolean;
55
+ strikethrough?: boolean;
56
+ }
57
+ /** The semantic roles text is drawn in. */
58
+ type InkThemeRole =
59
+ /** Ordinary answer text. */
60
+ 'content'
61
+ /** Secondary text: timers, metrics, hints, reasoning bodies. */
62
+ | 'muted'
63
+ /** Emphasis: group/row labels. */
64
+ | 'strong'
65
+ /** Markers: glyphs, disclosure carets, `+N` counts. */
66
+ | 'marker'
67
+ /** Frame and separators (composer border). */
68
+ | 'chrome'
69
+ /** The composer cursor. */
70
+ | 'cursor' | 'positive' | 'caution' | 'danger'
71
+ /** The user's own messages. */
72
+ | 'user'
73
+ /** The assistant's role label. */
74
+ | 'assistant'
75
+ /** Per-message accent: frame gutters, footer corners, the live pulse. */
76
+ | 'accent'
77
+ /** Code spans and blocks. */
78
+ | 'code'
79
+ /** Links. */
80
+ | 'link'
81
+ /** Selected text (mouse selection). */
82
+ | 'selection';
83
+ type InkTheme = Record<InkThemeRole, InkAppearance>;
84
+ /** Greyscale defaults: no hue anywhere. */
85
+ declare const plainTheme: InkTheme;
86
+ /** Glyphs used by the renderer. */
87
+ interface InkGlyphs {
88
+ collapsed: string;
89
+ expanded: string;
90
+ /** Kind icons, by the core summary icon name. */
91
+ brain: string;
92
+ wrench: string;
93
+ gear: string;
94
+ checklist: string;
95
+ /** Plan entry markers by status (a live in-progress entry draws the spinner). */
96
+ planDone: string;
97
+ planActive: string;
98
+ planPending: string;
99
+ success: string;
100
+ error: string;
101
+ /** Prefix of the user's messages. */
102
+ user: string;
103
+ /** Prefix of the assistant's messages. */
104
+ assistant: string;
105
+ /** Prompt shown before the composer input. */
106
+ prompt: string;
107
+ /** Spinner animation frames. */
108
+ spinner: readonly string[];
109
+ /** Between footer facts and status facts. */
110
+ separator: string;
111
+ /** @deprecated No longer drawn (the live tail's row uses `earlier`); kept for existing glyph sets. */
112
+ tool: string;
113
+ /** Before the live tail's row counting hidden earlier activity (empty: no marker). */
114
+ earlier: string;
115
+ /** Tool row status markers (a live call draws the spinner). */
116
+ toolSuccess: string;
117
+ toolError: string;
118
+ /** @deprecated No longer drawn (a live call draws the spinner); kept for existing glyph sets. */
119
+ toolRunning: string;
120
+ toolIdle: string;
121
+ /** Frames: boxes, gutters and footer corner. */
122
+ boxTopLeft: string;
123
+ boxTopRight: string;
124
+ boxBottomLeft: string;
125
+ boxBottomRight: string;
126
+ boxJunction: string;
127
+ boxHorizontal: string;
128
+ boxVertical: string;
129
+ footer: string;
130
+ /** The dotted bar before tool output lines. */
131
+ outputBar: string;
132
+ attachment: string;
133
+ image: string;
134
+ /** Queued (pending) message rows. */
135
+ pending: string;
136
+ /** Live pulse frames for the status line. */
137
+ pulse: readonly string[];
138
+ /** Markdown: list bullets, task markers, rules, quote/code bars, table joints. */
139
+ bullet: string;
140
+ taskOpen: string;
141
+ taskDone: string;
142
+ rule: string;
143
+ quoteBar: string;
144
+ codeBar: string;
145
+ tableColumn: string;
146
+ tableCross: string;
147
+ }
148
+ /**
149
+ * The default glyphs: monochrome text, one cell each. No glyph is
150
+ * emoji-capable (Unicode `Emoji=No`), so terminals cannot widen or colour one,
151
+ * and every glyph is one that common monospace fonts carry.
152
+ */
153
+ declare const plainGlyphs: InkGlyphs;
154
+ /** Glyphs using only ASCII, for terminals without box-drawing or emoji symbols. */
155
+ declare const asciiGlyphs: InkGlyphs;
156
+ /**
157
+ * Opt-in colour icons over {@link plainGlyphs}: only code points that are
158
+ * emoji by default (`Emoji_Presentation=Yes`), which terminals and the
159
+ * default measure both treat as two cells. Turn it on with
160
+ * `<InkChatProvider components={{ glyphs: emojiGlyphs }}>`.
161
+ */
162
+ declare const emojiGlyphs: InkGlyphs;
163
+
164
+ //#endregion
165
+ //#region src/lines/model.d.ts
166
+ interface Span {
167
+ text: string;
168
+ /** Theme role, resolved at render time. */
169
+ role?: InkThemeRole;
170
+ /** An explicit appearance (e.g. a per-message accent), applied over the role. */
171
+ style?: InkAppearance;
172
+ bold?: boolean;
173
+ italic?: boolean;
174
+ dim?: boolean;
175
+ underline?: boolean;
176
+ strike?: boolean;
177
+ inverse?: boolean;
178
+ /** A literal colour, only where no role can say it. */
179
+ color?: string;
180
+ /** Layout (borders, gutters, indents, bars): drawn, never copied. */
181
+ chrome?: boolean;
182
+ /** Copied as this string once per character instead of as itself. */
183
+ copyEach?: string;
184
+ }
185
+ /** What a click on a line acts on. */
186
+ type LineTarget = {
187
+ kind: 'tool-output';
188
+ blockId: string;
189
+ } | {
190
+ kind: 'json-toggle';
191
+ blockId: string;
192
+ path: string;
193
+ open: boolean;
194
+ } | {
195
+ kind: 'image';
196
+ src: string;
197
+ } | {
198
+ kind: 'row-toggle';
199
+ rowKey: string;
200
+ };
201
+ interface Line {
202
+ spans: Span[];
203
+ /** How this line joins the previous one in a copy: a space (`soft`) or nothing (`hard`). */
204
+ wrap?: 'soft' | 'hard';
205
+ /** Pure decoration (rules, frames, footers): skipped by copy. */
206
+ frame?: boolean;
207
+ /** Which message (and block) produced it. */
208
+ owner: {
209
+ messageId: string;
210
+ blockId?: string;
211
+ };
212
+ target?: LineTarget;
213
+ }
214
+ /** A span. */
215
+ declare function span(text: string, extra?: Omit<Span, 'text'>): Span;
216
+ /** Prepend spans, keeping the line's metadata. */
217
+ declare function prefix(line: Line, spans: Span[]): Line;
218
+ /** Append spans, keeping the line's metadata. */
219
+ declare function suffix(line: Line, spans: Span[]): Line;
220
+ /** The line's text as drawn. */
221
+ declare function lineText(line: Line): string;
222
+
223
+ //#endregion
224
+ //#region src/lines/layout.d.ts
225
+ /** A line before its owner is assigned. */
226
+ type LineDraft = Omit<Line, 'owner'> & {
227
+ owner?: Line['owner'];
228
+ };
229
+ /** Host labels for the keys that act on a message (empty by default). */
230
+ interface FooterHints {
231
+ /** Shown with an outcome, e.g. `^R again`. */
232
+ retry?: string;
233
+ /** Collapsed with activity, e.g. `^T details`. */
234
+ details?: string;
235
+ /** In details with tool output, e.g. `^T open all`. */
236
+ openAll?: string;
237
+ /** In details or full, e.g. `Esc close`. */
238
+ close?: string;
239
+ }
240
+ /** Open state of foldable rows and JSON members, per block (or row key) and path. */
241
+ interface OpenState {
242
+ version: number;
243
+ isOpen(blockId: string, path: string): boolean;
244
+ }
245
+ declare const NOTHING_OPEN: OpenState;
246
+ /** The path a collapsible row's open state is kept under. */
247
+ declare const ROW_PATH = "#row";
248
+ /** A markdown layout: source → lines (owners are assigned by the caller). */
249
+ type MarkdownLayout = (source: string, width: number, options: {
250
+ streaming?: boolean;
251
+ measure: Measure;
252
+ glyphs: InkGlyphs;
253
+ /** Lays out a line that is only an image, when the host draws images. */
254
+ image?: LayoutContext['image'];
255
+ }) => LineDraft[];
256
+ /** The default markdown layout: plain text, wrapped, in the `content` role. */
257
+ declare const plainMarkdownLayout: MarkdownLayout;
258
+ /** Context for a host's custom block layout. */
259
+ interface BlockLayoutContext {
260
+ width: number;
261
+ isLive: boolean;
262
+ measure: Measure;
263
+ }
264
+ interface LayoutContext {
265
+ width: number;
266
+ detail: DetailLevel;
267
+ placement: 'inline' | 'after-content';
268
+ /** Newest tool rows a collapsed live message shows (default 3). */
269
+ liveTail: number;
270
+ isActive: (message: RenderableMessage) => boolean;
271
+ /** Whole-second clock for live facts and timers; only live messages read it. */
272
+ now: number;
273
+ opened: OpenState;
274
+ markdown: MarkdownLayout;
275
+ glyphs: InkGlyphs;
276
+ theme: InkTheme;
277
+ hints: FooterHints;
278
+ /** Footer on a live message (default true in the viewport). */
279
+ liveFooter: boolean;
280
+ blockLayouts?: Record<string, (block: CustomBlock, ctx: BlockLayoutContext) => LineDraft[]>;
281
+ frame: MessageFrame;
282
+ accentFor?: ((message: RenderableMessage) => InkThemeRole | InkAppearance) | undefined;
283
+ measure: Measure;
284
+ /** Lays out a block image line, when the host draws images. */
285
+ image?: ((src: string, alt: string, width: number) => LineDraft[] | undefined) | undefined;
286
+ /** Host facts after a message's own footer facts (e.g. `turn 42` in details). */
287
+ footerExtras?: ((message: RenderableMessage, detail: DetailLevel) => string[]) | undefined;
288
+ /**
289
+ * A finished answer's activity when collapsed: its own rows (default), or
290
+ * only the footer's counts (every activity row is dropped, in either
291
+ * placement, when the answer has a footer).
292
+ */
293
+ finishedActivity?: 'row' | 'footer' | undefined;
294
+ /**
295
+ * A blank decoration line between content and an adjacent activity row or
296
+ * group (default true). Consecutive activity rows stay together.
297
+ */
298
+ activitySpacing?: boolean | undefined;
299
+ }
300
+ /** What a frame needs to wrap a laid-out message. */
301
+ interface FrameInput {
302
+ message: RenderableMessage;
303
+ body: LineDraft[];
304
+ /** Footer spans (facts and hints), when the message has a footer. */
305
+ footer?: Span[];
306
+ /** Whether this message shows anything. */
307
+ shows: boolean;
308
+ /** Whether the next message (an answer) shows — a frame junction may hang from it. */
309
+ nextShows: boolean;
310
+ live: boolean;
311
+ /** The message's accent as an explicit appearance. */
312
+ accent: InkAppearance;
313
+ ctx: LayoutContext;
314
+ }
315
+ /** A message frame: the injected look around a message. */
316
+ interface MessageFrame {
317
+ /** Stable id (part of the layout cache key). */
318
+ id: string;
319
+ /** Width available to the message body inside the frame. */
320
+ innerWidth(message: RenderableMessage, width: number): number;
321
+ layout(input: FrameInput): LineDraft[];
322
+ }
323
+ /** Resolve an accent to an explicit appearance. */
324
+ declare function resolveAccent(message: RenderableMessage, ctx: Pick<LayoutContext, 'accentFor' | 'theme'>): InkAppearance;
325
+ /** Lay out a message's body (no frame) at `w` cells. */
326
+ declare function layoutBody(original: RenderableMessage, w: number, ctx: LayoutContext): LineDraft[];
327
+ /** The footer spans of a message: planned facts with injected glyphs, then hints. */
328
+ declare function footerSpans(original: RenderableMessage, ctx: LayoutContext): Span[] | undefined;
329
+ /** Whether an answer shows anything: content, reasoning, an outcome, a tool call or a plan. */
330
+ declare function showsSomething(message: RenderableMessage): boolean;
331
+ /** Lay out one message inside its frame. */
332
+ declare function layoutMessage(original: RenderableMessage, ctx: LayoutContext, next?: RenderableMessage): Line[];
333
+
334
+ //#endregion
335
+ //#region src/lines/images.d.ts
336
+ /** A cell box: columns × rows. */
337
+ interface CellBox {
338
+ columns: number;
339
+ rows: number;
340
+ }
341
+ type ImageState = {
342
+ kind: 'ask';
343
+ } | {
344
+ kind: 'loading';
345
+ } | {
346
+ kind: 'failed';
347
+ error: string;
348
+ }
349
+ /** One string per cell row, drawn as text in `color`. */
350
+ | {
351
+ kind: 'ready';
352
+ rows: string[];
353
+ color?: string;
354
+ };
355
+ interface ImageProvider {
356
+ /** The state for a source in a box, or undefined for "not mine — render as text". */
357
+ get(src: string, box: CellBox): ImageState | undefined;
358
+ /** Allow an `ask` source (the user clicked it). */
359
+ allow(src: string): void;
360
+ /** Bumped on any change; part of the layout cache key. */
361
+ readonly version: number;
362
+ /** Called on every change; returns an unsubscribe. */
363
+ subscribe(listener: () => void): () => void;
364
+ /** A terminal query the viewport writes once (e.g. the cell size in pixels). */
365
+ readonly query?: string;
366
+ /** Recognise the terminal's reply to the query; returns whether `input` was one. */
367
+ takeReply?(input: string): boolean;
368
+ }
369
+ /** The largest thumbnail, in cells. */
370
+ declare const THUMBNAIL: CellBox;
371
+ interface ImageLayoutOptions {
372
+ provider?: ImageProvider | undefined;
373
+ expanded: boolean;
374
+ /** Rows an expanded image may take. */
375
+ maxHeight: number;
376
+ glyphs: InkGlyphs;
377
+ measure: Measure;
378
+ }
379
+ /** Lay out a block image: its ready rows, then a wrapped caption; every line targets the source. */
380
+ declare function layoutImage(src: string, alt: string, width: number, { provider, expanded, maxHeight, glyphs, measure }: ImageLayoutOptions): LineDraft[];
381
+
382
+ //#endregion
383
+ export { BlockLayoutContext, CellBox, FooterHints, FrameInput, ImageLayoutOptions, ImageProvider, ImageState, InkAppearance, InkGlyphs, InkTheme, InkThemeRole, LayoutContext, Line, LineDraft, LineTarget, MarkdownLayout, Measure, MessageFrame, NOTHING_OPEN, OpenState, ROW_PATH, Span, THUMBNAIL, WrappedRow, asciiGlyphs, cachedMeasure, defaultMeasure, emojiGlyphs, footerSpans, graphemes, hardSplit, layoutBody, layoutImage, layoutMessage, lineText, plainGlyphs, plainMarkdownLayout, plainTheme, prefix, resolveAccent, showsSomething, span, suffix, truncate, wrapLines, wrapText };