@softov/ahpc 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/src/ahp/fake.js +61 -14
- package/dist/src/ahp/live.d.ts +7 -0
- package/dist/src/ahp/live.js +93 -37
- package/dist/src/ahp/publish.js +13 -0
- package/dist/src/ahp/types.d.ts +20 -0
- package/dist/src/app.js +34 -7
- package/dist/src/blocks.d.ts +4 -74
- package/dist/src/blocks.js +10 -48
- package/dist/src/cli/main.d.ts +1 -1
- package/dist/src/cli/main.js +75 -2
- package/dist/src/connect.d.ts +2 -0
- package/dist/src/connect.js +1 -0
- package/dist/src/control.d.ts +6 -0
- package/dist/src/control.js +140 -10
- package/dist/src/flags.js +1 -1
- package/dist/src/links.d.ts +54 -0
- package/dist/src/links.js +120 -0
- package/dist/src/resources.d.ts +13 -0
- package/dist/src/resources.js +46 -0
- package/dist/src/screens.js +95 -50
- package/dist/src/state.d.ts +42 -0
- package/dist/src/state.js +80 -1
- package/dist/src/tui.d.ts +3 -1
- package/dist/src/tui.js +28 -4
- package/dist/src/view/creature.d.ts +0 -12
- package/dist/src/view/creature.js +0 -20
- package/dist/src/view/wire.d.ts +36 -0
- package/dist/src/view/wire.js +196 -0
- package/dist/src/wire.d.ts +70 -0
- package/dist/src/wire.js +194 -0
- package/dist/src/wiretui.d.ts +22 -0
- package/dist/src/wiretui.js +69 -0
- package/package.json +6 -5
- package/dist/src/diff.d.ts +0 -44
- package/dist/src/diff.js +0 -111
- package/dist/src/view/bubble.d.ts +0 -75
- package/dist/src/view/bubble.js +0 -86
- package/dist/src/view/composer.d.ts +0 -64
- package/dist/src/view/composer.js +0 -192
- package/dist/src/view/controls.d.ts +0 -44
- package/dist/src/view/controls.js +0 -49
- package/dist/src/view/details.d.ts +0 -65
- package/dist/src/view/details.js +0 -65
- package/dist/src/view/filediff.d.ts +0 -29
- package/dist/src/view/filediff.js +0 -24
- package/dist/src/view/hitl.d.ts +0 -43
- package/dist/src/view/hitl.js +0 -171
- package/dist/src/view/icons.d.ts +0 -13
- package/dist/src/view/icons.js +0 -71
- package/dist/src/view/picker.d.ts +0 -42
- package/dist/src/view/picker.js +0 -71
- package/dist/src/view/sessionhead.d.ts +0 -41
- package/dist/src/view/sessionhead.js +0 -60
- package/dist/src/view/sessions.d.ts +0 -34
- package/dist/src/view/sessions.js +0 -61
- package/dist/src/view/toolcall.d.ts +0 -27
- package/dist/src/view/toolcall.js +0 -48
- package/dist/src/view/transcript.d.ts +0 -50
- package/dist/src/view/transcript.js +0 -60
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ahpc wire <file>`: a capture, watched.
|
|
3
|
+
*
|
|
4
|
+
* A screen with no host behind it. On a terminal it is the wire screen,
|
|
5
|
+
* reading the file as it grows; anywhere else it is one line per frame to
|
|
6
|
+
* stdout, which is what a shell wants from it. Loaded only when asked for,
|
|
7
|
+
* for the same reason the conversation screen is: it pulls in a renderer,
|
|
8
|
+
* and `ahpc session list` should not pay for one.
|
|
9
|
+
*/
|
|
10
|
+
import { WRITER_KEY, createApp } from '@textui/core';
|
|
11
|
+
import { createNodeTerminal, createWriter } from '@textui/terminal';
|
|
12
|
+
import { registerWire } from './view/wire.js';
|
|
13
|
+
import { follow, matches, rowText } from './wire.js';
|
|
14
|
+
/** The screen, or the lines. */
|
|
15
|
+
export async function wireTui(options) {
|
|
16
|
+
if (!process.stdout.isTTY || options.json) {
|
|
17
|
+
await new Promise((resolve) => {
|
|
18
|
+
let quiet;
|
|
19
|
+
const done = () => { reader.close(); resolve(); };
|
|
20
|
+
const reader = follow(options.file, (rows) => {
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
if (options.filter !== undefined && !matches(row, options.filter))
|
|
23
|
+
continue;
|
|
24
|
+
process.stdout.write(options.json ? `${JSON.stringify(row)}\n` : `${rowText(row)}\n`);
|
|
25
|
+
}
|
|
26
|
+
if (!options.follow) {
|
|
27
|
+
if (quiet)
|
|
28
|
+
clearTimeout(quiet);
|
|
29
|
+
quiet = setTimeout(done, 50);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
// Without `--follow`, what is there now and then out: the first read
|
|
33
|
+
// is synchronous, so nothing arriving in the next moment means the
|
|
34
|
+
// file has been read to its end.
|
|
35
|
+
if (!options.follow && quiet === undefined)
|
|
36
|
+
quiet = setTimeout(done, 50);
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const terminal = createNodeTerminal();
|
|
41
|
+
const app = createApp({
|
|
42
|
+
terminal,
|
|
43
|
+
...(options.theme ? { theme: options.theme } : {}),
|
|
44
|
+
...(options.shell ? { shell: options.shell } : {}),
|
|
45
|
+
session: { managed: true, altScreen: true, mouse: true, title: 'wire' },
|
|
46
|
+
onBoot: (booted) => {
|
|
47
|
+
registerWire(booted, { file: options.file });
|
|
48
|
+
booted.commands.register({
|
|
49
|
+
id: 'app.quit',
|
|
50
|
+
title: 'Quit',
|
|
51
|
+
slots: ['palette'],
|
|
52
|
+
run: () => void app.stop().then(() => process.exit(0)),
|
|
53
|
+
});
|
|
54
|
+
booted.keybindings.register({ keys: 'ctrl+c', commandId: 'app.quit' });
|
|
55
|
+
booted.keybindings.register({ keys: 'ctrl+q', commandId: 'app.quit' });
|
|
56
|
+
booted.keybindings.register({ keys: 'q', commandId: 'app.quit', scopeId: 'wire.rows' });
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
app.services.provide(WRITER_KEY, createWriter(terminal.capabilities()));
|
|
60
|
+
await app.start();
|
|
61
|
+
const bail = (label) => (error) => {
|
|
62
|
+
void app.stop().finally(() => {
|
|
63
|
+
process.stderr.write(`${label}: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
process.on('unhandledRejection', bail('Unhandled rejection'));
|
|
68
|
+
process.on('uncaughtException', bail('Uncaught exception'));
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softov/ahpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A terminal client for the Agent Host Protocol: sessions, a streaming transcript, and interactive prompts",
|
|
6
6
|
"keywords": [
|
|
@@ -54,12 +54,13 @@
|
|
|
54
54
|
},
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@microsoft/agent-host-protocol": "^0.9.0",
|
|
57
|
-
"@textui/
|
|
58
|
-
"@textui/
|
|
59
|
-
"@textui/
|
|
57
|
+
"@textui/chat": "^0.6.1",
|
|
58
|
+
"@textui/core": "^0.6.1",
|
|
59
|
+
"@textui/terminal": "^0.6.1",
|
|
60
|
+
"@textui/widgets": "^0.6.1"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
|
-
"@textui/testing": "^0.
|
|
63
|
+
"@textui/testing": "^0.6.1",
|
|
63
64
|
"@types/node": "^22.10.2",
|
|
64
65
|
"ajv": "^8.17.1",
|
|
65
66
|
"ajv-formats": "^3.0.1",
|
package/dist/src/diff.d.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A line diff, small enough to read.
|
|
3
|
-
*
|
|
4
|
-
* The host sends two whole files and a count of what changed between them; it
|
|
5
|
-
* does not send the diff itself, so somebody has to work out which lines those
|
|
6
|
-
* were. This is that, and it is deliberately the textbook algorithm rather
|
|
7
|
-
* than anything clever: the longest common subsequence of the two line arrays,
|
|
8
|
-
* with everything not in it marked as removed on the left or added on the
|
|
9
|
-
* right.
|
|
10
|
-
*
|
|
11
|
-
* The cost is quadratic in the number of lines, which is why `diffLines` takes
|
|
12
|
-
* a ceiling. Two files of ten thousand lines each is a hundred million cells
|
|
13
|
-
* and a terminal that stops answering, and the honest answer at that size is
|
|
14
|
-
* to say the files are too big rather than to spend a minute proving it.
|
|
15
|
-
*/
|
|
16
|
-
export type DiffKind = 'same' | 'added' | 'removed';
|
|
17
|
-
export interface DiffRow {
|
|
18
|
-
kind: DiffKind;
|
|
19
|
-
/** 1-based, on the side this row exists on. Absent on the side it does not. */
|
|
20
|
-
before?: number;
|
|
21
|
-
after?: number;
|
|
22
|
-
text: string;
|
|
23
|
-
}
|
|
24
|
-
export interface DiffResult {
|
|
25
|
-
rows: DiffRow[];
|
|
26
|
-
added: number;
|
|
27
|
-
removed: number;
|
|
28
|
-
/** Set instead of a diff when the pair was over `limit`. */
|
|
29
|
-
tooLarge?: {
|
|
30
|
-
lines: number;
|
|
31
|
-
limit: number;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/** Lines of a file, with the trailing newline not counted as an empty last line. */
|
|
35
|
-
export declare function toLines(text: string): string[];
|
|
36
|
-
/**
|
|
37
|
-
* The two sides, lined up.
|
|
38
|
-
*
|
|
39
|
-
* A creation has no `before` and a deletion no `after`; both are passed as an
|
|
40
|
-
* empty string rather than as a special case, because "every line is an
|
|
41
|
-
* addition" is exactly the right diff for a new file and needs no branch of
|
|
42
|
-
* its own.
|
|
43
|
-
*/
|
|
44
|
-
export declare function diffLines(before: string, after: string, limit?: number): DiffResult;
|
package/dist/src/diff.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A line diff, small enough to read.
|
|
3
|
-
*
|
|
4
|
-
* The host sends two whole files and a count of what changed between them; it
|
|
5
|
-
* does not send the diff itself, so somebody has to work out which lines those
|
|
6
|
-
* were. This is that, and it is deliberately the textbook algorithm rather
|
|
7
|
-
* than anything clever: the longest common subsequence of the two line arrays,
|
|
8
|
-
* with everything not in it marked as removed on the left or added on the
|
|
9
|
-
* right.
|
|
10
|
-
*
|
|
11
|
-
* The cost is quadratic in the number of lines, which is why `diffLines` takes
|
|
12
|
-
* a ceiling. Two files of ten thousand lines each is a hundred million cells
|
|
13
|
-
* and a terminal that stops answering, and the honest answer at that size is
|
|
14
|
-
* to say the files are too big rather than to spend a minute proving it.
|
|
15
|
-
*/
|
|
16
|
-
/** Lines of a file, with the trailing newline not counted as an empty last line. */
|
|
17
|
-
export function toLines(text) {
|
|
18
|
-
if (text === '')
|
|
19
|
-
return [];
|
|
20
|
-
const lines = text.split('\n');
|
|
21
|
-
if (lines[lines.length - 1] === '')
|
|
22
|
-
lines.pop();
|
|
23
|
-
return lines;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* The two sides, lined up.
|
|
27
|
-
*
|
|
28
|
-
* A creation has no `before` and a deletion no `after`; both are passed as an
|
|
29
|
-
* empty string rather than as a special case, because "every line is an
|
|
30
|
-
* addition" is exactly the right diff for a new file and needs no branch of
|
|
31
|
-
* its own.
|
|
32
|
-
*/
|
|
33
|
-
export function diffLines(before, after, limit = 4000) {
|
|
34
|
-
const a = toLines(before);
|
|
35
|
-
const b = toLines(after);
|
|
36
|
-
if (a.length + b.length > limit) {
|
|
37
|
-
return { rows: [], added: 0, removed: 0, tooLarge: { lines: a.length + b.length, limit } };
|
|
38
|
-
}
|
|
39
|
-
// The common head and tail first. Two files that differ in one line share
|
|
40
|
-
// everything either side of it, and taking those off shrinks the table the
|
|
41
|
-
// quadratic part has to fill to the part that actually differs.
|
|
42
|
-
let head = 0;
|
|
43
|
-
while (head < a.length && head < b.length && a[head] === b[head])
|
|
44
|
-
head++;
|
|
45
|
-
let tail = 0;
|
|
46
|
-
while (tail < a.length - head
|
|
47
|
-
&& tail < b.length - head
|
|
48
|
-
&& a[a.length - 1 - tail] === b[b.length - 1 - tail])
|
|
49
|
-
tail++;
|
|
50
|
-
const midA = a.slice(head, a.length - tail);
|
|
51
|
-
const midB = b.slice(head, b.length - tail);
|
|
52
|
-
const table = lcs(midA, midB);
|
|
53
|
-
const rows = [];
|
|
54
|
-
let added = 0;
|
|
55
|
-
let removed = 0;
|
|
56
|
-
const push = (kind, text, ai, bi) => {
|
|
57
|
-
rows.push({
|
|
58
|
-
kind,
|
|
59
|
-
...(kind !== 'added' ? { before: ai + 1 } : {}),
|
|
60
|
-
...(kind !== 'removed' ? { after: bi + 1 } : {}),
|
|
61
|
-
text,
|
|
62
|
-
});
|
|
63
|
-
if (kind === 'added')
|
|
64
|
-
added++;
|
|
65
|
-
if (kind === 'removed')
|
|
66
|
-
removed++;
|
|
67
|
-
};
|
|
68
|
-
for (let i = 0; i < head; i++)
|
|
69
|
-
push('same', a[i], i, i);
|
|
70
|
-
// Walking the table forwards, so the rows come out in file order.
|
|
71
|
-
let i = 0;
|
|
72
|
-
let j = 0;
|
|
73
|
-
while (i < midA.length || j < midB.length) {
|
|
74
|
-
if (i < midA.length && j < midB.length && midA[i] === midB[j]) {
|
|
75
|
-
push('same', midA[i], head + i, head + j);
|
|
76
|
-
i++;
|
|
77
|
-
j++;
|
|
78
|
-
// A tie goes to the removal, so a replaced line reads `-old` then `+new`
|
|
79
|
-
// the way every other diff on the machine prints it. With `>=` here the
|
|
80
|
-
// pair comes out the other way round, which is not wrong so much as
|
|
81
|
-
// unreadable next to `git diff`.
|
|
82
|
-
}
|
|
83
|
-
else if (j < midB.length && (i === midA.length || (table[i]?.[j + 1] ?? 0) > (table[i + 1]?.[j] ?? 0))) {
|
|
84
|
-
push('added', midB[j], head + i, head + j);
|
|
85
|
-
j++;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
push('removed', midA[i], head + i, head + j);
|
|
89
|
-
i++;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
for (let k = 0; k < tail; k++) {
|
|
93
|
-
push('same', a[a.length - tail + k], a.length - tail + k, b.length - tail + k);
|
|
94
|
-
}
|
|
95
|
-
return { rows, added, removed };
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* `table[i][j]` is the length of the longest common subsequence of `a[i..]`
|
|
99
|
-
* and `b[j..]`, filled from the end so the walk above can go forwards.
|
|
100
|
-
*/
|
|
101
|
-
function lcs(a, b) {
|
|
102
|
-
const table = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
103
|
-
for (let i = a.length - 1; i >= 0; i--) {
|
|
104
|
-
for (let j = b.length - 1; j >= 0; j--) {
|
|
105
|
-
table[i][j] = a[i] === b[j]
|
|
106
|
-
? (table[i + 1]?.[j + 1] ?? 0) + 1
|
|
107
|
-
: Math.max(table[i + 1]?.[j] ?? 0, table[i]?.[j + 1] ?? 0);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
return table;
|
|
111
|
-
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import type { BoxProps, RenderOutput, SemanticVariant } from '@textui/core';
|
|
2
|
-
/**
|
|
3
|
-
* One thing said, and the two ways it is still being said.
|
|
4
|
-
*
|
|
5
|
-
* A bubble in a terminal is not a rounded rectangle. It is a gutter that says
|
|
6
|
-
* who is speaking and a body that owns the rest of the width - because the
|
|
7
|
-
* width is 80 cells and half of it spent on alignment is half the conversation
|
|
8
|
-
* gone.
|
|
9
|
-
*/
|
|
10
|
-
export type Speaker = 'user' | 'agent' | 'system';
|
|
11
|
-
/**
|
|
12
|
-
* The rule down the left of everything one speaker said.
|
|
13
|
-
*
|
|
14
|
-
* A box that fills rather than a `text`: the text is one row tall and the
|
|
15
|
-
* paragraph beside it is nine, so a rule written as a character marks the
|
|
16
|
-
* first line of a wrapped answer and abandons the rest of it.
|
|
17
|
-
*/
|
|
18
|
-
export declare const Gutter: (props: BoxProps) => RenderOutput;
|
|
19
|
-
export interface ChatBubbleProps extends BoxProps {
|
|
20
|
-
speaker: Speaker;
|
|
21
|
-
/** The name, when the speaker is not enough: a model, a person, a host. */
|
|
22
|
-
author?: string;
|
|
23
|
-
/** Right of the author line: a time, a duration, a model. */
|
|
24
|
-
meta?: string;
|
|
25
|
-
tone?: SemanticVariant;
|
|
26
|
-
/** The transcript's cursor is on this block. */
|
|
27
|
-
active?: boolean;
|
|
28
|
-
children?: unknown;
|
|
29
|
-
}
|
|
30
|
-
export declare const ChatBubble: (props: ChatBubbleProps) => RenderOutput;
|
|
31
|
-
export interface StreamingTextProps extends BoxProps {
|
|
32
|
-
content: string;
|
|
33
|
-
/** Still arriving. Draws a caret and keeps it on the last word. */
|
|
34
|
-
streaming?: boolean;
|
|
35
|
-
quiet?: boolean;
|
|
36
|
-
maxLines?: number;
|
|
37
|
-
/**
|
|
38
|
-
* Draw it as markdown, or as the characters that arrived.
|
|
39
|
-
*
|
|
40
|
-
* Unstated it follows the application's own switch, which is what the key
|
|
41
|
-
* that toggles it moves - so a caller has to say something here only when
|
|
42
|
-
* it wants one or the other regardless.
|
|
43
|
-
*/
|
|
44
|
-
markdown?: boolean;
|
|
45
|
-
/** Text to pick out, for the find box. Coloured wherever it appears. */
|
|
46
|
-
match?: string;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Text that is still being said.
|
|
50
|
-
*
|
|
51
|
-
* The caret is part of the content rather than a node beside it, because a
|
|
52
|
-
* caret placed after the block sits under the last line instead of at the end
|
|
53
|
-
* of it - and the end of the sentence is the only place it means anything.
|
|
54
|
-
*
|
|
55
|
-
* It blinks on the theme's own ticker, so animation being off (a pipe, a test,
|
|
56
|
-
* a `--static` capture) leaves a steady caret rather than a missing one.
|
|
57
|
-
*/
|
|
58
|
-
export declare const StreamingText: (props: StreamingTextProps) => RenderOutput;
|
|
59
|
-
export interface ReasoningBlockProps extends BoxProps {
|
|
60
|
-
/** Text to pick out, for the find box. Handed to the text inside it. */
|
|
61
|
-
match?: string;
|
|
62
|
-
content: string;
|
|
63
|
-
expanded?: boolean;
|
|
64
|
-
streaming?: boolean;
|
|
65
|
-
/** Shown collapsed: "thought for 12s". */
|
|
66
|
-
summary?: string;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* What the agent was thinking, folded away.
|
|
70
|
-
*
|
|
71
|
-
* Reasoning is prose the host sends like any other, and it is not what the
|
|
72
|
-
* reader came for - so it is one row until it is asked for. Dropping it
|
|
73
|
-
* instead loses the only account of *why* a turn did what it did.
|
|
74
|
-
*/
|
|
75
|
-
export declare const ReasoningBlock: (props: ReasoningBlockProps) => RenderOutput;
|
package/dist/src/view/bubble.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
|
|
2
|
-
import { defineComponent, useFrame, useStoreValue, useTheme } from '@textui/core';
|
|
3
|
-
import { Column, MarkdownView, Row } from '@textui/widgets';
|
|
4
|
-
import { MARKDOWN } from '../state.js';
|
|
5
|
-
/**
|
|
6
|
-
* The rule down the left of everything one speaker said.
|
|
7
|
-
*
|
|
8
|
-
* A box that fills rather than a `text`: the text is one row tall and the
|
|
9
|
-
* paragraph beside it is nine, so a rule written as a character marks the
|
|
10
|
-
* first line of a wrapped answer and abandons the rest of it.
|
|
11
|
-
*/
|
|
12
|
-
export const Gutter = defineComponent('ChatGutter', (props) => {
|
|
13
|
-
const theme = useTheme();
|
|
14
|
-
// `alignSelf` because `Row` centres its children: a one-cell box in a
|
|
15
|
-
// centred row is one cell tall, wherever the rule was meant to reach.
|
|
16
|
-
return _jsx("box", { width: 1, alignSelf: "stretch", fill: theme.borderChars().left, fg: "borderSubtle", ...props });
|
|
17
|
-
});
|
|
18
|
-
const SPEAKER = {
|
|
19
|
-
user: { fg: 'primary', label: 'you' },
|
|
20
|
-
agent: { fg: 'accent', label: 'agent' },
|
|
21
|
-
system: { fg: 'muted', label: 'system' },
|
|
22
|
-
};
|
|
23
|
-
export const ChatBubble = defineComponent('ChatBubble', (props) => {
|
|
24
|
-
const { speaker, author, meta, tone, active, children, ...rest } = props;
|
|
25
|
-
const theme = useTheme();
|
|
26
|
-
const look = SPEAKER[speaker];
|
|
27
|
-
const glyph = speaker === 'user' ? theme.glyphs.chevronRight
|
|
28
|
-
: speaker === 'agent' ? theme.glyphs.bulletFilled
|
|
29
|
-
: theme.glyphs.info;
|
|
30
|
-
// The gutter is one column of glyph and one of rule. It is what makes a
|
|
31
|
-
// wrapped paragraph read as one person talking rather than as the page
|
|
32
|
-
// starting again, and it survives losing colour - which a tinted
|
|
33
|
-
// background does not.
|
|
34
|
-
return (_jsxs(Column, { ...rest, ...(active ? { bg: 'selected' } : {}), children: [_jsxs(Row, { gap: 1, children: [_jsx("text", { content: glyph, fg: tone ?? look.fg }), _jsx("text", { content: author ?? look.label, bold: true, fg: tone ?? look.fg }), meta ? _jsx("text", { content: meta, fg: "subtle", flex: 1, truncate: "end" }) : _jsx("text", { content: "", flex: 1 })] }), _jsxs(Row, { gap: 1, flex: 1, children: [_jsx(Gutter, {}), _jsx(Column, { flex: 1, gap: 1, children: children })] })] }));
|
|
35
|
-
});
|
|
36
|
-
/**
|
|
37
|
-
* Text that is still being said.
|
|
38
|
-
*
|
|
39
|
-
* The caret is part of the content rather than a node beside it, because a
|
|
40
|
-
* caret placed after the block sits under the last line instead of at the end
|
|
41
|
-
* of it - and the end of the sentence is the only place it means anything.
|
|
42
|
-
*
|
|
43
|
-
* It blinks on the theme's own ticker, so animation being off (a pipe, a test,
|
|
44
|
-
* a `--static` capture) leaves a steady caret rather than a missing one.
|
|
45
|
-
*/
|
|
46
|
-
export const StreamingText = defineComponent('StreamingText', (props) => {
|
|
47
|
-
const { content, streaming, quiet, maxLines, markdown, match, ...rest } = props;
|
|
48
|
-
const theme = useTheme();
|
|
49
|
-
// Only while something is arriving. A ticker marks its component dirty
|
|
50
|
-
// whether or not the frame it produces differs, so an unconditional one
|
|
51
|
-
// here meant every settled paragraph in the transcript asked the
|
|
52
|
-
// application to redraw twice a second, for ever - a conversation that
|
|
53
|
-
// got heavier to sit in the longer it got.
|
|
54
|
-
const frame = useFrame(2, { enabled: streaming === true });
|
|
55
|
-
const caret = streaming && frame % 2 === 0 ? theme.glyphs.caret : '';
|
|
56
|
-
// Read unconditionally: `??` short-circuits, and a hook that is only
|
|
57
|
-
// reached when a prop is absent is a hook that changes position between
|
|
58
|
-
// renders. The prop still wins - it is just decided after the read.
|
|
59
|
-
const preference = useStoreValue(MARKDOWN, true) ?? true;
|
|
60
|
-
const rendered = markdown ?? preference;
|
|
61
|
-
const shown = streaming ? `${content}${caret}` : content;
|
|
62
|
-
// Raw is a `text`, not a `MarkdownView` that was told not to parse: the
|
|
63
|
-
// point of turning it off is to see the characters that arrived, and
|
|
64
|
-
// anything that lays the document out has already decided some of them
|
|
65
|
-
// were structure. `wrap` rather than truncate, because the lines being
|
|
66
|
-
// read are the long ones - a fenced block and a table are exactly what is
|
|
67
|
-
// wider than the pane.
|
|
68
|
-
if (!rendered) {
|
|
69
|
-
return (_jsx("text", { content: shown, wrap: "word", ...(quiet ? { fg: 'muted' } : {}), ...(match ? { match } : {}), ...rest }));
|
|
70
|
-
}
|
|
71
|
-
return (_jsx(MarkdownView, { content: shown, ...(quiet ? { quiet: true } : {}), ...(maxLines !== undefined ? { maxLines } : {}), ...(match ? { match } : {}), ...rest }));
|
|
72
|
-
});
|
|
73
|
-
/**
|
|
74
|
-
* What the agent was thinking, folded away.
|
|
75
|
-
*
|
|
76
|
-
* Reasoning is prose the host sends like any other, and it is not what the
|
|
77
|
-
* reader came for - so it is one row until it is asked for. Dropping it
|
|
78
|
-
* instead loses the only account of *why* a turn did what it did.
|
|
79
|
-
*/
|
|
80
|
-
export const ReasoningBlock = defineComponent('ReasoningBlock', (props) => {
|
|
81
|
-
const { content, expanded, streaming, summary, match, ...rest } = props;
|
|
82
|
-
const theme = useTheme();
|
|
83
|
-
const chevron = expanded ? theme.glyphs.chevronDown : theme.glyphs.chevronRight;
|
|
84
|
-
const words = content.trim().split(/\s+/).filter(Boolean).length;
|
|
85
|
-
return (_jsxs(Column, { ...rest, children: [_jsxs(Row, { gap: 1, children: [_jsx("text", { content: chevron, fg: "subtle" }), _jsx("text", { content: summary ?? (streaming ? 'thinking' : `thought, ${words} words`), fg: "subtle", italic: true })] }), expanded ? (_jsxs(Row, { gap: 1, children: [_jsx("text", { content: " " }), _jsx(StreamingText, { content: content, quiet: true, flex: 1, ...(streaming ? { streaming: true } : {}), ...(match ? { match } : {}) })] })) : null] }));
|
|
86
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import type { BoxProps, RenderOutput } from '@textui/core';
|
|
2
|
-
import type { Completion, SlashCommand } from '../ahp/types.js';
|
|
3
|
-
import type { ComposerOption } from './controls.js';
|
|
4
|
-
/**
|
|
5
|
-
* What you type, and one line saying what it will be sent as.
|
|
6
|
-
*
|
|
7
|
-
* The field itself is `TextArea` from the catalog - growing, scrolling and
|
|
8
|
-
* giving back the keys it does not want is not a chat problem. What is here is
|
|
9
|
-
* the rest of a composer: what enter means while a turn is running, the slash
|
|
10
|
-
* menu over what has already been typed, and the control row.
|
|
11
|
-
*
|
|
12
|
-
* The row used to be four ghost buttons naming their own keys - `send enter`,
|
|
13
|
-
* `newline alt+enter`, `stop ctrl+c`, `commands ctrl+p` - which spent the one
|
|
14
|
-
* line under the field on a keyboard legend. The keys belong in the footer,
|
|
15
|
-
* which already lists them and changes with where the focus is. The line under
|
|
16
|
-
* the field is worth more as *what is about to happen*: which harness, which
|
|
17
|
-
* model, what it may do without asking, where it runs.
|
|
18
|
-
*/
|
|
19
|
-
export interface ChatComposerProps extends BoxProps {
|
|
20
|
-
value: string;
|
|
21
|
-
onChange(value: string): void;
|
|
22
|
-
onSubmit(value: string): void;
|
|
23
|
-
onCancel?(): void;
|
|
24
|
-
onHistory?(direction: -1 | 1): void;
|
|
25
|
-
/** Left off the front of the field: out of the composer entirely. */
|
|
26
|
-
onLeave?(): void;
|
|
27
|
-
/** A turn is running: enter queues rather than sends, and stop is offered. */
|
|
28
|
-
running?: boolean;
|
|
29
|
-
queued?: number;
|
|
30
|
-
/** The control row. Each is a value, and each may open a picker. */
|
|
31
|
-
options?: ComposerOption[];
|
|
32
|
-
onOption?(option: ComposerOption, anchorId: string): void;
|
|
33
|
-
placeholder?: string;
|
|
34
|
-
/** Offered when the draft starts with a slash. */
|
|
35
|
-
commands?: SlashCommand[];
|
|
36
|
-
/**
|
|
37
|
-
* One of `commands` was chosen from the slash menu.
|
|
38
|
-
*
|
|
39
|
-
* The whole command rather than its id, because the two kinds go different
|
|
40
|
-
* places and only the command knows which it is. A `client` command is
|
|
41
|
-
* *ours*: it opens a screen, changes a setting or picks a theme, and none of
|
|
42
|
-
* that is a message - sending it down the session channel would put
|
|
43
|
-
* "/theme" in the transcript and ask the agent to make sense of it. A
|
|
44
|
-
* `session` command is a skill the host contributed, and the only way to
|
|
45
|
-
* invoke one is to send its name as the message.
|
|
46
|
-
*
|
|
47
|
-
* A slash the menu does not match is left alone and sent, which is how a
|
|
48
|
-
* command the host offers but did not list still reaches it.
|
|
49
|
-
*/
|
|
50
|
-
onCommand?(command: SlashCommand): void;
|
|
51
|
-
/**
|
|
52
|
-
* What the host offers to complete the word the caret is in.
|
|
53
|
-
*
|
|
54
|
-
* Fetched rather than filtered: a path is a path on the *host's*
|
|
55
|
-
* filesystem, so which of them match what has been typed is a question only
|
|
56
|
-
* it can answer, and the answer changes with every keystroke.
|
|
57
|
-
*/
|
|
58
|
-
paths?: Completion[];
|
|
59
|
-
/** One of `paths` was chosen. The range it replaces is on the completion. */
|
|
60
|
-
onPath?(path: Completion): void;
|
|
61
|
-
autoFocus?: boolean;
|
|
62
|
-
focusId?: string;
|
|
63
|
-
}
|
|
64
|
-
export declare const ChatComposer: (props: ChatComposerProps) => RenderOutput;
|