@alisio/alisio-code 0.1.0-alpha.10
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/LICENSE +21 -0
- package/README.md +23 -0
- package/dist/banner.d.ts +56 -0
- package/dist/banner.js +69 -0
- package/dist/builtin.d.ts +8 -0
- package/dist/builtin.js +49 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +508 -0
- package/dist/prompts/index.d.ts +5 -0
- package/dist/prompts/index.js +3 -0
- package/dist/prompts/init.d.ts +2 -0
- package/dist/prompts/init.js +57 -0
- package/dist/tui/app.d.ts +8 -0
- package/dist/tui/app.js +1412 -0
- package/dist/tui/attachments.d.ts +68 -0
- package/dist/tui/attachments.js +132 -0
- package/dist/tui/clipboard.d.ts +30 -0
- package/dist/tui/clipboard.js +57 -0
- package/dist/tui/components.d.ts +159 -0
- package/dist/tui/components.js +487 -0
- package/dist/tui/connect-input.d.ts +35 -0
- package/dist/tui/connect-input.js +104 -0
- package/dist/tui/panel.d.ts +54 -0
- package/dist/tui/panel.js +140 -0
- package/dist/tui/questions.d.ts +56 -0
- package/dist/tui/questions.js +113 -0
- package/dist/tui/queue.d.ts +35 -0
- package/dist/tui/queue.js +79 -0
- package/dist/tui/skills-manager.d.ts +67 -0
- package/dist/tui/skills-manager.js +200 -0
- package/dist/tui/state.d.ts +199 -0
- package/dist/tui/state.js +566 -0
- package/dist/tui/theme.d.ts +23 -0
- package/dist/tui/theme.js +49 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +28 -0
- package/package.json +66 -0
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
|
+
import { style } from "./theme.js";
|
|
3
|
+
export const initialSkillManagerState = (entries) => ({
|
|
4
|
+
query: "",
|
|
5
|
+
searching: false,
|
|
6
|
+
sort: "name",
|
|
7
|
+
selectedId: entries[0]?.id,
|
|
8
|
+
offset: 0,
|
|
9
|
+
});
|
|
10
|
+
export function visibleSkills(entries, state) {
|
|
11
|
+
const query = state.query.trim().toLowerCase();
|
|
12
|
+
const filtered = query
|
|
13
|
+
? entries.filter((entry) => `${entry.displayId} ${entry.description} ${entry.scope} ${entry.source} ${entry.owner?.name ?? ""}`
|
|
14
|
+
.toLowerCase()
|
|
15
|
+
.includes(query))
|
|
16
|
+
: [...entries];
|
|
17
|
+
const byName = (a, b) => a.displayId.localeCompare(b.displayId);
|
|
18
|
+
filtered.sort((a, b) => {
|
|
19
|
+
if (state.sort === "source")
|
|
20
|
+
return a.source.localeCompare(b.source) || a.scope.localeCompare(b.scope) || byName(a, b);
|
|
21
|
+
if (state.sort === "tokens")
|
|
22
|
+
return b.approximateTokens - a.approximateTokens || byName(a, b);
|
|
23
|
+
return byName(a, b);
|
|
24
|
+
});
|
|
25
|
+
return filtered;
|
|
26
|
+
}
|
|
27
|
+
export function retainSkillSelection(entries, state) {
|
|
28
|
+
const visible = visibleSkills(entries, state);
|
|
29
|
+
if (visible.some((entry) => entry.id === state.selectedId))
|
|
30
|
+
return state;
|
|
31
|
+
return { ...state, selectedId: visible[0]?.id, offset: 0 };
|
|
32
|
+
}
|
|
33
|
+
export function skillViewport(entries, state, capacity) {
|
|
34
|
+
const visible = visibleSkills(entries, state);
|
|
35
|
+
const selected = Math.max(0, visible.findIndex((entry) => entry.id === state.selectedId));
|
|
36
|
+
const size = Math.max(1, capacity);
|
|
37
|
+
let offset = Math.max(0, Math.min(state.offset, Math.max(0, visible.length - size)));
|
|
38
|
+
if (selected < offset)
|
|
39
|
+
offset = selected;
|
|
40
|
+
else if (selected >= offset + size)
|
|
41
|
+
offset = selected - size + 1;
|
|
42
|
+
const items = visible.slice(offset, offset + size);
|
|
43
|
+
return {
|
|
44
|
+
items,
|
|
45
|
+
selected,
|
|
46
|
+
offset,
|
|
47
|
+
above: offset,
|
|
48
|
+
below: Math.max(0, visible.length - offset - items.length),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const SORTS = ["name", "source", "tokens"];
|
|
52
|
+
export const cycleSkillSort = (sort) => SORTS[(SORTS.indexOf(sort) + 1) % SORTS.length] ?? "name";
|
|
53
|
+
export function moveSkillSelection(entries, state, move, capacity) {
|
|
54
|
+
const visible = visibleSkills(entries, state);
|
|
55
|
+
if (!visible.length)
|
|
56
|
+
return { ...state, selectedId: undefined, offset: 0 };
|
|
57
|
+
const current = Math.max(0, visible.findIndex((entry) => entry.id === state.selectedId));
|
|
58
|
+
const delta = Math.max(1, capacity - 1);
|
|
59
|
+
const next = move === "home"
|
|
60
|
+
? 0
|
|
61
|
+
: move === "end"
|
|
62
|
+
? visible.length - 1
|
|
63
|
+
: Math.max(0, Math.min(visible.length - 1, current +
|
|
64
|
+
(move === "up" ? -1 : move === "down" ? 1 : move === "pageUp" ? -delta : delta)));
|
|
65
|
+
return { ...state, selectedId: visible[next]?.id };
|
|
66
|
+
}
|
|
67
|
+
const sourceLabel = (entry) => entry.locked ? "locked by plugin · plugin" : entry.scope === "config" ? "config" : entry.scope;
|
|
68
|
+
const marker = (entry) => (!entry.effective ? "↳" : entry.enabled ? "✔" : "○");
|
|
69
|
+
export class SkillsManager {
|
|
70
|
+
entries;
|
|
71
|
+
state;
|
|
72
|
+
width = 80;
|
|
73
|
+
height;
|
|
74
|
+
pending = false;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.entries = options.entries;
|
|
77
|
+
this.state = initialSkillManagerState(this.entries);
|
|
78
|
+
this.height = options.height;
|
|
79
|
+
this.onClose = options.onClose;
|
|
80
|
+
this.onToggle = options.onToggle;
|
|
81
|
+
this.onError = options.onError;
|
|
82
|
+
this.onChanged = options.onChanged;
|
|
83
|
+
this.requestRender = options.requestRender;
|
|
84
|
+
}
|
|
85
|
+
onClose;
|
|
86
|
+
onToggle;
|
|
87
|
+
onError;
|
|
88
|
+
onChanged;
|
|
89
|
+
requestRender;
|
|
90
|
+
invalidate() { }
|
|
91
|
+
capacity() {
|
|
92
|
+
return Math.max(1, this.height() - 12);
|
|
93
|
+
}
|
|
94
|
+
selected() {
|
|
95
|
+
return visibleSkills(this.entries, this.state).find((entry) => entry.id === this.state.selectedId);
|
|
96
|
+
}
|
|
97
|
+
handleInput(data) {
|
|
98
|
+
if (matchesKey(data, Key.escape))
|
|
99
|
+
return this.onClose();
|
|
100
|
+
if (this.state.searching) {
|
|
101
|
+
if (matchesKey(data, Key.backspace))
|
|
102
|
+
this.state.query = this.state.query.slice(0, -1);
|
|
103
|
+
else if (matchesKey(data, Key.enter))
|
|
104
|
+
this.state.searching = false;
|
|
105
|
+
else if (data.length === 1 && data >= " " && data <= "~")
|
|
106
|
+
this.state.query += data;
|
|
107
|
+
this.state = retainSkillSelection(this.entries, this.state);
|
|
108
|
+
this.requestRender();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const capacity = this.capacity();
|
|
112
|
+
let move;
|
|
113
|
+
if (matchesKey(data, Key.up) || /^\x1b\[<64;/.test(data))
|
|
114
|
+
move = "up";
|
|
115
|
+
else if (matchesKey(data, Key.down) || /^\x1b\[<65;/.test(data))
|
|
116
|
+
move = "down";
|
|
117
|
+
else if (data === "\x1b[5~")
|
|
118
|
+
move = "pageUp";
|
|
119
|
+
else if (data === "\x1b[6~")
|
|
120
|
+
move = "pageDown";
|
|
121
|
+
else if (["\x1b[H", "\x1b[1~"].includes(data))
|
|
122
|
+
move = "home";
|
|
123
|
+
else if (["\x1b[F", "\x1b[4~"].includes(data))
|
|
124
|
+
move = "end";
|
|
125
|
+
if (move)
|
|
126
|
+
this.state = moveSkillSelection(this.entries, this.state, move, capacity);
|
|
127
|
+
else if (data === "/")
|
|
128
|
+
this.state = { ...this.state, searching: true };
|
|
129
|
+
else if (data === "t") {
|
|
130
|
+
this.state = retainSkillSelection(this.entries, {
|
|
131
|
+
...this.state,
|
|
132
|
+
sort: cycleSkillSort(this.state.sort),
|
|
133
|
+
offset: 0,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
else if ((matchesKey(data, Key.enter) || data === " ") && !this.pending) {
|
|
137
|
+
const selected = this.selected();
|
|
138
|
+
if (selected?.manageable && !selected.locked && selected.effective) {
|
|
139
|
+
this.pending = true;
|
|
140
|
+
void this.onToggle(selected.id, !selected.enabled)
|
|
141
|
+
.then((updated) => {
|
|
142
|
+
this.entries = this.entries.map((entry) => (entry.id === updated.id ? updated : entry));
|
|
143
|
+
this.onChanged(`${updated.displayId}: ${updated.enabled ? "enabled" : "disabled"}.`);
|
|
144
|
+
})
|
|
145
|
+
.catch(this.onError)
|
|
146
|
+
.finally(() => {
|
|
147
|
+
this.pending = false;
|
|
148
|
+
this.requestRender();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
this.requestRender();
|
|
153
|
+
}
|
|
154
|
+
render(width) {
|
|
155
|
+
this.width = width;
|
|
156
|
+
const state = retainSkillSelection(this.entries, this.state);
|
|
157
|
+
this.state = state;
|
|
158
|
+
const viewport = skillViewport(this.entries, state, this.capacity());
|
|
159
|
+
this.state.offset = viewport.offset;
|
|
160
|
+
const all = visibleSkills(this.entries, state);
|
|
161
|
+
const selected = this.selected();
|
|
162
|
+
const fit = (line) => truncateToWidth(line, Math.max(1, width));
|
|
163
|
+
const lines = [
|
|
164
|
+
fit(style.bold(style.yellow("Skills"))),
|
|
165
|
+
fit(style.dim(`${all.length}/${this.entries.length} · sort: ${state.sort} · enter/space to cycle, / to search, t to sort, Esc to close`)),
|
|
166
|
+
fit(state.searching || state.query
|
|
167
|
+
? `Search: ${state.query}${state.searching ? "▏" : ""}`
|
|
168
|
+
: "Search: —"),
|
|
169
|
+
];
|
|
170
|
+
if (viewport.above)
|
|
171
|
+
lines.push(fit(style.dim(`↑ ${viewport.above} more above`)));
|
|
172
|
+
for (const entry of viewport.items) {
|
|
173
|
+
const active = entry.id === state.selectedId;
|
|
174
|
+
const text = `${marker(entry)} ${entry.displayId} ${sourceLabel(entry)} · ~${entry.approximateTokens} tok`;
|
|
175
|
+
lines.push(fit(active ? `\x1b[7m${text}\x1b[27m` : text));
|
|
176
|
+
}
|
|
177
|
+
if (viewport.below)
|
|
178
|
+
lines.push(fit(style.dim(`↓ ${viewport.below} more below`)));
|
|
179
|
+
if (!viewport.items.length)
|
|
180
|
+
lines.push(fit(style.dim("No skills match this search.")));
|
|
181
|
+
if (selected) {
|
|
182
|
+
const status = !selected.effective
|
|
183
|
+
? `shadowed by ${selected.shadowedBy ?? "a higher-precedence skill"}`
|
|
184
|
+
: selected.enabled
|
|
185
|
+
? "effective · enabled"
|
|
186
|
+
: "effective · disabled";
|
|
187
|
+
const action = selected.locked
|
|
188
|
+
? `Locked by plugin ${selected.owner?.name ?? selected.owner?.id ?? "owner"}; use /plugins to manage it.`
|
|
189
|
+
: selected.effective
|
|
190
|
+
? `${this.pending ? "Saving…" : "Enter/Space"} ${selected.enabled ? "disables" : "enables"} this skill for this project.`
|
|
191
|
+
: "This lower-precedence copy cannot be toggled independently.";
|
|
192
|
+
lines.push(fit(style.bold(selected.displayId)));
|
|
193
|
+
lines.push(...wrapTextWithAnsi(selected.description, Math.max(1, width)).slice(0, 2).map(fit));
|
|
194
|
+
lines.push(fit(style.dim(`Source: ${sourceLabel(selected)} · Scope: ${selected.scope} · ~${selected.approximateTokens} tokens`)));
|
|
195
|
+
lines.push(fit(style.dim(`Status: ${status}`)));
|
|
196
|
+
lines.push(fit(style.dim(action)));
|
|
197
|
+
}
|
|
198
|
+
return lines.slice(0, Math.max(4, this.height()));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure presentation logic for the TUI: formatting, command parsing and the reduction of
|
|
3
|
+
* versioned runner events into a view model. No terminal or pi-tui imports here.
|
|
4
|
+
*/
|
|
5
|
+
import type { Message, ModelInfo, RunEvent } from "@alisio/sdk";
|
|
6
|
+
export type Level = "ok" | "warn" | "danger";
|
|
7
|
+
export declare function formatTokens(n: number): string;
|
|
8
|
+
/** Prefixes every model with its owning provider so identical model ids are never ambiguous. */
|
|
9
|
+
export declare function providerModelItems(provider: string, models: ModelInfo[], current?: string): {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string | undefined;
|
|
13
|
+
}[];
|
|
14
|
+
export interface ProviderCatalogView {
|
|
15
|
+
profile: string;
|
|
16
|
+
provider: string;
|
|
17
|
+
title: string;
|
|
18
|
+
configuredModel: string;
|
|
19
|
+
models: ModelInfo[];
|
|
20
|
+
unavailable: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface ConfiguredProviderModelItem {
|
|
23
|
+
value: string;
|
|
24
|
+
label: string;
|
|
25
|
+
description: string;
|
|
26
|
+
unavailable: boolean;
|
|
27
|
+
}
|
|
28
|
+
export interface PluginCatalogView {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
categories: string[];
|
|
33
|
+
builtin: boolean;
|
|
34
|
+
source: string;
|
|
35
|
+
status: "active" | "inactive" | "failed" | "restart-required";
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
manageable: boolean;
|
|
38
|
+
diagnostic?: string;
|
|
39
|
+
}
|
|
40
|
+
/** Text markers remain meaningful without color: [x] active, [ ] inactive, [!] failed, [*] pending. */
|
|
41
|
+
/** Group headings are derived from the primary category (or "General") of each plugin. */
|
|
42
|
+
export declare function pluginCatalogItems(entries: PluginCatalogView[]): {
|
|
43
|
+
value: string;
|
|
44
|
+
label: string;
|
|
45
|
+
description: string;
|
|
46
|
+
}[];
|
|
47
|
+
export declare const pluginToggleNeedsConfirmation: (entry: PluginCatalogView) => boolean;
|
|
48
|
+
export interface McpServerView {
|
|
49
|
+
name: string;
|
|
50
|
+
displayName: string;
|
|
51
|
+
source: {
|
|
52
|
+
kind: "global" | "project" | "explicit" | "builtin" | "plugin";
|
|
53
|
+
};
|
|
54
|
+
status: "disabled" | "disconnected" | "connecting" | "connected" | "failed" | "needs-authentication" | "restart-required";
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
runtimePermission: "granted" | "not-granted" | "read-only";
|
|
57
|
+
counts: {
|
|
58
|
+
tools: number;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** Group headings are generated only for sources that actually registered servers. */
|
|
62
|
+
export declare function mcpServerItems(entries: McpServerView[]): {
|
|
63
|
+
value: string;
|
|
64
|
+
label: string;
|
|
65
|
+
description: string;
|
|
66
|
+
}[];
|
|
67
|
+
export interface McpToolView {
|
|
68
|
+
name: string;
|
|
69
|
+
effectiveName?: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
annotations?: {
|
|
73
|
+
readOnly?: boolean;
|
|
74
|
+
destructive?: boolean;
|
|
75
|
+
openWorld?: boolean;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export declare function mcpToolItems(tools: McpToolView[]): {
|
|
79
|
+
value: string;
|
|
80
|
+
label: string;
|
|
81
|
+
description: string;
|
|
82
|
+
}[];
|
|
83
|
+
/** Builds one filterable list while retaining provider/profile ownership in each opaque value. */
|
|
84
|
+
export declare function configuredProviderModelItems(catalogs: ProviderCatalogView[], current?: {
|
|
85
|
+
provider: string;
|
|
86
|
+
model: string;
|
|
87
|
+
}): ConfiguredProviderModelItem[];
|
|
88
|
+
export declare function contextLevel(pct: number, compactionAt?: number): Level;
|
|
89
|
+
export declare function contextPercent(used: number, total: number | undefined): number | undefined;
|
|
90
|
+
/** The effective total the context bar measures against, and what it is derived from. */
|
|
91
|
+
export interface ContextBudget {
|
|
92
|
+
/** Effective total in tokens: the model's context window, or absent when it is unknown. */
|
|
93
|
+
total?: number;
|
|
94
|
+
/** Basis of the total: the model's context window, or unknown (no fabricated total). */
|
|
95
|
+
basis: "window" | "unknown";
|
|
96
|
+
/** Percentage of `total` at which the engine auto-compacts; the bar turns red there. */
|
|
97
|
+
compactionAt: number;
|
|
98
|
+
}
|
|
99
|
+
export declare function formatContext(used: number, total: number | undefined, estimated: boolean, basis?: "window" | "unknown"): string;
|
|
100
|
+
export declare function formatDuration(ms: number): string;
|
|
101
|
+
export declare const textWidth: (text: string) => number;
|
|
102
|
+
export declare function truncatePlain(text: string, width: number): string;
|
|
103
|
+
export declare function shortenPath(path: string, home: string, max?: number): string;
|
|
104
|
+
/** Host (and port) only: never path, query, user info or credentials. */
|
|
105
|
+
export declare function hostOf(baseURL: string): string;
|
|
106
|
+
export declare const shortId: (id: string) => string;
|
|
107
|
+
export interface Segment {
|
|
108
|
+
text: string;
|
|
109
|
+
priority: number;
|
|
110
|
+
}
|
|
111
|
+
/** Keeps segments in order, dropping the lowest priority ones until the line fits. */
|
|
112
|
+
export declare function fitSegments<T extends Segment>(segments: T[], width: number, separator: string): T[];
|
|
113
|
+
export interface CommandSpec {
|
|
114
|
+
name: string;
|
|
115
|
+
description: string;
|
|
116
|
+
argumentHint?: string;
|
|
117
|
+
aliases?: string[];
|
|
118
|
+
}
|
|
119
|
+
export declare const COMMANDS: CommandSpec[];
|
|
120
|
+
/** Every TUI slash name (commands, aliases and routing prefixes); templates cannot take them. */
|
|
121
|
+
export declare function reservedCommandNames(): string[];
|
|
122
|
+
export declare function resolveCommand(name: string): string | undefined;
|
|
123
|
+
export declare function parseCommand(input: string): {
|
|
124
|
+
name: string;
|
|
125
|
+
args: string;
|
|
126
|
+
} | undefined;
|
|
127
|
+
export declare function summarizeToolArgs(name: string, args: string): string;
|
|
128
|
+
export interface DiffLine {
|
|
129
|
+
sign: "+" | "-";
|
|
130
|
+
text: string;
|
|
131
|
+
}
|
|
132
|
+
export interface EditSummary {
|
|
133
|
+
path: string;
|
|
134
|
+
added: number;
|
|
135
|
+
removed: number;
|
|
136
|
+
lines: DiffLine[];
|
|
137
|
+
}
|
|
138
|
+
export declare function editSummary(name: string, args: string): EditSummary | undefined;
|
|
139
|
+
export type ToolStatus = "running" | "approval" | "ok" | "error";
|
|
140
|
+
export type TranscriptItem = {
|
|
141
|
+
kind: "user";
|
|
142
|
+
text: string;
|
|
143
|
+
} | {
|
|
144
|
+
kind: "assistant";
|
|
145
|
+
text: string;
|
|
146
|
+
reasoning: string;
|
|
147
|
+
done: boolean;
|
|
148
|
+
} | {
|
|
149
|
+
kind: "tool";
|
|
150
|
+
id: string;
|
|
151
|
+
name: string;
|
|
152
|
+
args: string;
|
|
153
|
+
summary: string;
|
|
154
|
+
status: ToolStatus;
|
|
155
|
+
durationMs?: number;
|
|
156
|
+
preview?: string;
|
|
157
|
+
} | {
|
|
158
|
+
kind: "notice";
|
|
159
|
+
text: string;
|
|
160
|
+
} | {
|
|
161
|
+
kind: "info";
|
|
162
|
+
text: string;
|
|
163
|
+
} | {
|
|
164
|
+
kind: "error";
|
|
165
|
+
text: string;
|
|
166
|
+
};
|
|
167
|
+
export interface Stats {
|
|
168
|
+
input: number;
|
|
169
|
+
output: number;
|
|
170
|
+
cached: number;
|
|
171
|
+
turns: number;
|
|
172
|
+
runs: number;
|
|
173
|
+
tools: Record<string, {
|
|
174
|
+
calls: number;
|
|
175
|
+
errors: number;
|
|
176
|
+
}>;
|
|
177
|
+
models: string[];
|
|
178
|
+
lastRunMs?: number;
|
|
179
|
+
startedAt: number;
|
|
180
|
+
}
|
|
181
|
+
export interface ViewState {
|
|
182
|
+
items: TranscriptItem[];
|
|
183
|
+
streaming: boolean;
|
|
184
|
+
compacting: boolean;
|
|
185
|
+
model: string;
|
|
186
|
+
/** Context size in tokens: provider-reported, or estimated (`~`). */
|
|
187
|
+
context?: {
|
|
188
|
+
used: number;
|
|
189
|
+
estimated: boolean;
|
|
190
|
+
};
|
|
191
|
+
runStartedAt?: number;
|
|
192
|
+
stats: Stats;
|
|
193
|
+
}
|
|
194
|
+
export declare function initialViewState(model: string, now?: number): ViewState;
|
|
195
|
+
export declare function addItem(state: ViewState, item: TranscriptItem): ViewState;
|
|
196
|
+
export declare function reduceEvent(state: ViewState, event: RunEvent): ViewState;
|
|
197
|
+
export declare function lastAssistantText(items: TranscriptItem[]): string | undefined;
|
|
198
|
+
/** Rebuilds transcript items from persisted history (used by /resume). */
|
|
199
|
+
export declare function itemsFromHistory(messages: Message[]): TranscriptItem[];
|