@itookit/dsht 0.2.4 → 0.3.2
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.i18n.yaml +2 -2
- package/README.md +54 -25
- package/README.zh.md +54 -25
- package/dist/catalog/controller.d.ts +32 -0
- package/dist/catalog/controller.js +88 -0
- package/dist/catalog/index.d.ts +2 -0
- package/dist/catalog/index.js +2 -0
- package/dist/{cli.js → cli/index.js} +40 -24
- package/dist/controller/connection.d.ts +80 -0
- package/dist/controller/connection.js +190 -0
- package/dist/controller/controller.d.ts +260 -0
- package/dist/controller/controller.js +362 -0
- package/dist/controller/index.d.ts +5 -0
- package/dist/controller/index.js +3 -0
- package/dist/controller/memory-log.d.ts +35 -0
- package/dist/controller/memory-log.js +95 -0
- package/dist/cost/controller.d.ts +34 -0
- package/dist/cost/controller.js +107 -0
- package/dist/cost/index.d.ts +9 -0
- package/dist/cost/index.js +7 -0
- package/dist/cost/ledger-files.d.ts +20 -0
- package/dist/cost/ledger-files.js +128 -0
- package/dist/cost/ledger.d.ts +65 -0
- package/dist/cost/ledger.js +136 -0
- package/dist/cost/pricing.d.ts +48 -0
- package/dist/cost/pricing.js +141 -0
- package/dist/cost/records.d.ts +17 -0
- package/dist/cost/records.js +66 -0
- package/dist/cost/scanner.d.ts +22 -0
- package/dist/cost/scanner.js +95 -0
- package/dist/cost/types.d.ts +69 -0
- package/dist/cost/types.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/session/connection-view.d.ts +18 -0
- package/dist/session/connection-view.js +1 -0
- package/dist/{controller.d.ts → session/controller.d.ts} +119 -137
- package/dist/session/controller.js +616 -0
- package/dist/session/export-html.d.ts +9 -0
- package/dist/session/export-html.js +39 -0
- package/dist/session/export.d.ts +9 -0
- package/dist/session/export.js +38 -0
- package/dist/{history.d.ts → session/history.d.ts} +18 -1
- package/dist/session/history.js +338 -0
- package/dist/session/index.d.ts +17 -0
- package/dist/session/index.js +10 -0
- package/dist/session/markdown.d.ts +39 -0
- package/dist/session/markdown.js +255 -0
- package/dist/session/math.d.ts +11 -0
- package/dist/session/math.js +82 -0
- package/dist/{navigation.d.ts → session/navigation.d.ts} +1 -1
- package/dist/{navigation.js → session/navigation.js} +1 -1
- package/dist/{references.js → session/references.js} +1 -1
- package/dist/{telemetry.d.ts → session/telemetry.d.ts} +12 -1
- package/dist/{telemetry.js → session/telemetry.js} +18 -4
- package/dist/{transcript.d.ts → session/transcript.d.ts} +23 -1
- package/dist/{transcript.js → session/transcript.js} +36 -18
- package/dist/session/types.d.ts +18 -0
- package/dist/session/types.js +2 -0
- package/dist/state.d.ts +41 -0
- package/dist/state.js +9 -0
- package/dist/storage/directories.d.ts +14 -0
- package/dist/storage/directories.js +24 -0
- package/dist/storage/files.d.ts +43 -0
- package/dist/storage/files.js +135 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +3 -0
- package/dist/transport/auth.js +65 -0
- package/dist/{client.d.ts → transport/client.d.ts} +8 -1
- package/dist/{client.js → transport/client.js} +21 -4
- package/dist/transport/host.d.ts +13 -0
- package/dist/transport/host.js +1 -0
- package/dist/ui/app.d.ts +11 -0
- package/dist/ui/app.js +726 -0
- package/dist/ui/chat/header.d.ts +11 -0
- package/dist/ui/chat/header.js +14 -0
- package/dist/{history-view.d.ts → ui/chat/history-view.d.ts} +1 -1
- package/dist/{history-view.js → ui/chat/history-view.js} +3 -3
- package/dist/ui/chat/status.d.ts +79 -0
- package/dist/ui/chat/status.js +311 -0
- package/dist/ui/chat/viewport.d.ts +17 -0
- package/dist/ui/chat/viewport.js +14 -0
- package/dist/ui/commands/parse.d.ts +96 -0
- package/dist/ui/commands/parse.js +121 -0
- package/dist/ui/commands/registry.d.ts +33 -0
- package/dist/ui/commands/registry.js +72 -0
- package/dist/ui/copy-mode.d.ts +4 -0
- package/dist/ui/copy-mode.js +6 -0
- package/dist/{cost-view.d.ts → ui/dialogs/cost.d.ts} +1 -1
- package/dist/{cost-view.js → ui/dialogs/cost.js} +3 -3
- package/dist/ui/dialogs/index.d.ts +120 -0
- package/dist/ui/dialogs/index.js +113 -0
- package/dist/ui/dialogs/picker.d.ts +18 -0
- package/dist/ui/dialogs/picker.js +38 -0
- package/dist/ui/frozen.d.ts +8 -0
- package/dist/ui/frozen.js +7 -0
- package/dist/ui/input/references.d.ts +12 -0
- package/dist/ui/input/references.js +15 -0
- package/dist/ui/mount.d.ts +6 -0
- package/dist/ui/mount.js +11 -0
- package/dist/{theme.d.ts → ui/theme/index.d.ts} +1 -1
- package/dsht-m.png +0 -0
- package/package.json +20 -14
- package/dist/app.d.ts +0 -21
- package/dist/app.js +0 -746
- package/dist/auth.js +0 -108
- package/dist/controller.js +0 -923
- package/dist/cost.d.ts +0 -119
- package/dist/cost.js +0 -313
- package/dist/history.js +0 -177
- package/dist/status.d.ts +0 -28
- package/dist/status.js +0 -157
- package/dsht.png +0 -0
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{memory.d.ts → session/memory.d.ts} +0 -0
- /package/dist/{memory.js → session/memory.js} +0 -0
- /package/dist/{references.d.ts → session/references.d.ts} +0 -0
- /package/dist/{auth.d.ts → transport/auth.d.ts} +0 -0
- /package/dist/{endpoint.d.ts → transport/endpoint.d.ts} +0 -0
- /package/dist/{endpoint.js → transport/endpoint.js} +0 -0
- /package/dist/{wire.d.ts → transport/wire.d.ts} +0 -0
- /package/dist/{wire.js → transport/wire.js} +0 -0
- /package/dist/{input-history.d.ts → ui/input/history.d.ts} +0 -0
- /package/dist/{input-history.js → ui/input/history.js} +0 -0
- /package/dist/{input.d.ts → ui/input/input.d.ts} +0 -0
- /package/dist/{input.js → ui/input/input.js} +0 -0
- /package/dist/{mouse.d.ts → ui/input/mouse.d.ts} +0 -0
- /package/dist/{mouse.js → ui/input/mouse.js} +0 -0
- /package/dist/{theme.js → ui/theme/index.js} +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Client } from '../transport/client.ts';
|
|
2
|
+
/** Save an archive without overwriting an existing file or retaining a partial download.
|
|
3
|
+
* @param client - Authenticated host transport.
|
|
4
|
+
* @param sessionId - Selected session to export.
|
|
5
|
+
* @param destination - Local filename, defaulting to a timestamped ZIP in the working directory.
|
|
6
|
+
* @param signal - Caller-owned cancellation.
|
|
7
|
+
* @returns Absolute saved filename.
|
|
8
|
+
*/
|
|
9
|
+
export declare function saveSessionLog(client: Client, sessionId: string, destination: string | undefined, signal: AbortSignal): Promise<string>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Stream authenticated session archives to exclusive local files. */
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { writeExclusiveStream } from "../storage/index.js";
|
|
4
|
+
/** Save an archive without overwriting an existing file or retaining a partial download.
|
|
5
|
+
* @param client - Authenticated host transport.
|
|
6
|
+
* @param sessionId - Selected session to export.
|
|
7
|
+
* @param destination - Local filename, defaulting to a timestamped ZIP in the working directory.
|
|
8
|
+
* @param signal - Caller-owned cancellation.
|
|
9
|
+
* @returns Absolute saved filename.
|
|
10
|
+
*/
|
|
11
|
+
export async function saveSessionLog(client, sessionId, destination, signal) {
|
|
12
|
+
const path = resolve(destination ?? `session-${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}-${Date.now()}.zip`);
|
|
13
|
+
await writeExclusiveStream(path, async () => {
|
|
14
|
+
const response = await client.sessionLog(sessionId, signal);
|
|
15
|
+
if (!response.body)
|
|
16
|
+
throw new Error('Session log export has no body');
|
|
17
|
+
const reader = response.body.getReader();
|
|
18
|
+
return (async function* chunks() {
|
|
19
|
+
try {
|
|
20
|
+
while (true) {
|
|
21
|
+
const chunk = await reader.read();
|
|
22
|
+
if (chunk.done)
|
|
23
|
+
return;
|
|
24
|
+
yield chunk.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
try {
|
|
29
|
+
await reader.cancel();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
})();
|
|
36
|
+
}, signal);
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Message, type MessagePart, type Transcript } from './transcript.ts';
|
|
2
|
+
import { type MarkdownSpan } from './markdown.ts';
|
|
2
3
|
/** Default fold mode; individual sequence overrides are view state, never stored content. */
|
|
3
4
|
export type Reasoning = 'row' | 'full';
|
|
4
5
|
/** Color semantics are independent of the selected terminal palette. */
|
|
@@ -9,17 +10,33 @@ export interface HistoryRow {
|
|
|
9
10
|
kind: RowKind;
|
|
10
11
|
bold?: boolean;
|
|
11
12
|
seq?: number;
|
|
13
|
+
spans?: MarkdownSpan[];
|
|
12
14
|
}
|
|
13
15
|
/** Drop all terminal rows and layout metadata for an evicted or inactive transcript.
|
|
14
16
|
* @param transcript - Transcript whose previously returned layout is no longer used.
|
|
15
17
|
*/
|
|
16
18
|
export declare function releaseHistoryLayout(transcript: Transcript): void;
|
|
19
|
+
/** Report what one transcript's layout still holds, for memory samples and diagnostics.
|
|
20
|
+
*
|
|
21
|
+
* The row cache is the only structure that grows with expanded reasoning and Markdown rows, and
|
|
22
|
+
* its byte bound counts neither the span objects nor the incremental live-tail state.
|
|
23
|
+
* @param transcript - Transcript whose layout should be measured.
|
|
24
|
+
* @returns Cache and live-tail counters, or undefined when no layout was built yet.
|
|
25
|
+
*/
|
|
26
|
+
export declare function layoutStats(transcript: Transcript): {
|
|
27
|
+
rows: number;
|
|
28
|
+
cacheBytes: number;
|
|
29
|
+
spans: number;
|
|
30
|
+
spanChars: number;
|
|
31
|
+
liveWraps: number;
|
|
32
|
+
liveMarkdown: number;
|
|
33
|
+
} | undefined;
|
|
17
34
|
/** Lay out an indexed conversation without concatenating its historical rows on every stream frame.
|
|
18
35
|
* @param transcript - Selected session's semantic content and unfinished assistant output.
|
|
19
36
|
* @param width - Available terminal columns.
|
|
20
37
|
* @param reasoning - Global committed/completed reasoning fold mode.
|
|
21
38
|
* @param overrides - Sequences whose fold mode differs from the global mode; replace the set on changes.
|
|
22
|
-
* @param liveReasoning - Fold mode for completed blocks
|
|
39
|
+
* @param liveReasoning - Fold mode for completed live blocks; below 60 content columns it also folds active reasoning.
|
|
23
40
|
* @returns Row count, sequence offsets, and a viewport reader; `lines` materializes all rows for exports only.
|
|
24
41
|
*/
|
|
25
42
|
export declare function historyLayout(transcript: Transcript, width: number, reasoning?: Reasoning, overrides?: ReadonlySet<number>, liveReasoning?: Reasoning): {
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/** Indexed semantic history with bounded terminal-row caching and viewport-only materialization. */
|
|
2
|
+
import wrapAnsi from 'wrap-ansi';
|
|
3
|
+
import { toolLine } from "./transcript.js";
|
|
4
|
+
import { hasMarkdown, markdownRows } from "./markdown.js";
|
|
5
|
+
/** A session owns one layout cache; dropped sessions release their entire cache. */
|
|
6
|
+
class LayoutIndex {
|
|
7
|
+
width;
|
|
8
|
+
reasoning;
|
|
9
|
+
overrides;
|
|
10
|
+
messages = [];
|
|
11
|
+
segments = [];
|
|
12
|
+
offsets = new Map();
|
|
13
|
+
length = 0;
|
|
14
|
+
cache = new Map();
|
|
15
|
+
cacheSize = 0;
|
|
16
|
+
// Bound both row objects and long rows. Full semantic content remains available for search.
|
|
17
|
+
maxRows = 2048;
|
|
18
|
+
heights = new WeakMap();
|
|
19
|
+
/** Incremental wrapping state for the growing live tail, keyed by each live part's identity. */
|
|
20
|
+
liveWraps = new Map();
|
|
21
|
+
liveMarkdown = new Map();
|
|
22
|
+
constructor(width, reasoning, overrides) {
|
|
23
|
+
this.width = width;
|
|
24
|
+
this.reasoning = reasoning;
|
|
25
|
+
this.overrides = overrides;
|
|
26
|
+
}
|
|
27
|
+
rows(message, reasoning, heading) {
|
|
28
|
+
const cached = this.cache.get(message);
|
|
29
|
+
if (cached?.width === this.width && cached.reasoning === reasoning && cached.heading === heading) {
|
|
30
|
+
this.cache.delete(message);
|
|
31
|
+
this.cache.set(message, cached);
|
|
32
|
+
return cached.rows;
|
|
33
|
+
}
|
|
34
|
+
const rows = messageRows(message, this.width, reasoning, heading);
|
|
35
|
+
if (cached) {
|
|
36
|
+
this.cache.delete(message);
|
|
37
|
+
this.cacheSize -= cached.rows.length;
|
|
38
|
+
}
|
|
39
|
+
if (rows.length <= this.maxRows && rows.reduce((sum, row) => sum + row.text.length, 0) <= 256 * 1024) {
|
|
40
|
+
while (this.cacheSize + rows.length > this.maxRows) {
|
|
41
|
+
const key = this.cache.keys().next().value;
|
|
42
|
+
this.cacheSize -= this.cache.get(key).rows.length;
|
|
43
|
+
this.cache.delete(key);
|
|
44
|
+
}
|
|
45
|
+
this.cache.set(message, { width: this.width, reasoning, heading, rows });
|
|
46
|
+
this.cacheSize += rows.length;
|
|
47
|
+
}
|
|
48
|
+
return rows;
|
|
49
|
+
}
|
|
50
|
+
dispose() {
|
|
51
|
+
this.messages = [];
|
|
52
|
+
this.segments = [];
|
|
53
|
+
this.offsets.clear();
|
|
54
|
+
this.cache.clear();
|
|
55
|
+
this.cacheSize = 0;
|
|
56
|
+
this.length = 0;
|
|
57
|
+
this.heights = new WeakMap();
|
|
58
|
+
this.liveWraps.clear();
|
|
59
|
+
this.liveMarkdown.clear();
|
|
60
|
+
}
|
|
61
|
+
get cachedRowCount() { return this.cacheSize; }
|
|
62
|
+
/** Measure what the row cache actually holds, including the style spans the byte bound ignores.
|
|
63
|
+
* @returns Cached rows, their accounted characters, and the span objects inside them.
|
|
64
|
+
*/
|
|
65
|
+
stats() {
|
|
66
|
+
let cacheBytes = 0, spans = 0, spanChars = 0;
|
|
67
|
+
for (const { rows } of this.cache.values()) {
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
cacheBytes += row.text.length;
|
|
70
|
+
for (const span of row.spans ?? []) {
|
|
71
|
+
spans++;
|
|
72
|
+
spanChars += span.text.length;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return { rows: this.cacheSize, cacheBytes, spans, spanChars };
|
|
77
|
+
}
|
|
78
|
+
get assistantSeen() { return this.segments.at(-1)?.assistantSeen ?? false; }
|
|
79
|
+
update(messages) {
|
|
80
|
+
if (messages === this.messages)
|
|
81
|
+
return;
|
|
82
|
+
// Appended durable messages extend the row index; replacements and older pages rebuild offsets.
|
|
83
|
+
let prefix = 0;
|
|
84
|
+
while (prefix < this.messages.length && prefix < messages.length && this.messages[prefix] === messages[prefix])
|
|
85
|
+
prefix++;
|
|
86
|
+
if (prefix < this.messages.length) {
|
|
87
|
+
const retained = new Set(messages);
|
|
88
|
+
for (const [message, cached] of this.cache)
|
|
89
|
+
if (!retained.has(message)) {
|
|
90
|
+
this.cacheSize -= cached.rows.length;
|
|
91
|
+
this.cache.delete(message);
|
|
92
|
+
}
|
|
93
|
+
this.segments.length = prefix;
|
|
94
|
+
this.offsets = new Map(this.segments.map(segment => [segment.message.seq, segment.start]));
|
|
95
|
+
this.length = this.segments.at(-1) ? this.segments.at(-1).start + this.segments.at(-1).count : 0;
|
|
96
|
+
}
|
|
97
|
+
let assistantSeen = this.assistantSeen;
|
|
98
|
+
for (let i = prefix; i < messages.length; i++) {
|
|
99
|
+
const message = messages[i];
|
|
100
|
+
if (message.role === 'You')
|
|
101
|
+
assistantSeen = false;
|
|
102
|
+
const heading = !message.compact && (message.role !== 'Assistant' || !assistantSeen);
|
|
103
|
+
if (message.role === 'Assistant' && heading)
|
|
104
|
+
assistantSeen = true;
|
|
105
|
+
const reasoning = this.overrides.has(message.seq) ? this.reasoning === 'row' ? 'full' : 'row' : this.reasoning;
|
|
106
|
+
const cached = this.heights.get(message);
|
|
107
|
+
const count = cached?.width === this.width && cached.reasoning === reasoning && cached.heading === heading ? cached.count : this.rows(message, reasoning, heading).length;
|
|
108
|
+
this.heights.set(message, { width: this.width, reasoning, heading, count });
|
|
109
|
+
this.offsets.set(message.seq, this.length);
|
|
110
|
+
this.segments.push({ message, start: this.length, count, reasoning, heading, assistantSeen });
|
|
111
|
+
this.length += count;
|
|
112
|
+
}
|
|
113
|
+
this.messages = messages;
|
|
114
|
+
}
|
|
115
|
+
viewport(start, end) {
|
|
116
|
+
let lo = 0, hi = this.segments.length;
|
|
117
|
+
while (lo < hi) {
|
|
118
|
+
const middle = (lo + hi) >>> 1;
|
|
119
|
+
const segment = this.segments[middle];
|
|
120
|
+
if (segment.start + segment.count <= start)
|
|
121
|
+
lo = middle + 1;
|
|
122
|
+
else
|
|
123
|
+
hi = middle;
|
|
124
|
+
}
|
|
125
|
+
const rows = [];
|
|
126
|
+
for (let i = lo; i < this.segments.length; i++) {
|
|
127
|
+
const segment = this.segments[i];
|
|
128
|
+
if (segment.start >= end)
|
|
129
|
+
break;
|
|
130
|
+
rows.push(...this.rows(segment.message, segment.reasoning, segment.heading).slice(Math.max(0, start - segment.start), end - segment.start));
|
|
131
|
+
}
|
|
132
|
+
return rows;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const liveRows = new WeakMap();
|
|
136
|
+
/** Wrap text into rows with the per-kind trimming rule.
|
|
137
|
+
* @param text - Text to wrap.
|
|
138
|
+
* @param width - Available terminal columns.
|
|
139
|
+
* @param kind - Part kind, which decides whether wrapped lines are trimmed.
|
|
140
|
+
* @param seq - Durable sequence, for committed parts.
|
|
141
|
+
* @returns The wrapped rows.
|
|
142
|
+
*/
|
|
143
|
+
function wrapRows(text, width, kind, seq) {
|
|
144
|
+
return wrapAnsi(text, width, { hard: true, trim: !['tool', 'success', 'error'].includes(kind) })
|
|
145
|
+
.split('\n').map(text => ({ text, kind, seq }));
|
|
146
|
+
}
|
|
147
|
+
/** Bound the source inspected for a folded row, which renders only the first `width` columns.
|
|
148
|
+
* @param text - Complete live text.
|
|
149
|
+
* @param width - Available terminal columns.
|
|
150
|
+
* @returns A prefix that cannot change the folded row.
|
|
151
|
+
*/
|
|
152
|
+
function foldedSource(text, width) {
|
|
153
|
+
let limit = width * 4 + 64;
|
|
154
|
+
while (limit < text.length && toolLine(text.slice(0, limit), width).length < width)
|
|
155
|
+
limit *= 2;
|
|
156
|
+
return limit >= text.length ? text : text.slice(0, limit);
|
|
157
|
+
}
|
|
158
|
+
/** Locate the start of one rendered row inside the source it was wrapped from.
|
|
159
|
+
*
|
|
160
|
+
* Trim removes the whitespace runs at both ends of a row, so a rendered row is not a contiguous
|
|
161
|
+
* slice; matching it backwards while skipping source whitespace recovers where it began. The scan
|
|
162
|
+
* gives up once it would have to skip more than `limit` characters, which sends the caller back to
|
|
163
|
+
* a whole-text wrap instead of scanning an unbounded whitespace run.
|
|
164
|
+
* @param pending - Source text the row was wrapped from.
|
|
165
|
+
* @param row - One rendered row from that wrap.
|
|
166
|
+
* @param limit - Maximum characters the backward scan may skip.
|
|
167
|
+
* @returns The row's start offset, or undefined when it cannot be located.
|
|
168
|
+
*/
|
|
169
|
+
function rawStart(pending, row, limit) {
|
|
170
|
+
let source = pending.length - 1;
|
|
171
|
+
let target = row.length - 1;
|
|
172
|
+
const floor = pending.length - limit;
|
|
173
|
+
while (target >= 0 && source >= floor) {
|
|
174
|
+
if (pending[source] === row[target]) {
|
|
175
|
+
source--;
|
|
176
|
+
target--;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (/\s/u.test(pending[source])) {
|
|
180
|
+
source--;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
return target < 0 ? source + 1 : undefined;
|
|
186
|
+
}
|
|
187
|
+
/** Wrap one live part incrementally, re-wrapping only rows that can still change plus the new delta.
|
|
188
|
+
*
|
|
189
|
+
* A streaming part only grows, so every row before its last non-empty one is final. The state keeps
|
|
190
|
+
* those rows plus the raw source of the unfinished remainder, and re-anchors from the whole text
|
|
191
|
+
* only when that remainder cannot be located or outgrows a few widths.
|
|
192
|
+
* @param part - One live part carrying a stable `key`.
|
|
193
|
+
* @param state - Per-layout state keyed by that identity.
|
|
194
|
+
* @param width - Available terminal columns.
|
|
195
|
+
* @param reasoning - Fold mode for completed live reasoning.
|
|
196
|
+
* @returns The part's rows.
|
|
197
|
+
*/
|
|
198
|
+
function livePartRows(part, state, width, reasoning) {
|
|
199
|
+
const key = part.key;
|
|
200
|
+
if (key === undefined)
|
|
201
|
+
return partRows([part], width, reasoning);
|
|
202
|
+
if (part.kind === 'reasoning' && reasoning === 'row' && (part.closed || width < 60)) {
|
|
203
|
+
state.delete(key);
|
|
204
|
+
return wrapRows(toolLine(`◇ /think live · ${foldedSource(part.text, width).slice(2)}`, width), width, 'reasoning');
|
|
205
|
+
}
|
|
206
|
+
const previous = state.get(key);
|
|
207
|
+
const extend = previous !== undefined && previous.width === width && previous.reasoning === reasoning
|
|
208
|
+
&& previous.kind === part.kind && part.text.length >= previous.length;
|
|
209
|
+
const trim = !['tool', 'success', 'error'].includes(part.kind);
|
|
210
|
+
const limit = width * 8 + 512;
|
|
211
|
+
const attempt = (source, prior) => {
|
|
212
|
+
const wrapped = wrapAnsi(source, width, { hard: true, trim }).split('\n');
|
|
213
|
+
const filled = wrapped.reduce((found, text, index) => text === '' ? found : index, -1);
|
|
214
|
+
if (filled < 0)
|
|
215
|
+
return { rows: [], tail: wrapped, carry: source };
|
|
216
|
+
const start = rawStart(source, wrapped[filled], limit);
|
|
217
|
+
if (start === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
return {
|
|
220
|
+
rows: [...prior, ...wrapped.slice(0, filled).map(text => ({ text, kind: part.kind }))],
|
|
221
|
+
tail: wrapped.slice(filled),
|
|
222
|
+
// The last non-empty row and everything after it, including trailing empty rows, stay unfinished.
|
|
223
|
+
carry: source.slice(start),
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
let result = attempt(extend ? previous.carry + part.text.slice(previous.length) : part.text, extend ? previous.rows : []);
|
|
227
|
+
if (result === undefined || result.carry.length > width * 4 + 64)
|
|
228
|
+
result = attempt(part.text, []);
|
|
229
|
+
// Recovery can fail on text whose last row cannot be located; the plain one-shot wrap is the fallback.
|
|
230
|
+
if (result === undefined) {
|
|
231
|
+
state.delete(key);
|
|
232
|
+
return partRows([part], width, reasoning);
|
|
233
|
+
}
|
|
234
|
+
const rows = [...result.rows, ...result.tail.map(text => ({ text, kind: part.kind }))];
|
|
235
|
+
state.set(key, { width, reasoning, kind: part.kind, length: part.text.length, rows: result.rows, carry: result.carry });
|
|
236
|
+
return rows;
|
|
237
|
+
}
|
|
238
|
+
function partRows(parts, width, reasoning, seq) {
|
|
239
|
+
return parts.flatMap(part => {
|
|
240
|
+
const cached = seq === undefined ? liveRows.get(part) : undefined;
|
|
241
|
+
if (cached?.width === width && cached.reasoning === reasoning)
|
|
242
|
+
return cached.rows;
|
|
243
|
+
const fold = part.kind === 'reasoning' && reasoning === 'row' && (seq !== undefined || part.closed || width < 60);
|
|
244
|
+
const text = fold ? toolLine(`◇ /think${seq === undefined ? ' live' : ` ${seq}`} · ${part.text.slice(2)}`, width) : part.text;
|
|
245
|
+
const rows = part.kind === 'text' ? markdownRows(text, width).map(row => ({ ...row, kind: part.kind, seq }))
|
|
246
|
+
: wrapRows(text, width, part.kind, seq);
|
|
247
|
+
if (seq === undefined)
|
|
248
|
+
liveRows.set(part, { width, reasoning, rows });
|
|
249
|
+
return rows;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function messageRows(message, width, reasoning, heading) {
|
|
253
|
+
const kind = message.role === 'You' ? 'user' : message.role === 'Context' ? 'context' : 'assistant';
|
|
254
|
+
const label = kind === 'user' ? '❯ User' : kind === 'context' ? '◆ Context' : '✦ Assistant';
|
|
255
|
+
return [
|
|
256
|
+
...(!heading ? [] : [{ text: label, kind, bold: true, seq: message.seq }]),
|
|
257
|
+
...partRows(message.parts, width, reasoning, message.seq),
|
|
258
|
+
{ text: '', kind: 'muted', seq: message.seq },
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
const indexes = new WeakMap();
|
|
262
|
+
const noOverrides = new Set();
|
|
263
|
+
/** Drop all terminal rows and layout metadata for an evicted or inactive transcript.
|
|
264
|
+
* @param transcript - Transcript whose previously returned layout is no longer used.
|
|
265
|
+
*/
|
|
266
|
+
export function releaseHistoryLayout(transcript) {
|
|
267
|
+
indexes.get(transcript)?.dispose();
|
|
268
|
+
indexes.delete(transcript);
|
|
269
|
+
}
|
|
270
|
+
/** Report what one transcript's layout still holds, for memory samples and diagnostics.
|
|
271
|
+
*
|
|
272
|
+
* The row cache is the only structure that grows with expanded reasoning and Markdown rows, and
|
|
273
|
+
* its byte bound counts neither the span objects nor the incremental live-tail state.
|
|
274
|
+
* @param transcript - Transcript whose layout should be measured.
|
|
275
|
+
* @returns Cache and live-tail counters, or undefined when no layout was built yet.
|
|
276
|
+
*/
|
|
277
|
+
export function layoutStats(transcript) {
|
|
278
|
+
const index = indexes.get(transcript);
|
|
279
|
+
if (!index)
|
|
280
|
+
return undefined;
|
|
281
|
+
return { ...index.stats(), liveWraps: index.liveWraps.size, liveMarkdown: index.liveMarkdown.size };
|
|
282
|
+
}
|
|
283
|
+
/** Lay out an indexed conversation without concatenating its historical rows on every stream frame.
|
|
284
|
+
* @param transcript - Selected session's semantic content and unfinished assistant output.
|
|
285
|
+
* @param width - Available terminal columns.
|
|
286
|
+
* @param reasoning - Global committed/completed reasoning fold mode.
|
|
287
|
+
* @param overrides - Sequences whose fold mode differs from the global mode; replace the set on changes.
|
|
288
|
+
* @param liveReasoning - Fold mode for completed live blocks; below 60 content columns it also folds active reasoning.
|
|
289
|
+
* @returns Row count, sequence offsets, and a viewport reader; `lines` materializes all rows for exports only.
|
|
290
|
+
*/
|
|
291
|
+
export function historyLayout(transcript, width, reasoning = 'row', overrides = noOverrides, liveReasoning = reasoning) {
|
|
292
|
+
let index = indexes.get(transcript);
|
|
293
|
+
if (!index || index.width !== width || index.reasoning !== reasoning || index.overrides !== overrides) {
|
|
294
|
+
index = new LayoutIndex(width, reasoning, overrides);
|
|
295
|
+
indexes.set(transcript, index);
|
|
296
|
+
}
|
|
297
|
+
index.update(transcript.messagesForWidth(width));
|
|
298
|
+
const live = transcript.liveParts(width);
|
|
299
|
+
const keys = new Set(live.map(part => part.key));
|
|
300
|
+
for (const key of index.liveWraps.keys())
|
|
301
|
+
if (!keys.has(key))
|
|
302
|
+
index.liveWraps.delete(key);
|
|
303
|
+
for (const key of index.liveMarkdown.keys())
|
|
304
|
+
if (!keys.has(key))
|
|
305
|
+
index.liveMarkdown.delete(key);
|
|
306
|
+
const streamed = live.length ? [
|
|
307
|
+
...(transcript.liveToolOnly || index.assistantSeen ? [] : [{ text: '✦ Assistant · streaming', kind: 'assistant', bold: true }]),
|
|
308
|
+
...live.flatMap(part => {
|
|
309
|
+
if (part.kind === 'text') {
|
|
310
|
+
const previous = part.key === undefined ? undefined : index.liveMarkdown.get(part.key);
|
|
311
|
+
const extendsPrevious = previous && part.text.length >= previous.length;
|
|
312
|
+
// Include the preceding line so a split list marker or blank line can change Markdown parsing.
|
|
313
|
+
const start = extendsPrevious ? part.text.lastIndexOf('\n', Math.max(0, previous.length - 2)) + 1 : 0;
|
|
314
|
+
const rich = !!(extendsPrevious && previous.rich) || hasMarkdown(part.text.slice(start));
|
|
315
|
+
if (part.key !== undefined)
|
|
316
|
+
index.liveMarkdown.set(part.key, { length: part.text.length, rich });
|
|
317
|
+
if (rich) {
|
|
318
|
+
if (part.key !== undefined)
|
|
319
|
+
index.liveWraps.delete(part.key);
|
|
320
|
+
return partRows([part], width, liveReasoning);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return livePartRows(part, index.liveWraps, width, liveReasoning);
|
|
324
|
+
}),
|
|
325
|
+
] : [];
|
|
326
|
+
const committed = index;
|
|
327
|
+
const length = committed.length + streamed.length;
|
|
328
|
+
const viewport = (start, end) => {
|
|
329
|
+
start = Math.max(0, start);
|
|
330
|
+
end = Math.min(length, end);
|
|
331
|
+
if (end <= start)
|
|
332
|
+
return [];
|
|
333
|
+
return [...committed.viewport(start, end), ...streamed.slice(Math.max(0, start - committed.length), Math.max(0, end - committed.length))];
|
|
334
|
+
};
|
|
335
|
+
return { messages: index.messages, offsets: index.offsets, first: transcript.beforeSeq, length, liveOffset: committed.length, viewport,
|
|
336
|
+
get cachedRowCount() { return committed.cachedRowCount; },
|
|
337
|
+
get lines() { return viewport(0, length).map(row => row.text); } };
|
|
338
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Session domain: the selected session, its transcript, layout, telemetry and navigation. */
|
|
2
|
+
export { SessionController } from './controller.ts';
|
|
3
|
+
export { contentText, toolLine, Transcript } from './transcript.ts';
|
|
4
|
+
export type { LivePhase, Message, MessagePart, ThoughtEntry } from './transcript.ts';
|
|
5
|
+
export { historyLayout, layoutStats, releaseHistoryLayout } from './history.ts';
|
|
6
|
+
export { markdownCacheStats } from './markdown.ts';
|
|
7
|
+
export type { HistoryRow, Reasoning, RowKind } from './history.ts';
|
|
8
|
+
export { Telemetry } from './telemetry.ts';
|
|
9
|
+
export type { QueuedInput } from './telemetry.ts';
|
|
10
|
+
export { DEFAULT_HISTORY_LIMITS, historyLimits } from './memory.ts';
|
|
11
|
+
export type { HistoryLimits } from './memory.ts';
|
|
12
|
+
export { navigationCommand, resolveTarget, sessionLabel } from './navigation.ts';
|
|
13
|
+
export { activeReference, fileMention, fileReferences } from './references.ts';
|
|
14
|
+
export type { FileReference } from './references.ts';
|
|
15
|
+
export { saveSessionLog } from './export.ts';
|
|
16
|
+
export type { HistorySearch, RemovalTarget } from './types.ts';
|
|
17
|
+
export type { ConnectionView } from './connection-view.ts';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Session domain: the selected session, its transcript, layout, telemetry and navigation. */
|
|
2
|
+
export { SessionController } from "./controller.js";
|
|
3
|
+
export { contentText, toolLine, Transcript } from "./transcript.js";
|
|
4
|
+
export { historyLayout, layoutStats, releaseHistoryLayout } from "./history.js";
|
|
5
|
+
export { markdownCacheStats } from "./markdown.js";
|
|
6
|
+
export { Telemetry } from "./telemetry.js";
|
|
7
|
+
export { DEFAULT_HISTORY_LIMITS, historyLimits } from "./memory.js";
|
|
8
|
+
export { navigationCommand, resolveTarget, sessionLabel } from "./navigation.js";
|
|
9
|
+
export { activeReference, fileMention, fileReferences } from "./references.js";
|
|
10
|
+
export { saveSessionLog } from "./export.js";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Local styles only; remote control sequences never become terminal instructions. */
|
|
2
|
+
export interface MarkdownSpan {
|
|
3
|
+
text: string;
|
|
4
|
+
bold?: boolean;
|
|
5
|
+
italic?: boolean;
|
|
6
|
+
underline?: boolean;
|
|
7
|
+
strikethrough?: boolean;
|
|
8
|
+
inverse?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** A measured row retains plain text for searching, copying and row geometry. */
|
|
11
|
+
export interface MarkdownRow {
|
|
12
|
+
text: string;
|
|
13
|
+
spans?: MarkdownSpan[];
|
|
14
|
+
}
|
|
15
|
+
/** Report the bounded math and diagram cache, so a memory sample can tell churn from retention.
|
|
16
|
+
* @returns Cached entries, their accounted characters, and lookup counters.
|
|
17
|
+
*/
|
|
18
|
+
export declare function markdownCacheStats(): {
|
|
19
|
+
entries: number;
|
|
20
|
+
chars: number;
|
|
21
|
+
hits: number;
|
|
22
|
+
misses: number;
|
|
23
|
+
};
|
|
24
|
+
/** Whether a source can contain Markdown constructs; false preserves incremental plain-text wrapping.
|
|
25
|
+
* @param source - Complete text or a delta with its preceding line.
|
|
26
|
+
* @returns Whether Markdown lexing is required.
|
|
27
|
+
*/
|
|
28
|
+
export declare function hasMarkdown(source: string): boolean;
|
|
29
|
+
/** Parse GFM and TeX into styled rows whose widths match the terminal viewport.
|
|
30
|
+
* @param source - Model or user message; remote terminal controls are removed.
|
|
31
|
+
* @param width - Available terminal columns.
|
|
32
|
+
* @returns Terminal rows, with literal source for invalid or unsupported rich blocks.
|
|
33
|
+
*/
|
|
34
|
+
export declare function markdownRows(source: string, width: number): MarkdownRow[];
|
|
35
|
+
/** Render safe, offline HTML with Mermaid SVG images and MathJax-generated MathML.
|
|
36
|
+
* @param source - Markdown source, never trusted as executable HTML.
|
|
37
|
+
* @returns An HTML fragment that requires no scripts or remote assets.
|
|
38
|
+
*/
|
|
39
|
+
export declare function markdownHtml(source: string): string;
|