@alisio/alisio-code 0.1.0-alpha.9 → 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.
Files changed (56) hide show
  1. package/README.md +95 -16
  2. package/dist/analysis.d.ts +27 -0
  3. package/dist/analysis.js +145 -0
  4. package/dist/banner.js +2 -1
  5. package/dist/builtin.d.ts +3 -1
  6. package/dist/builtin.js +0 -24
  7. package/dist/main.js +133 -6
  8. package/dist/serve.d.ts +15 -0
  9. package/dist/serve.js +99 -0
  10. package/dist/tui/agent-manager.d.ts +105 -0
  11. package/dist/tui/agent-manager.js +269 -0
  12. package/dist/tui/agents.d.ts +5 -0
  13. package/dist/tui/agents.js +5 -0
  14. package/dist/tui/app.d.ts +42 -1
  15. package/dist/tui/app.js +1895 -97
  16. package/dist/tui/artifact-preview.d.ts +52 -0
  17. package/dist/tui/artifact-preview.js +212 -0
  18. package/dist/tui/artifacts.d.ts +54 -0
  19. package/dist/tui/artifacts.js +207 -0
  20. package/dist/tui/btw.d.ts +57 -0
  21. package/dist/tui/btw.js +76 -0
  22. package/dist/tui/components.d.ts +174 -12
  23. package/dist/tui/components.js +962 -66
  24. package/dist/tui/connect-input.d.ts +2 -0
  25. package/dist/tui/connect-input.js +1 -1
  26. package/dist/tui/exit.d.ts +10 -0
  27. package/dist/tui/exit.js +15 -0
  28. package/dist/tui/git-branch.d.ts +48 -0
  29. package/dist/tui/git-branch.js +99 -0
  30. package/dist/tui/goal.d.ts +75 -0
  31. package/dist/tui/goal.js +171 -0
  32. package/dist/tui/modes.d.ts +71 -0
  33. package/dist/tui/modes.js +90 -0
  34. package/dist/tui/open-path.d.ts +38 -0
  35. package/dist/tui/open-path.js +80 -0
  36. package/dist/tui/plan-review.d.ts +48 -0
  37. package/dist/tui/plan-review.js +76 -0
  38. package/dist/tui/queue.d.ts +2 -0
  39. package/dist/tui/queue.js +5 -1
  40. package/dist/tui/scroll.d.ts +8 -0
  41. package/dist/tui/scroll.js +12 -0
  42. package/dist/tui/settings-menu.d.ts +40 -0
  43. package/dist/tui/settings-menu.js +132 -0
  44. package/dist/tui/settings.d.ts +194 -0
  45. package/dist/tui/settings.js +482 -0
  46. package/dist/tui/state.d.ts +343 -7
  47. package/dist/tui/state.js +823 -54
  48. package/dist/tui/tasks.d.ts +123 -0
  49. package/dist/tui/tasks.js +369 -0
  50. package/dist/tui/theme.d.ts +42 -1
  51. package/dist/tui/theme.js +516 -1
  52. package/dist/tui/viewer-state.d.ts +7 -0
  53. package/dist/tui/viewer-state.js +36 -0
  54. package/dist/version.d.ts +7 -5
  55. package/dist/version.js +44 -8
  56. package/package.json +7 -9
@@ -0,0 +1,123 @@
1
+ /**
2
+ * `/tasks`: the terminal panel of the session's background tasks. A list (shell tasks started with
3
+ * `bg_run`, plus a read-only mirror of the subagents), a detail view that follows the live output
4
+ * and the user's stop (asked to confirm: the task is not recoverable).
5
+ *
6
+ * The logic is split like the other panels: pure helpers and a reducer (`reduceTasksPanel`), and a
7
+ * thin `TasksView` component that owns the polling. The view never talks to a process: it reads
8
+ * and stops through the `TasksDeps` the app wires to `app.tasks`.
9
+ */
10
+ import type { BackgroundTaskInfo, BackgroundTaskOutput } from "@alisio/sdk";
11
+ import { type Component } from "@earendil-works/pi-tui";
12
+ /** Characters of output kept in memory by the view (the stored log can be much larger). */
13
+ export declare const VIEW_OUTPUT_CAP = 200000;
14
+ export declare const isLiveTask: (task: Pick<BackgroundTaskInfo, "status">) => boolean;
15
+ /** `3 s`, `2 min 5 s`, `1 h 2 min`. */
16
+ export declare function formatElapsed(ms: number): string;
17
+ /** The status as shown, with the reason when there is one (`failed · time limit`). */
18
+ export declare function statusText(task: BackgroundTaskInfo): string;
19
+ /** Removes terminal escape sequences and control characters so output cannot move the cursor. */
20
+ export declare function sanitizeOutput(text: string): string;
21
+ /** Appends a chunk to the kept output; older text beyond the cap is dropped. */
22
+ export declare function appendKept(current: string, chunk: string, cap?: number): string;
23
+ /** One list row: `label status kind elapsed size`. */
24
+ export declare function taskRow(task: BackgroundTaskInfo, now: number, selected: boolean): string;
25
+ /**
26
+ * Whether the TUI can take the wake-up of a finished background task now. `skip` only while
27
+ * exiting; everything else waits (`busy`) and the core retries: a turn is running, a reload is
28
+ * in progress, a question or approval is on screen, or another session is open (the notification
29
+ * is for the session that started the task and waits until the user is back in it).
30
+ */
31
+ export declare function wakeDecision(state: {
32
+ exiting: boolean;
33
+ reloading: boolean;
34
+ busy: boolean;
35
+ promptPending: boolean;
36
+ openRoot: string;
37
+ ownerRoot: string;
38
+ }): "start" | "busy" | "skip";
39
+ export interface TasksPanelState {
40
+ view: "list" | "detail";
41
+ cursor: number;
42
+ /** Detail view: the task shown. */
43
+ selected?: string;
44
+ /** Detail view: first visible output line, or `undefined` while following the end. */
45
+ scroll?: number;
46
+ /** A stop waits for `y`/`n` (the id of the task). */
47
+ confirm?: string;
48
+ }
49
+ export type TasksAction = {
50
+ type: "up" | "down" | "pageUp" | "pageDown" | "home" | "end";
51
+ } | {
52
+ type: "enter" | "escape" | "stop" | "yes" | "no" | "quit";
53
+ };
54
+ export type TasksEffect = {
55
+ type: "close";
56
+ } | {
57
+ type: "open";
58
+ id: string;
59
+ } | {
60
+ type: "back";
61
+ } | {
62
+ type: "stop";
63
+ id: string;
64
+ } | {
65
+ type: "notice";
66
+ text: string;
67
+ };
68
+ export declare const initialTasksState: () => TasksPanelState;
69
+ /**
70
+ * Pure reducer. `tasks` is the current list (newest first), `lines` the number of output lines
71
+ * of the open task and `page` the rows that fit on screen. Only a LIVE SHELL task can be stopped.
72
+ */
73
+ export declare function reduceTasksPanel(state: TasksPanelState, action: TasksAction, context: {
74
+ tasks: BackgroundTaskInfo[];
75
+ lines: number;
76
+ page: number;
77
+ }): {
78
+ state: TasksPanelState;
79
+ effect?: TasksEffect;
80
+ };
81
+ export interface TasksDeps {
82
+ /** The tasks of the session, newest first. */
83
+ list(): BackgroundTaskInfo[];
84
+ read(id: string, offset: number): Promise<BackgroundTaskOutput>;
85
+ stop(id: string): Promise<unknown>;
86
+ /** Terminal rows available for the panel. */
87
+ rows(): number;
88
+ requestRender(): void;
89
+ /** Closes the panel (the app puts the editor back). */
90
+ close(): void;
91
+ /** Whether this panel is still the one on screen (a stale timer stops itself). */
92
+ active(): boolean;
93
+ notice(text: string): void;
94
+ now?(): number;
95
+ }
96
+ /** The `/tasks` panel component. */
97
+ export declare class TasksView implements Component {
98
+ private readonly deps;
99
+ private state;
100
+ private text;
101
+ private offset;
102
+ private eof;
103
+ private lineCache;
104
+ private timer;
105
+ private reading;
106
+ private disposed;
107
+ private error;
108
+ constructor(deps: TasksDeps);
109
+ /** Starts the refresh timer (one second): the list and the output of the open task. */
110
+ start(): void;
111
+ dispose(): void;
112
+ invalidate(): void;
113
+ private now;
114
+ private selectedTask;
115
+ private page;
116
+ private outputLines;
117
+ /** Reads what the open task wrote since the last read, and repaints. */
118
+ refresh(): Promise<void>;
119
+ private open;
120
+ handleInput(data: string): void;
121
+ private actionFor;
122
+ render(width: number): string[];
123
+ }
@@ -0,0 +1,369 @@
1
+ import { Key, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { style } from "./theme.js";
3
+ /** Characters of output kept in memory by the view (the stored log can be much larger). */
4
+ export const VIEW_OUTPUT_CAP = 200_000;
5
+ const POLL_MS = 1_000;
6
+ const LIVE = new Set(["queued", "running", "stopping"]);
7
+ export const isLiveTask = (task) => LIVE.has(task.status);
8
+ /** `3 s`, `2 min 5 s`, `1 h 2 min`. */
9
+ export function formatElapsed(ms) {
10
+ const seconds = Math.max(0, Math.round(ms / 1000));
11
+ if (seconds < 60)
12
+ return `${seconds} s`;
13
+ const minutes = Math.floor(seconds / 60);
14
+ if (minutes < 60)
15
+ return `${minutes} min${seconds % 60 ? ` ${seconds % 60} s` : ""}`;
16
+ const hours = Math.floor(minutes / 60);
17
+ return `${hours} h${minutes % 60 ? ` ${minutes % 60} min` : ""}`;
18
+ }
19
+ const STATUS_LABEL = {
20
+ queued: "queued",
21
+ running: "running",
22
+ stopping: "stopping",
23
+ succeeded: "done",
24
+ failed: "failed",
25
+ cancelled: "stopped",
26
+ lost: "lost",
27
+ };
28
+ /** The status as shown, with the reason when there is one (`failed · time limit`). */
29
+ export function statusText(task) {
30
+ const base = STATUS_LABEL[task.status];
31
+ if (task.errorCode === "timeout")
32
+ return `${base} · time limit`;
33
+ if (task.status === "failed" && task.exitCode !== undefined)
34
+ return `${base} · exit ${task.exitCode}`;
35
+ if (task.status === "cancelled" && task.abortOrigin)
36
+ return `${base} · by ${task.abortOrigin === "timeout" ? "time limit" : task.abortOrigin}`;
37
+ return base;
38
+ }
39
+ /** Removes terminal escape sequences and control characters so output cannot move the cursor. */
40
+ export function sanitizeOutput(text) {
41
+ return (text
42
+ // OSC (title, hyperlinks) up to BEL or ST, then CSI and two-character escapes.
43
+ .replace(/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g, "")
44
+ .replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, "")
45
+ .replace(/\u001b[@-Z\\-_]/g, "")
46
+ .replace(/\r\n/g, "\n")
47
+ // A carriage return rewrites the line: keep what was written last.
48
+ .replace(/[^\n]*\r(?!\n)/g, "")
49
+ .replace(/\t/g, " ")
50
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, ""));
51
+ }
52
+ /** Appends a chunk to the kept output; older text beyond the cap is dropped. */
53
+ export function appendKept(current, chunk, cap = VIEW_OUTPUT_CAP) {
54
+ const joined = current + chunk;
55
+ return joined.length > cap ? joined.slice(joined.length - cap) : joined;
56
+ }
57
+ /** One list row: `label status kind elapsed size`. */
58
+ export function taskRow(task, now, selected) {
59
+ const ran = task.startedAt ? formatElapsed((task.endedAt ?? now) - task.startedAt) : "";
60
+ const bits = [
61
+ statusText(task),
62
+ task.kind === "subagent" ? "subagent" : "",
63
+ ran,
64
+ task.kind === "shell" && task.bytes ? `${Math.max(1, Math.round(task.bytes / 1024))} KB` : "",
65
+ ].filter(Boolean);
66
+ return `${selected ? "›" : " "} ${task.label} ${bits.join(" · ")}`;
67
+ }
68
+ /**
69
+ * Whether the TUI can take the wake-up of a finished background task now. `skip` only while
70
+ * exiting; everything else waits (`busy`) and the core retries: a turn is running, a reload is
71
+ * in progress, a question or approval is on screen, or another session is open (the notification
72
+ * is for the session that started the task and waits until the user is back in it).
73
+ */
74
+ export function wakeDecision(state) {
75
+ if (state.exiting)
76
+ return "skip";
77
+ if (state.reloading || state.busy || state.promptPending || state.openRoot !== state.ownerRoot)
78
+ return "busy";
79
+ return "start";
80
+ }
81
+ export const initialTasksState = () => ({ view: "list", cursor: 0 });
82
+ /**
83
+ * Pure reducer. `tasks` is the current list (newest first), `lines` the number of output lines
84
+ * of the open task and `page` the rows that fit on screen. Only a LIVE SHELL task can be stopped.
85
+ */
86
+ export function reduceTasksPanel(state, action, context) {
87
+ const { tasks, lines, page } = context;
88
+ if (state.confirm) {
89
+ if (action.type === "yes")
90
+ return {
91
+ state: { ...state, confirm: undefined },
92
+ effect: { type: "stop", id: state.confirm },
93
+ };
94
+ return { state: { ...state, confirm: undefined } };
95
+ }
96
+ if (action.type === "quit")
97
+ return { state, effect: { type: "close" } };
98
+ if (state.view === "list") {
99
+ const last = Math.max(0, tasks.length - 1);
100
+ switch (action.type) {
101
+ case "up":
102
+ return { state: { ...state, cursor: Math.max(0, state.cursor - 1) } };
103
+ case "down":
104
+ return { state: { ...state, cursor: Math.min(last, state.cursor + 1) } };
105
+ case "home":
106
+ return { state: { ...state, cursor: 0 } };
107
+ case "end":
108
+ return { state: { ...state, cursor: last } };
109
+ case "enter": {
110
+ const task = tasks[Math.min(state.cursor, last)];
111
+ if (!task)
112
+ return { state };
113
+ return {
114
+ state: { ...state, view: "detail", selected: task.id, scroll: undefined },
115
+ effect: { type: "open", id: task.id },
116
+ };
117
+ }
118
+ case "escape":
119
+ return { state, effect: { type: "close" } };
120
+ case "stop": {
121
+ const task = tasks[Math.min(state.cursor, last)];
122
+ if (!task)
123
+ return { state };
124
+ if (task.kind !== "shell")
125
+ return {
126
+ state,
127
+ effect: { type: "notice", text: "Subagents are managed from the agents panel." },
128
+ };
129
+ return isLiveTask(task) ? { state: { ...state, confirm: task.id } } : { state };
130
+ }
131
+ default:
132
+ return { state };
133
+ }
134
+ }
135
+ // Detail view.
136
+ const maxScroll = Math.max(0, lines - page);
137
+ const current = state.scroll ?? maxScroll;
138
+ const to = (scroll) => ({
139
+ ...state,
140
+ scroll: scroll >= maxScroll ? undefined : Math.max(0, scroll),
141
+ });
142
+ switch (action.type) {
143
+ case "up":
144
+ return { state: to(current - 1) };
145
+ case "down":
146
+ return { state: to(current + 1) };
147
+ case "pageUp":
148
+ return { state: to(current - Math.max(1, page - 1)) };
149
+ case "pageDown":
150
+ return { state: to(current + Math.max(1, page - 1)) };
151
+ case "home":
152
+ return { state: to(0) };
153
+ case "end":
154
+ return { state: { ...state, scroll: undefined } };
155
+ case "escape":
156
+ case "enter":
157
+ return {
158
+ state: { ...state, view: "list", selected: undefined, scroll: undefined },
159
+ effect: { type: "back" },
160
+ };
161
+ case "stop": {
162
+ const task = tasks.find((item) => item.id === state.selected);
163
+ if (!task || task.kind !== "shell" || !isLiveTask(task))
164
+ return { state };
165
+ return { state: { ...state, confirm: task.id } };
166
+ }
167
+ default:
168
+ return { state };
169
+ }
170
+ }
171
+ /** The `/tasks` panel component. */
172
+ export class TasksView {
173
+ deps;
174
+ state = initialTasksState();
175
+ text = "";
176
+ offset = 0;
177
+ eof = false;
178
+ lineCache = [];
179
+ timer;
180
+ reading = false;
181
+ disposed = false;
182
+ error;
183
+ constructor(deps) {
184
+ this.deps = deps;
185
+ }
186
+ /** Starts the refresh timer (one second): the list and the output of the open task. */
187
+ start() {
188
+ this.timer = setInterval(() => {
189
+ if (!this.deps.active())
190
+ return this.dispose();
191
+ void this.refresh();
192
+ }, POLL_MS);
193
+ this.timer.unref?.();
194
+ }
195
+ dispose() {
196
+ this.disposed = true;
197
+ if (this.timer)
198
+ clearInterval(this.timer);
199
+ this.timer = undefined;
200
+ }
201
+ invalidate() { }
202
+ now() {
203
+ return (this.deps.now ?? Date.now)();
204
+ }
205
+ selectedTask() {
206
+ return this.deps.list().find((task) => task.id === this.state.selected);
207
+ }
208
+ page() {
209
+ return Math.max(3, this.deps.rows() - 9);
210
+ }
211
+ outputLines() {
212
+ return this.lineCache;
213
+ }
214
+ /** Reads what the open task wrote since the last read, and repaints. */
215
+ async refresh() {
216
+ if (this.disposed)
217
+ return;
218
+ const task = this.selectedTask();
219
+ if (this.state.view === "detail" &&
220
+ task &&
221
+ task.kind === "shell" &&
222
+ !this.reading &&
223
+ !this.eof) {
224
+ this.reading = true;
225
+ try {
226
+ // Keep reading while the task fills the buffer faster than one read returns.
227
+ for (let i = 0; i < 8; i++) {
228
+ const out = await this.deps.read(task.id, this.offset);
229
+ if (this.disposed || this.state.selected !== task.id)
230
+ break;
231
+ this.error = undefined;
232
+ this.offset = out.nextOffset;
233
+ this.eof = out.eof;
234
+ if (out.text) {
235
+ this.text = appendKept(this.text, sanitizeOutput(out.text));
236
+ this.lineCache = this.text.split("\n");
237
+ if (this.lineCache.at(-1) === "")
238
+ this.lineCache.pop();
239
+ }
240
+ if (!out.text || out.eof)
241
+ break;
242
+ }
243
+ }
244
+ catch (error) {
245
+ this.error = error instanceof Error ? error.message : String(error);
246
+ }
247
+ finally {
248
+ this.reading = false;
249
+ }
250
+ }
251
+ this.deps.requestRender();
252
+ }
253
+ open() {
254
+ this.text = "";
255
+ this.lineCache = [];
256
+ this.offset = 0;
257
+ this.eof = false;
258
+ this.error = undefined;
259
+ void this.refresh().catch(() => { });
260
+ }
261
+ handleInput(data) {
262
+ const action = this.actionFor(data);
263
+ if (!action)
264
+ return;
265
+ const tasks = this.deps.list();
266
+ const { state, effect } = reduceTasksPanel(this.state, action, {
267
+ tasks,
268
+ lines: this.outputLines().length,
269
+ page: this.page(),
270
+ });
271
+ this.state = state;
272
+ if (effect?.type === "close") {
273
+ this.dispose();
274
+ this.deps.close();
275
+ return;
276
+ }
277
+ if (effect?.type === "open")
278
+ this.open();
279
+ if (effect?.type === "notice")
280
+ this.deps.notice(effect.text);
281
+ if (effect?.type === "stop")
282
+ void this.deps
283
+ .stop(effect.id)
284
+ .catch((error) => this.deps.notice(`Could not stop the task: ${error instanceof Error ? error.message : String(error)}`))
285
+ .finally(() => void this.refresh());
286
+ this.deps.requestRender();
287
+ }
288
+ actionFor(data) {
289
+ if (matchesKey(data, Key.up))
290
+ return { type: "up" };
291
+ if (matchesKey(data, Key.down))
292
+ return { type: "down" };
293
+ if (matchesKey(data, Key.pageUp))
294
+ return { type: "pageUp" };
295
+ if (matchesKey(data, Key.pageDown))
296
+ return { type: "pageDown" };
297
+ if (matchesKey(data, Key.home))
298
+ return { type: "home" };
299
+ if (matchesKey(data, Key.end))
300
+ return { type: "end" };
301
+ if (matchesKey(data, Key.enter))
302
+ return { type: "enter" };
303
+ if (matchesKey(data, Key.escape))
304
+ return { type: "escape" };
305
+ if (data === "q")
306
+ return { type: "quit" };
307
+ if (data === "s")
308
+ return { type: "stop" };
309
+ if (data === "y" || data === "Y")
310
+ return { type: "yes" };
311
+ if (data === "n" || data === "N")
312
+ return { type: "no" };
313
+ return undefined;
314
+ }
315
+ render(width) {
316
+ const tasks = this.deps.list();
317
+ const now = this.now();
318
+ const fit = (text) => truncateToWidth(text, width);
319
+ if (this.state.view === "list") {
320
+ const cursor = Math.min(this.state.cursor, Math.max(0, tasks.length - 1));
321
+ const live = tasks.filter(isLiveTask).length;
322
+ const lines = [
323
+ fit(style.bold(style.yellow(`Background tasks · ${tasks.length}${live ? ` · ${live} running` : ""}`))),
324
+ ];
325
+ if (!tasks.length)
326
+ lines.push(fit(style.dim(" No background tasks in this session yet. The agent starts them with bg_run.")));
327
+ for (const [index, task] of tasks.entries())
328
+ lines.push(fit(taskRow(task, now, index === cursor)));
329
+ if (this.state.confirm)
330
+ lines.push(fit(style.yellow(` Stop "${tasks.find((t) => t.id === this.state.confirm)?.label ?? "task"}"? y/n`)));
331
+ lines.push(fit(style.dim(" ↑↓ move · Enter open · s stop · Esc close · Tasks end when Alisio exits")));
332
+ return lines;
333
+ }
334
+ const task = this.selectedTask();
335
+ if (!task)
336
+ return [fit(style.dim(" This task no longer exists. Esc to go back."))];
337
+ const page = this.page();
338
+ const all = this.outputLines();
339
+ const maxScroll = Math.max(0, all.length - page);
340
+ const first = this.state.scroll === undefined ? maxScroll : Math.min(this.state.scroll, maxScroll);
341
+ const shown = all.slice(first, first + page);
342
+ const facts = [
343
+ task.command ? `$ ${task.command}` : "",
344
+ task.kind === "shell" && task.cwd ? `in ${task.cwd}` : "",
345
+ ].filter(Boolean);
346
+ const lines = [
347
+ fit(style.bold(style.yellow(`Task · ${task.label}`)) + style.dim(` ${statusText(task)}`)),
348
+ ...facts.map((fact) => fit(style.dim(` ${fact}`))),
349
+ ...(task.kind === "subagent"
350
+ ? [
351
+ fit(style.dim(" Subagents are managed from the agents panel; this shows their status only.")),
352
+ ]
353
+ : this.error
354
+ ? [fit(style.red(` ${this.error}`))]
355
+ : shown.length
356
+ ? shown.map(fit)
357
+ : [fit(style.dim(isLiveTask(task) ? " (no output yet)" : " (no output)"))]),
358
+ ];
359
+ if (task.truncated)
360
+ lines.push(fit(style.dim(" The stored log was cut at its size limit.")));
361
+ if (this.state.confirm)
362
+ lines.push(fit(style.yellow(` Stop this task? y/n`)));
363
+ const position = all.length
364
+ ? `${Math.min(all.length, first + shown.length)}/${all.length}${this.state.scroll === undefined && isLiveTask(task) ? " · following" : ""}`
365
+ : "";
366
+ lines.push(fit(style.dim(` ${position ? `${position} · ` : ""}↑↓ PgUp/PgDn Home/End · ${isLiveTask(task) && task.kind === "shell" ? "s stop · " : ""}Esc back`)));
367
+ return lines;
368
+ }
369
+ }
@@ -1,4 +1,4 @@
1
- import type { EditorTheme, ImageTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
1
+ import type { DefaultTextStyle, EditorTheme, ImageTheme, MarkdownTheme, SelectListTheme } from "@earendil-works/pi-tui";
2
2
  import type { Level } from "./state.ts";
3
3
  export declare const style: {
4
4
  bold: (text: string) => string;
@@ -17,6 +17,47 @@ export declare const style: {
17
17
  userBg: (text: string) => string;
18
18
  };
19
19
  export declare const levelColor: (level: Level) => (text: string) => string;
20
+ /**
21
+ * Dependency-free, line-based syntax highlighting for code blocks in responses.
22
+ *
23
+ * The tokenizer is deliberately approximate: it walks each line once and colors keywords, strings,
24
+ * comments, numbers, function calls and property keys for the common response languages. It never
25
+ * tries to be a real parser, never throws on malformed input, and strips ANSI/control characters
26
+ * from the source first (a model response must never be able to inject terminal control codes).
27
+ */
28
+ export type TokenStyle = "keyword" | "string" | "comment" | "number" | "function" | "property" | "plain";
29
+ export interface CodeToken {
30
+ text: string;
31
+ style: TokenStyle;
32
+ }
33
+ /** Strips ANSI escapes, OSC sequences and control characters; normalizes tabs. */
34
+ export declare function sanitizeCode(code: string): string;
35
+ export interface ScanState {
36
+ /** Closing delimiter of a block comment started on a previous line. */
37
+ inComment?: string;
38
+ /** Delimiter of a multi-line string started on a previous line. */
39
+ inString?: string;
40
+ }
41
+ /** Tokenizes code into styled segments, maintaining multi-line string/comment state. */
42
+ export declare function tokenizeCode(code: string, lang?: string): CodeToken[][];
43
+ /**
44
+ * Per-line highlighting for markdown code blocks (pi-tui `MarkdownTheme.highlightCode`).
45
+ * Returns one styled (or plain, under NO_COLOR) line per source line; never emits ANSI for
46
+ * bytes that came from the input.
47
+ */
48
+ export declare function highlightCode(code: string, lang?: string): string[];
49
+ /**
50
+ * Normalizes table separator rows so models' sloppy ASCII art still renders as aligned tables:
51
+ * em/en/box dashes become plain `-` runs (marked's table tokenizer only accepts `-`), while
52
+ * alignment colons are preserved. Anything that is not an obvious separator row, including
53
+ * fenced or indented code, is left untouched. A bare `---` (horizontal rule / setext) has no
54
+ * `|` and is never touched.
55
+ */
56
+ export declare function normalizeTableSeparators(markdown: string): string;
57
+ /** Pre-parse hook for the Markdown component: normalizes table separators before marked parses. */
58
+ export declare const markdownTransform: (markdown: string, _availableWidth: number) => string;
59
+ /** Subtle base tone for response body text; inline styles override it. */
60
+ export declare const markdownDefaultTextStyle: DefaultTextStyle;
20
61
  export declare const markdownTheme: MarkdownTheme;
21
62
  export declare const selectListTheme: SelectListTheme;
22
63
  export declare const editorTheme: EditorTheme;