@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,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agent-host-session://` links, which are how one session names another.
|
|
3
|
+
*
|
|
4
|
+
* The reference host's session tools answer with one - `create_session`,
|
|
5
|
+
* `list_sessions` and `send_message` each put an `openLink` in their result -
|
|
6
|
+
* and its window turns the link into a click that opens the session or the
|
|
7
|
+
* chat. A terminal has no click, so here the same link is something to pick:
|
|
8
|
+
* the links in the open transcript are offered as a list, and one chosen
|
|
9
|
+
* opens what it names. The shape is the reference host's
|
|
10
|
+
* (`common/openSessionLink.ts`): `agent-host-session://<provider>/<id>`, with
|
|
11
|
+
* `?chat=<chatId>` for one chat of it and `&turn=<turnId>` for one turn.
|
|
12
|
+
*/
|
|
13
|
+
const LINK = /^agent-host-session:\/\/([^/?#]+)\/([^?#]+)(?:\?([^#]*))?(?:#.*)?$/i;
|
|
14
|
+
/** A link wherever it sits in prose: up to the whitespace or the bracket that ends it. */
|
|
15
|
+
const IN_TEXT = /agent-host-session:\/\/[^\s<>()\[\]"'`]+/gi;
|
|
16
|
+
const param = (query, name) => {
|
|
17
|
+
const found = new RegExp(`(?:^|&)${name}=([^&]*)`).exec(query)?.[1];
|
|
18
|
+
if (found === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
try {
|
|
21
|
+
return decodeURIComponent(found);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/** The link read, or undefined for text that is not one. */
|
|
28
|
+
export function parseSessionLink(text) {
|
|
29
|
+
const found = LINK.exec(text.trim());
|
|
30
|
+
if (found === null)
|
|
31
|
+
return undefined;
|
|
32
|
+
let id;
|
|
33
|
+
try {
|
|
34
|
+
id = decodeURIComponent(found[2] ?? '');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
id = found[2] ?? '';
|
|
38
|
+
}
|
|
39
|
+
if (id === '')
|
|
40
|
+
return undefined;
|
|
41
|
+
const query = found[3] ?? '';
|
|
42
|
+
const chatId = param(query, 'chat');
|
|
43
|
+
const turnId = param(query, 'turn');
|
|
44
|
+
return {
|
|
45
|
+
provider: found[1] ?? '',
|
|
46
|
+
id,
|
|
47
|
+
// The default chat is the session itself, which is how the reference
|
|
48
|
+
// host builds the link: it leaves the query off for it.
|
|
49
|
+
...(chatId !== undefined && chatId !== '' && chatId !== 'default' ? { chatId } : {}),
|
|
50
|
+
...(turnId !== undefined && turnId !== '' ? { turnId } : {}),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** The id inside a session URI, whatever scheme it is under: `ahp-session:/x` and `claude:/x` are both `x`. */
|
|
54
|
+
export const idOf = (uri) => {
|
|
55
|
+
const colon = uri.indexOf(':');
|
|
56
|
+
return (colon < 0 ? uri : uri.slice(colon + 1)).replace(/^\/+/, '');
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The row a link names.
|
|
60
|
+
*
|
|
61
|
+
* Matched on the id and the provider, whichever scheme the row is under: a
|
|
62
|
+
* session this client started is `ahp-session:/` and one read off the host's
|
|
63
|
+
* catalogue is `<provider>:/`, and the link says neither.
|
|
64
|
+
*/
|
|
65
|
+
export const sessionOfLink = (link, rows) => rows.find((row) => idOf(row.resource) === link.id && row.provider === link.provider)
|
|
66
|
+
?? rows.find((row) => idOf(row.resource) === link.id);
|
|
67
|
+
/**
|
|
68
|
+
* Every link in the transcript, once each, in the order they appear.
|
|
69
|
+
*
|
|
70
|
+
* Read from what a person can see: the prose, the tool calls' outcomes and
|
|
71
|
+
* outputs, and the host's notices. A tool answering `create_session` puts the
|
|
72
|
+
* link in its output, which is where somebody reading the transcript finds it.
|
|
73
|
+
*/
|
|
74
|
+
export function linksIn(turns) {
|
|
75
|
+
const found = [];
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
const take = (text, turnId) => {
|
|
78
|
+
if (!text)
|
|
79
|
+
return;
|
|
80
|
+
for (const match of text.matchAll(IN_TEXT)) {
|
|
81
|
+
const link = match[0].replace(/[.,;:!?]+$/, '');
|
|
82
|
+
if (seen.has(link) || parseSessionLink(link) === undefined)
|
|
83
|
+
continue;
|
|
84
|
+
seen.add(link);
|
|
85
|
+
const from = Math.max(0, (match.index ?? 0) - 40);
|
|
86
|
+
const context = text.slice(from, match.index ?? 0).replace(/\s+/g, ' ').trim();
|
|
87
|
+
found.push({ link, context, turnId });
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
for (const turn of turns) {
|
|
91
|
+
take(turn.message, turn.id);
|
|
92
|
+
for (const part of turn.parts) {
|
|
93
|
+
if (part.kind === 'markdown' || part.kind === 'systemNotification')
|
|
94
|
+
take(part.content, turn.id);
|
|
95
|
+
else if (part.kind === 'toolCall') {
|
|
96
|
+
take(part.call.outcome, turn.id);
|
|
97
|
+
take(part.call.output, turn.id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return found;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Whether a chat URI is the chat a link names.
|
|
105
|
+
*
|
|
106
|
+
* The id is the URI's authority where it has one - `ahp-chat://<chatId>/<session>`,
|
|
107
|
+
* which is how the reference host and ahpd spell a chat - and the path
|
|
108
|
+
* otherwise, `ahp-chat:/<id>`, which is the protocol's own shape.
|
|
109
|
+
*/
|
|
110
|
+
export const chatMatches = (chatUri, chatId) => {
|
|
111
|
+
const found = /^[^:]+:\/\/([^/]+)\//.exec(chatUri)?.[1] ?? idOf(chatUri);
|
|
112
|
+
if (found === '')
|
|
113
|
+
return false;
|
|
114
|
+
try {
|
|
115
|
+
return decodeURIComponent(found) === chatId;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return found === chatId;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's filesystem, as a textui resource provider.
|
|
3
|
+
*
|
|
4
|
+
* textui's file picker, its search and its viewers read the resource
|
|
5
|
+
* registry and never `node:fs` - which is the reason they can be pointed at
|
|
6
|
+
* a filesystem on another machine. This is that pointing: `file:` on this
|
|
7
|
+
* client is the host's disk, reached through the protocol's `resource*`
|
|
8
|
+
* requests, so a folder picked in a dialog is a folder the host can start a
|
|
9
|
+
* session in. Read-only here; what writes goes through `ahpc resource`.
|
|
10
|
+
*/
|
|
11
|
+
import type { ResourceProvider } from '@textui/core';
|
|
12
|
+
import type { HostConnection } from './ahp/connection.js';
|
|
13
|
+
export declare function hostResources(host: HostConnection): ResourceProvider;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's filesystem, as a textui resource provider.
|
|
3
|
+
*
|
|
4
|
+
* textui's file picker, its search and its viewers read the resource
|
|
5
|
+
* registry and never `node:fs` - which is the reason they can be pointed at
|
|
6
|
+
* a filesystem on another machine. This is that pointing: `file:` on this
|
|
7
|
+
* client is the host's disk, reached through the protocol's `resource*`
|
|
8
|
+
* requests, so a folder picked in a dialog is a folder the host can start a
|
|
9
|
+
* session in. Read-only here; what writes goes through `ahpc resource`.
|
|
10
|
+
*/
|
|
11
|
+
const nameOf = (uri) => decodeURIComponent(uri.replace(/\/+$/, '').split('/').pop() ?? '');
|
|
12
|
+
export function hostResources(host) {
|
|
13
|
+
const resource = (uri, kind, size) => ({
|
|
14
|
+
uri,
|
|
15
|
+
kind: kind === 'directory' ? 'directory' : 'file',
|
|
16
|
+
metadata: { name: nameOf(uri), ...(size === undefined ? {} : { size }), readonly: true },
|
|
17
|
+
capabilities: kind === 'directory' ? ['list'] : ['read'],
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
scheme: 'file',
|
|
21
|
+
// A host without the family answers -32601, which is the same answer as
|
|
22
|
+
// "nothing there" to something asking whether it can list a folder.
|
|
23
|
+
stat: async (uri) => {
|
|
24
|
+
if (!host.resourceResolve)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const found = await host.resourceResolve(uri);
|
|
28
|
+
return resource(found.uri, found.type, found.size);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
list: async (uri) => {
|
|
35
|
+
if (!host.resourceList)
|
|
36
|
+
return [];
|
|
37
|
+
return (await host.resourceList(uri)).map((entry) => resource(entry.uri, entry.kind, entry.size));
|
|
38
|
+
},
|
|
39
|
+
read: async (uri) => {
|
|
40
|
+
if (!host.resourceRead)
|
|
41
|
+
throw new Error('This host does not serve files.');
|
|
42
|
+
const found = await host.resourceRead(uri);
|
|
43
|
+
return found.encoding === 'base64' ? Uint8Array.from(Buffer.from(found.data, 'base64')) : found.data;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/src/screens.js
CHANGED
|
@@ -3,24 +3,15 @@ import { defineComponent, useApp, useCapabilities, useEffect, useFocusScope, use
|
|
|
3
3
|
import { Badge, Column, Divider, EmptyState, Field, Form, FormActions, Marquee, Panel, RadioGroup, Row, SearchBox, Select, TextInput, argumentOf, useForm } from '@textui/widgets';
|
|
4
4
|
import { PRESETS, presetFor, scheduleProblem, zoneIsKnownHere } from './schedule.js';
|
|
5
5
|
import { AUTOMATIONS_SCOPE, CHANGES_SCOPE, CHAT_SCOPE, CONTROLLER, MCP_SCOPE, SESSIONS_SCOPE, SKILLS_SCOPE, modelCommand, settingCommand, } from './control.js';
|
|
6
|
-
import { branchName, branchDrift, ARCHIVED, AUTOMATIONS, AUTOMATION_ROW, CHANGES, CUSTOMIZATIONS, DRAFT, EXPANDED, FILTER, FOCUS, HISTORY, HOST, INPUT, CHANGE_AT, CHANGE_ROW, CHANGE_SCOPES, FILES_AT, FILES_ENTRIES, FILES_OPEN, MODEL, MODEL_CONFIG, OPEN, OPEN_FILE, CHAT_URI, PROVIDER, QUEUE, SELECTED, SESSIONS, SETTINGS, SIDEBAR, CHATS, CURSOR, FIND, FINDING, FIND_AT, OPEN_TERMINAL, PRESENT, SPLIT_AT, SPLIT_DEFAULT, TERMINAL, TERMINALS, TURNS, WORKSPACE, hiddenSessions, openSession, visibleSessions, workspaceName, } from './state.js';
|
|
7
|
-
import {
|
|
6
|
+
import { branchName, branchDrift, pullRequestLabel, ARCHIVED, AUTOMATIONS, AUTOMATION_ROW, CHANGES, CUSTOMIZATIONS, DRAFT, EXPANDED, FILTER, FOCUS, HISTORY, HOST, INPUT, CHANGE_AT, CHANGE_ROW, CHANGE_SCOPES, FILES_AT, FILES_ENTRIES, FILES_OPEN, MODEL, MODEL_CONFIG, OPEN, OPEN_FILE, CHAT_URI, PROVIDER, QUEUE, SELECTED, SESSIONS, SETTINGS, SIDEBAR, CHATS, CURSOR, FIND, FINDING, FIND_AT, OPEN_TERMINAL, PRESENT, SPLIT_AT, SPLIT_DEFAULT, TERMINAL, TERMINALS, TURNS, WORKSPACE, ANSWERS, INPUT_STATUS, MARKDOWN, boodFloorFor, hiddenSessions, openSession, sessionView, visibleSessions, workspaceName, } from './state.js';
|
|
7
|
+
import { toBlocks } from './blocks.js';
|
|
8
8
|
import { decodeStatus } from './ahp/status.js';
|
|
9
|
-
import { ChatTranscript } from '
|
|
10
|
-
import { ChatComposer } from './view/composer.js';
|
|
11
|
-
import { ChatSessionHead } from './view/sessionhead.js';
|
|
12
|
-
import { settingIcon, valueIcon } from './view/icons.js';
|
|
13
|
-
import { ChatHitl, ChatInputStatus } from './view/hitl.js';
|
|
9
|
+
import { ChatComposer, ChatHitl, ChatInputStatus, ChatSessionHead, ChatTranscript, ConnectionBadge, FileDiff, SessionDetails, SessionList, diffLines, findBlocks, openPicker, settingIcon, valueIcon, } from '@textui/chat';
|
|
14
10
|
import { ChangesList } from './view/changes.js';
|
|
15
11
|
import { FileList } from './view/files.js';
|
|
16
12
|
import { AutomationList } from './view/automations.js';
|
|
17
13
|
import { CustomizationList } from './view/customizations.js';
|
|
18
14
|
import { TerminalView } from './view/terminal.js';
|
|
19
|
-
import { FileDiff } from './view/filediff.js';
|
|
20
|
-
import { diffLines } from './diff.js';
|
|
21
|
-
import { ConnectionBadge, SessionList } from './view/sessions.js';
|
|
22
|
-
import { SessionDetails } from './view/details.js';
|
|
23
|
-
import { openPicker } from './view/picker.js';
|
|
24
15
|
/**
|
|
25
16
|
* The screens.
|
|
26
17
|
*
|
|
@@ -31,9 +22,19 @@ import { openPicker } from './view/picker.js';
|
|
|
31
22
|
* command palette, a confirm - is a layer or an expansion inside one of these,
|
|
32
23
|
* because none of them is a place you navigate *to*.
|
|
33
24
|
*
|
|
34
|
-
* Every screen is composition. The parts are
|
|
35
|
-
* `control.ts`, and what is
|
|
25
|
+
* Every screen is composition. The chat parts are `@textui/chat`'s, the
|
|
26
|
+
* AHP-only ones are in `view/`, the actions are in `control.ts`, and what is
|
|
27
|
+
* left here is which part goes where.
|
|
36
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* Keys the reference host seeds from the client's own settings and never asks
|
|
31
|
+
* about (`WELL_KNOWN_PICKER_PROPERTIES` in `agentHostChatInputPicker.ts`):
|
|
32
|
+
* how a worktree's branch is named and tracked, which ignored files come
|
|
33
|
+
* along, the shell's init script. Declared so the value rides in the config
|
|
34
|
+
* bag; drawn, they are five chips nobody can change, which is what "too many
|
|
35
|
+
* options" looked like.
|
|
36
|
+
*/
|
|
37
|
+
const SEEDED = new Set(['worktreeBranchPrefix', 'worktreeBranchTrack', 'worktreeCreateNewBranch', 'worktreeIncludeFiles', 'shellInitScripts']);
|
|
37
38
|
/**
|
|
38
39
|
* Everything the catalogue knows about one session, as rows.
|
|
39
40
|
*
|
|
@@ -80,7 +81,7 @@ function describe(session, detail) {
|
|
|
80
81
|
// how the pane came to show a blank "Permissions" against a host whose key
|
|
81
82
|
// for it is `autoApprove`.
|
|
82
83
|
...(detail?.config.properties ?? [])
|
|
83
|
-
.filter((property) => property.values.length > 0)
|
|
84
|
+
.filter((property) => property.values.length > 0 && !SEEDED.has(property.key))
|
|
84
85
|
.map((property) => ({
|
|
85
86
|
id: `config.${property.key}`,
|
|
86
87
|
label: property.title,
|
|
@@ -90,7 +91,7 @@ function describe(session, detail) {
|
|
|
90
91
|
{
|
|
91
92
|
id: 'branch',
|
|
92
93
|
label: 'Branch',
|
|
93
|
-
value: [branchName(session), branchDrift(session)].filter(Boolean).join(' '),
|
|
94
|
+
value: [branchName(session), branchDrift(session), pullRequestLabel(session)].filter(Boolean).join(' '),
|
|
94
95
|
absent: 'not a repository, or the host does not say',
|
|
95
96
|
},
|
|
96
97
|
// The identifiers, in full and copyable. A URI you can read half of is
|
|
@@ -202,7 +203,7 @@ export const SessionsScreen = defineComponent('SessionsScreen', () => {
|
|
|
202
203
|
return (_jsxs(Row, { flex: 1, gap: 1, children: [_jsxs(Panel, { title: "Sessions", ...(reading ? { width: aside } : { flex: 1 }), meta: waiting > 0 ? `${theme.glyphs.warning} ${waiting} waiting on you` : `${sessions.length} shown`, children: [_jsx(SearchBox, { value: filter, placeholder: "title, provider or workspace",
|
|
203
204
|
// Named, so `ctrl+f` has something to focus. A control whose id
|
|
204
205
|
// comes from its instance cannot be the target of a command.
|
|
205
|
-
focusId: "chat.filter", onChange: (value) => app.store.set(FILTER, value) }), _jsx(SessionList, { sessions: sessions, selectedId: selected, focusId: "chat.sessions",
|
|
206
|
+
focusId: "chat.filter", onChange: (value) => app.store.set(FILTER, value) }), _jsx(SessionList, { sessions: sessions.map(sessionView), selectedId: selected, focusId: "chat.sessions",
|
|
206
207
|
// The list, not the filter. Whatever registers first would
|
|
207
208
|
// otherwise hold the keyboard on arrival, and the filter is drawn
|
|
208
209
|
// above the list - which made every single-letter command a letter
|
|
@@ -341,6 +342,12 @@ function useHarnessCommands() {
|
|
|
341
342
|
}, []);
|
|
342
343
|
return items;
|
|
343
344
|
}
|
|
345
|
+
/**
|
|
346
|
+
* The keys that say where a session runs, which get a row of their own, in
|
|
347
|
+
* the order they read: after the directory, whether the session sits in it
|
|
348
|
+
* or in a worktree of it, and then the branch a worktree starts from.
|
|
349
|
+
*/
|
|
350
|
+
const WHERE = ['isolation', 'branch'];
|
|
344
351
|
function useComposerOptions() {
|
|
345
352
|
const unicode = useCapabilities().unicode;
|
|
346
353
|
const controller = useRequiredService(CONTROLLER);
|
|
@@ -357,15 +364,27 @@ function useComposerOptions() {
|
|
|
357
364
|
}, []);
|
|
358
365
|
// Asking is also what registers a command per property, so the chips below
|
|
359
366
|
// have something to open and the palette has the same questions in it.
|
|
367
|
+
// Asked again after every answer while there is no session yet, because
|
|
368
|
+
// the questions depend on the answers: `branch` is read-only until
|
|
369
|
+
// isolation is `worktree`, and the reference host rewrites the schema on
|
|
370
|
+
// `resolveSessionConfig` to say so. On what the values say rather than the
|
|
371
|
+
// object, since the host's echo of the same answers is a new object every
|
|
372
|
+
// time and would ask forever. And again when the directory changes, since
|
|
373
|
+
// isolation and the branches are questions about one repository. An open
|
|
374
|
+
// session's config arrives on its own channel, and asking for it after
|
|
375
|
+
// every change would answer with the value the host held before the
|
|
376
|
+
// change reached it.
|
|
377
|
+
const answered = open ? '' : JSON.stringify(settings);
|
|
360
378
|
useEffect(() => {
|
|
361
379
|
void controller.settings().then(setConfig)
|
|
362
380
|
.catch((error) => controller.report(error));
|
|
363
|
-
}, [provider, open]);
|
|
381
|
+
}, [provider, open, answered, open ? '' : workspace]);
|
|
364
382
|
const agent = agents.find((found) => found.provider === provider);
|
|
365
383
|
// A harness with no models is the ordinary answer for one nobody has signed
|
|
366
384
|
// into, so the chip says so rather than opening on an empty list. Until the
|
|
367
385
|
// catalogue has arrived there is no harness to say it about.
|
|
368
386
|
const models = agent ? agent.models : null;
|
|
387
|
+
const fromConfig = fromConfigOf(unicode, config, models, model, settings, open);
|
|
369
388
|
return [
|
|
370
389
|
{
|
|
371
390
|
id: 'harness',
|
|
@@ -412,42 +431,58 @@ function useComposerOptions() {
|
|
|
412
431
|
commandId: modelCommand(property.key),
|
|
413
432
|
};
|
|
414
433
|
}),
|
|
415
|
-
...(
|
|
416
|
-
.filter((property) => property.values.length > 0)
|
|
417
|
-
// A key the model also asks about is the model's to answer: the session
|
|
418
|
-
// key is the whole harness's default and the model's is what this
|
|
419
|
-
// message runs at, and drawing both put two controls on one row
|
|
420
|
-
// disagreeing about the same setting.
|
|
421
|
-
.filter((property) => !(models?.find((found) => found.id === model)?.options ?? [])
|
|
422
|
-
.some((one) => one.key === property.key))
|
|
423
|
-
.map((property) => {
|
|
424
|
-
const value = settings[property.key];
|
|
425
|
-
const chosen = property.values.find((found) => found.value === value);
|
|
426
|
-
return {
|
|
427
|
-
id: property.key,
|
|
428
|
-
// The value's own mark where it has one - which of five approval
|
|
429
|
-
// modes is in force is the thing worth reading from the row itself.
|
|
430
|
-
// The question's mark otherwise, so a branch chip is still a branch.
|
|
431
|
-
icon: (value !== undefined
|
|
432
|
-
? valueIcon(unicode, value, chosen?.label)
|
|
433
|
-
: undefined)
|
|
434
|
-
?? settingIcon(unicode, property.key, property.title),
|
|
435
|
-
label: chosen?.label ?? value ?? property.title,
|
|
436
|
-
// The question, for anything showing these with room for the pair.
|
|
437
|
-
title: property.title,
|
|
438
|
-
// Shown but not asked where the host says it cannot be changed on a
|
|
439
|
-
// running session: offering it produces a refusal, not an edit.
|
|
440
|
-
...(open && !property.sessionMutable ? {} : { commandId: settingCommand(property.key) }),
|
|
441
|
-
};
|
|
442
|
-
}),
|
|
434
|
+
...fromConfig.filter((option) => !option.where),
|
|
443
435
|
{
|
|
444
436
|
id: 'workspace',
|
|
445
437
|
icon: settingIcon(unicode, 'workspace'),
|
|
446
438
|
label: workspaceName(workspace ? `file://${workspace}` : undefined),
|
|
439
|
+
where: true,
|
|
447
440
|
...(open ? {} : { commandId: 'compose.workspace' }),
|
|
448
441
|
},
|
|
442
|
+
...fromConfig.filter((option) => option.where)
|
|
443
|
+
.sort((a, b) => WHERE.indexOf(a.id) - WHERE.indexOf(b.id)),
|
|
449
444
|
];
|
|
450
445
|
}
|
|
446
|
+
/** The session's own questions, as chips. */
|
|
447
|
+
function fromConfigOf(unicode, config, models, model, settings, open) {
|
|
448
|
+
return (config?.properties ?? [])
|
|
449
|
+
// Something to choose from, or a host that said to ask it: `branch` is
|
|
450
|
+
// `enumDynamic` with its values behind `sessionConfigCompletions`, and
|
|
451
|
+
// a chip that needed the list up front never showed the one question
|
|
452
|
+
// that has more answers than a schema holds.
|
|
453
|
+
.filter((property) => (property.values.length > 0 || property.enumDynamic === true) && !SEEDED.has(property.key))
|
|
454
|
+
// A key the model also asks about is the model's to answer: the session
|
|
455
|
+
// key is the whole harness's default and the model's is what this
|
|
456
|
+
// message runs at, and drawing both put two controls on one row
|
|
457
|
+
// disagreeing about the same setting.
|
|
458
|
+
.filter((property) => !(models?.find((found) => found.id === model)?.options ?? [])
|
|
459
|
+
.some((one) => one.key === property.key))
|
|
460
|
+
// Shown but not asked where the host says it cannot be changed on a
|
|
461
|
+
// running session, or at all: offering it produces a refusal, not an
|
|
462
|
+
// edit. And a value that is neither askable nor known is nothing to
|
|
463
|
+
// draw: a read-only `branch` with no default is a chip saying "Branch".
|
|
464
|
+
.map((property) => ({ property, asked: !((open && !property.sessionMutable) || property.readOnly === true) }))
|
|
465
|
+
.filter(({ property, asked }) => asked || settings[property.key] !== undefined)
|
|
466
|
+
.map(({ property, asked }) => {
|
|
467
|
+
const value = settings[property.key];
|
|
468
|
+
const chosen = property.values.find((found) => found.value === value);
|
|
469
|
+
return {
|
|
470
|
+
id: property.key,
|
|
471
|
+
// The value's own mark where it has one - which of five approval
|
|
472
|
+
// modes is in force is the thing worth reading from the row itself.
|
|
473
|
+
// The question's mark otherwise, so a branch chip is still a branch.
|
|
474
|
+
icon: (value !== undefined
|
|
475
|
+
? valueIcon(unicode, value, chosen?.label)
|
|
476
|
+
: undefined)
|
|
477
|
+
?? settingIcon(unicode, property.key, property.title),
|
|
478
|
+
label: chosen?.label ?? value ?? property.title,
|
|
479
|
+
// The question, for anything showing these with room for the pair.
|
|
480
|
+
title: property.title,
|
|
481
|
+
...(WHERE.includes(property.key) ? { where: true } : {}),
|
|
482
|
+
...(asked ? { commandId: settingCommand(property.key) } : {}),
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
}
|
|
451
486
|
// -------------------------------------------------------------------- 2. chat
|
|
452
487
|
/**
|
|
453
488
|
* What goes after a slash, from both places it can come from.
|
|
@@ -533,6 +568,12 @@ export const ChatScreen = defineComponent('ChatScreen', () => {
|
|
|
533
568
|
const present = useStoreValue(PRESENT, []) ?? [];
|
|
534
569
|
const chat = useStoreValue(CHAT_URI, null) ?? null;
|
|
535
570
|
const running = turns.some((turn) => turn.state === 'running');
|
|
571
|
+
// What the components used to read for themselves. The switch, the row
|
|
572
|
+
// under the composer and the draft answers are this client's state; the
|
|
573
|
+
// components take them as props and know nothing of the paths.
|
|
574
|
+
const markdown = useStoreValue(MARKDOWN, true) ?? true;
|
|
575
|
+
const inputStatus = useStoreValue(INPUT_STATUS, null) ?? null;
|
|
576
|
+
const [answers, setAnswers] = useStore(`${ANSWERS}/${input?.id ?? 'none'}`, {});
|
|
536
577
|
/*
|
|
537
578
|
* Memoised, and the three below it with it, because of what the
|
|
538
579
|
* transcript does with them.
|
|
@@ -593,7 +634,7 @@ export const ChatScreen = defineComponent('ChatScreen', () => {
|
|
|
593
634
|
// What the caption is made of, as one value that can be compared. The
|
|
594
635
|
// parts are rebuilt every render and the caption they describe is not.
|
|
595
636
|
const headSignature = JSON.stringify([session, model, chat, settingRows, chats.length, reading]);
|
|
596
|
-
const head = useMemo(() => (_jsxs(Column, { padding: [0, 0, 1, 0], children: [session ? (_jsx(ChatSessionHead, { session: session, present: present, ...(model ? { model } : {}), ...(chat ? { chat } : {}), settings: chats.length > 1
|
|
637
|
+
const head = useMemo(() => (_jsxs(Column, { padding: [0, 0, 1, 0], children: [session ? (_jsx(ChatSessionHead, { session: sessionView(session), present: present, ...(model ? { model } : {}), ...(chat ? { chat } : {}), settings: chats.length > 1
|
|
597
638
|
// Where a session holds several, which one is being read is the
|
|
598
639
|
// first thing somebody needs from the header - a transcript
|
|
599
640
|
// that changed under the same title is otherwise unexplained.
|
|
@@ -669,7 +710,11 @@ export const ChatScreen = defineComponent('ChatScreen', () => {
|
|
|
669
710
|
// The top of the conversation, inside it. See `head`.
|
|
670
711
|
, {
|
|
671
712
|
// The top of the conversation, inside it. See `head`.
|
|
672
|
-
head: head, flex: 1, blocks: blocks, expanded: expanded, ...(finding && query.trim() !== '' ? { match: query.trim(), pinCursor: true } : {}), cursor: cursor ?? 0, onCursor: onCursor, onToggle: onToggle }), input ? (_jsx(ChatHitl, { input: input,
|
|
713
|
+
head: head, flex: 1, blocks: blocks, expanded: expanded, ...(finding && query.trim() !== '' ? { match: query.trim(), pinCursor: true } : {}), cursor: cursor ?? 0, onCursor: onCursor, onToggle: onToggle, markdown: markdown }), input ? (_jsx(ChatHitl, { input: input, draft: answers ?? {}, onDraft: setAnswers,
|
|
714
|
+
// Where it starts, so the creature has somewhere to stand that is
|
|
715
|
+
// not on it. The block sits above the composer, so this is the
|
|
716
|
+
// floor while a question is up.
|
|
717
|
+
onMeasure: (rect) => app.store.set(boodFloorFor('ask'), rect?.y ?? 0), onApprove: (option) => controller.approve(option), onDeny: () => controller.deny(), onAnswer: (answers, accepted) => controller.answer(answers, accepted),
|
|
673
718
|
// The block takes keys globally while it is up, escape included -
|
|
674
719
|
// which is right for `a` and `d` and wrong for the one key that is
|
|
675
720
|
// how you leave. Sent back to the transcript unconditionally it
|
|
@@ -688,7 +733,7 @@ export const ChatScreen = defineComponent('ChatScreen', () => {
|
|
|
688
733
|
app.screens.pop();
|
|
689
734
|
else
|
|
690
735
|
app.focus.focus('chat.transcript');
|
|
691
|
-
} })) : null, _jsx(ChatInputStatus, {}), _jsx(ChatComposer, { value: draft, running: running, queued: queued.length, options: options, onOption: (option, anchorId) => {
|
|
736
|
+
} })) : null, _jsx(ChatInputStatus, { status: inputStatus }), _jsx(ChatComposer, { value: draft, onMeasure: (rect) => app.store.set(boodFloorFor('composer'), rect?.y ?? 0), running: running, queued: queued.length, options: options, onOption: (option, anchorId) => {
|
|
692
737
|
if (option.commandId)
|
|
693
738
|
openPicker(app, { commandId: option.commandId, anchorId });
|
|
694
739
|
}, commands: slashCommands(app, skills), paths: paths, onPath: (path) => {
|
|
@@ -741,7 +786,7 @@ export const NewSessionScreen = defineComponent('NewSessionScreen', () => {
|
|
|
741
786
|
const options = useComposerOptions();
|
|
742
787
|
const harnessSkills = useHarnessCommands();
|
|
743
788
|
const newPaths = usePathCompletions(draft, 'ahp-root://');
|
|
744
|
-
return (_jsxs(Column, { flex: 1, gap: 1, children: [_jsxs(Column, { flex: 1, justify: "center", align: "center", gap: 0, children: [_jsx("text", { content: "A new session", fg: "muted" }), _jsx("text", { content: "The first message is what starts it.", fg: "subtle" }), _jsx("text", { content: `${theme.glyphs.chevronLeft} esc for the sessions you already have`, fg: "subtle" })] }), _jsx(ChatComposer, { value: draft, options: options, onOption: (option, anchorId) => {
|
|
789
|
+
return (_jsxs(Column, { flex: 1, gap: 1, children: [_jsxs(Column, { flex: 1, justify: "center", align: "center", gap: 0, children: [_jsx("text", { content: "A new session", fg: "muted" }), _jsx("text", { content: "The first message is what starts it.", fg: "subtle" }), _jsx("text", { content: `${theme.glyphs.chevronLeft} esc for the sessions you already have`, fg: "subtle" })] }), _jsx(ChatComposer, { value: draft, onMeasure: (rect) => app.store.set(boodFloorFor('composer'), rect?.y ?? 0), options: options, onOption: (option, anchorId) => {
|
|
745
790
|
if (option.commandId)
|
|
746
791
|
openPicker(app, { commandId: option.commandId, anchorId });
|
|
747
792
|
},
|
package/dist/src/state.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BindingPath, ReactiveStore } from '@textui/core';
|
|
2
|
+
import type { ChatSession } from '@textui/chat';
|
|
2
3
|
import type { HostEvent } from './ahp/connection.js';
|
|
3
4
|
import type { Changeset, PendingInput, QueuedMessage, SessionSummary, Turn } from './ahp/types.js';
|
|
4
5
|
/**
|
|
@@ -237,6 +238,14 @@ export declare const CURSOR: BindingPath;
|
|
|
237
238
|
* link's target rather than its label.
|
|
238
239
|
*/
|
|
239
240
|
export declare const MARKDOWN: BindingPath;
|
|
241
|
+
/**
|
|
242
|
+
* Draft answers to the question the host is waiting on, one map per request.
|
|
243
|
+
*
|
|
244
|
+
* In the store, not in the block: AHP has an action for a draft answer
|
|
245
|
+
* precisely because another client may be looking at the same question, and
|
|
246
|
+
* a value only one box knows is one nobody else can see.
|
|
247
|
+
*/
|
|
248
|
+
export declare const ANSWERS: BindingPath;
|
|
240
249
|
/**
|
|
241
250
|
* Whether the catalogue's detail pane is out, or `null` for "whatever the
|
|
242
251
|
* terminal is wide enough for".
|
|
@@ -344,6 +353,39 @@ export declare function projectName(session: SessionSummary): string;
|
|
|
344
353
|
* "the host does not say" against a host that was saying it all along.
|
|
345
354
|
*/
|
|
346
355
|
export declare function branchName(session: SessionSummary): string | undefined;
|
|
356
|
+
/**
|
|
357
|
+
* A session as the components take it: decoded, and with nothing in it that
|
|
358
|
+
* only this client knows how to read.
|
|
359
|
+
*
|
|
360
|
+
* The status bitfield, the git metadata, the project name and the pull
|
|
361
|
+
* request are all AHP's business, and `@textui/chat` has no opinion about
|
|
362
|
+
* any of them.
|
|
363
|
+
*/
|
|
364
|
+
export declare function sessionView(session: SessionSummary): ChatSession;
|
|
365
|
+
/** A pull request the session's branch is known by, and what became of it. */
|
|
366
|
+
export interface PullRequest {
|
|
367
|
+
/** The number in its URL, which is how a person names one. */
|
|
368
|
+
number: string;
|
|
369
|
+
/** The last state the host observed, where it observed one. */
|
|
370
|
+
state?: 'open' | 'closed' | 'merged';
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* The pull request the reference host found for this session, if it applies.
|
|
374
|
+
*
|
|
375
|
+
* `_meta.github` is the reference host's, the way `_meta.git` is: a
|
|
376
|
+
* convention read by its own window, not a declaration. `pullRequestUrls` is
|
|
377
|
+
* the history, most recent first, and only the most recent counts. Two
|
|
378
|
+
* checks are the host's own, and are kept here so a row never says more than
|
|
379
|
+
* the host would: a request found for another branch is not this branch's
|
|
380
|
+
* (`pullRequestBranchName`, when the host has recorded one), and a state is
|
|
381
|
+
* only the state of the URL it was observed on (`pullRequestStateUrl`) - the
|
|
382
|
+
* host keeps reporting the last request it knew while it looks for one on the
|
|
383
|
+
* branch the checkout moved to, and "merged" beside the wrong number is worse
|
|
384
|
+
* than no state at all.
|
|
385
|
+
*/
|
|
386
|
+
export declare function pullRequest(session: SessionSummary): PullRequest | undefined;
|
|
387
|
+
/** The pull request as a row says it: the number, and its state where known. */
|
|
388
|
+
export declare function pullRequestLabel(session: SessionSummary): string | undefined;
|
|
347
389
|
/**
|
|
348
390
|
* How far the branch has drifted, where the host counted.
|
|
349
391
|
*
|
package/dist/src/state.js
CHANGED
|
@@ -234,6 +234,14 @@ export const CURSOR = '$/screen.chat/cursor';
|
|
|
234
234
|
* link's target rather than its label.
|
|
235
235
|
*/
|
|
236
236
|
export const MARKDOWN = '$/chat/ui/markdown';
|
|
237
|
+
/**
|
|
238
|
+
* Draft answers to the question the host is waiting on, one map per request.
|
|
239
|
+
*
|
|
240
|
+
* In the store, not in the block: AHP has an action for a draft answer
|
|
241
|
+
* precisely because another client may be looking at the same question, and
|
|
242
|
+
* a value only one box knows is one nobody else can see.
|
|
243
|
+
*/
|
|
244
|
+
export const ANSWERS = '$/chat/ui/answers';
|
|
237
245
|
/**
|
|
238
246
|
* Whether the catalogue's detail pane is out, or `null` for "whatever the
|
|
239
247
|
* terminal is wide enough for".
|
|
@@ -483,7 +491,9 @@ export function projectName(session) {
|
|
|
483
491
|
* anything here, including a `git` that is not an object.
|
|
484
492
|
*/
|
|
485
493
|
function git(session) {
|
|
486
|
-
|
|
494
|
+
return objectAt(session._meta?.git);
|
|
495
|
+
}
|
|
496
|
+
function objectAt(found) {
|
|
487
497
|
return typeof found === 'object' && found !== null && !Array.isArray(found)
|
|
488
498
|
? found
|
|
489
499
|
: {};
|
|
@@ -511,6 +521,75 @@ export function branchName(session) {
|
|
|
511
521
|
}
|
|
512
522
|
return undefined;
|
|
513
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* A session as the components take it: decoded, and with nothing in it that
|
|
526
|
+
* only this client knows how to read.
|
|
527
|
+
*
|
|
528
|
+
* The status bitfield, the git metadata, the project name and the pull
|
|
529
|
+
* request are all AHP's business, and `@textui/chat` has no opinion about
|
|
530
|
+
* any of them.
|
|
531
|
+
*/
|
|
532
|
+
export function sessionView(session) {
|
|
533
|
+
const branch = branchName(session);
|
|
534
|
+
const pull = pullRequestLabel(session);
|
|
535
|
+
return {
|
|
536
|
+
id: session.resource,
|
|
537
|
+
title: session.title,
|
|
538
|
+
provider: session.provider,
|
|
539
|
+
status: decodeStatus(session.status),
|
|
540
|
+
createdAt: session.createdAt,
|
|
541
|
+
modifiedAt: session.modifiedAt,
|
|
542
|
+
workingDirectories: session.workingDirectories,
|
|
543
|
+
project: projectName(session),
|
|
544
|
+
...(branch ? { branch } : {}),
|
|
545
|
+
...(pull ? { pullRequest: pull } : {}),
|
|
546
|
+
...(session.activity ? { activity: session.activity } : {}),
|
|
547
|
+
...(session.origin?.kind === 'automation' ? { origin: 'by an automation' } : {}),
|
|
548
|
+
...(session.changes ? { changes: session.changes } : {}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
/** The key two spellings of one pull request URL share: case and a trailing slash. */
|
|
552
|
+
const urlKey = (url) => url.trim().replace(/\/+$/, '').toLowerCase();
|
|
553
|
+
/**
|
|
554
|
+
* The pull request the reference host found for this session, if it applies.
|
|
555
|
+
*
|
|
556
|
+
* `_meta.github` is the reference host's, the way `_meta.git` is: a
|
|
557
|
+
* convention read by its own window, not a declaration. `pullRequestUrls` is
|
|
558
|
+
* the history, most recent first, and only the most recent counts. Two
|
|
559
|
+
* checks are the host's own, and are kept here so a row never says more than
|
|
560
|
+
* the host would: a request found for another branch is not this branch's
|
|
561
|
+
* (`pullRequestBranchName`, when the host has recorded one), and a state is
|
|
562
|
+
* only the state of the URL it was observed on (`pullRequestStateUrl`) - the
|
|
563
|
+
* host keeps reporting the last request it knew while it looks for one on the
|
|
564
|
+
* branch the checkout moved to, and "merged" beside the wrong number is worse
|
|
565
|
+
* than no state at all.
|
|
566
|
+
*/
|
|
567
|
+
export function pullRequest(session) {
|
|
568
|
+
const found = objectAt(session._meta?.github);
|
|
569
|
+
const urls = Array.isArray(found.pullRequestUrls)
|
|
570
|
+
? found.pullRequestUrls.filter((url) => typeof url === 'string')
|
|
571
|
+
: typeof found.pullRequestUrl === 'string' ? [found.pullRequestUrl] : [];
|
|
572
|
+
const url = urls[0];
|
|
573
|
+
if (url === undefined)
|
|
574
|
+
return undefined;
|
|
575
|
+
const branch = found.pullRequestBranchName;
|
|
576
|
+
if (typeof branch === 'string' && branch !== branchName(session))
|
|
577
|
+
return undefined;
|
|
578
|
+
const number = /\/pull\/(\d+)\/?$/.exec(url)?.[1];
|
|
579
|
+
if (number === undefined)
|
|
580
|
+
return undefined;
|
|
581
|
+
const state = found.pullRequestState;
|
|
582
|
+
const applies = typeof found.pullRequestStateUrl === 'string' && urlKey(found.pullRequestStateUrl) === urlKey(url);
|
|
583
|
+
return {
|
|
584
|
+
number,
|
|
585
|
+
...(applies && (state === 'open' || state === 'closed' || state === 'merged') ? { state } : {}),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
/** The pull request as a row says it: the number, and its state where known. */
|
|
589
|
+
export function pullRequestLabel(session) {
|
|
590
|
+
const found = pullRequest(session);
|
|
591
|
+
return found === undefined ? undefined : [`#${found.number}`, found.state].filter(Boolean).join(' ');
|
|
592
|
+
}
|
|
514
593
|
/**
|
|
515
594
|
* How far the branch has drifted, where the host counted.
|
|
516
595
|
*
|
package/dist/src/tui.d.ts
CHANGED
|
@@ -77,9 +77,11 @@ interface Options {
|
|
|
77
77
|
publish?: string;
|
|
78
78
|
/** Whether the host may write into it. Read-only otherwise. */
|
|
79
79
|
publishWritable?: boolean;
|
|
80
|
+
/** A file every frame is appended to, both directions, as JSON lines. */
|
|
81
|
+
wire?: string;
|
|
80
82
|
help: boolean;
|
|
81
83
|
}
|
|
82
|
-
export declare const USAGE = "ahpc - a terminal client for the Agent Host Protocol\n\n ahpc [options]\n\nThe host\n --host <url> A live agent host, ws://host:port\n --token <tkn> A bearer token for it\n --config-file <f> Read this instead of ~/.config/ahpc/config.json\n (none of these) The scripted host, which needs nothing installed\n\nWhere the agent works\n --path <dir> A path on the host, not on this machine. The host\n has to serve it, and says so if it does not.\n Left out, the host decides.\n\nWhat this client serves back\n --publish <dir> Serve this directory to the host under\n virtual://<clientId>/. Nothing is served without\n it, and every such request is refused.\n --publish-writable Let the host write into it. Read-only otherwise.\n Serving lasts as long as the screen does: with no\n terminal attached this prints one frame and exits,\n so a background shell publishes nothing.\n\nAppearance\n --theme <name> workbench, paper-light, ...\n --shell <name> The shell layout\n --screen <name> Which screen to open on\n --bood Let the creature loose on the whole screen. It\n keeps off the composer and off anything asking\n a question, and alt+g turns it off again.\n --session <uri> Open this session\n\nStills, for a README or a test\n --static, -s One frame to stdout instead of running\n --width, -w <n> Columns\n --height <n> Rows\n --unicode <level> ascii, bmp, full\n --colors <n> 0, 4, 8 or 24\n --svg <file> Write the still as SVG here\n --tick <ms> Milliseconds per scripted word\n --settled Run the script out before the frame\n --pump <n> Or exactly this many scripted words\n --say <text> Say this on the open session first\n --approve Answer the confirmation the script stops at\n --answer ...and then the question\n\n --version, -v What version this is\n --help, -h This\n\nCommands\n ahpc <command> ... Drive a host without the screen. 'ahpc help' lists\n them: sessions, prompts, approvals, terminals.\n";
|
|
84
|
+
export declare const USAGE = "ahpc - a terminal client for the Agent Host Protocol\n\n ahpc [options]\n\nThe host\n --host <url> A live agent host, ws://host:port\n --token <tkn> A bearer token for it\n --config-file <f> Read this instead of ~/.config/ahpc/config.json\n (none of these) The scripted host, which needs nothing installed\n\nWhere the agent works\n --path <dir> A path on the host, not on this machine. The host\n has to serve it, and says so if it does not.\n Left out, the host decides.\n\nWhat this client serves back\n --publish <dir> Serve this directory to the host under\n virtual://<clientId>/. Nothing is served without\n it, and every such request is refused.\n --publish-writable Let the host write into it. Read-only otherwise.\n Serving lasts as long as the screen does: with no\n terminal attached this prints one frame and exits,\n so a background shell publishes nothing.\n\nSeeing the wire\n --wire <file> Append every frame, both directions, as JSON lines:\n { at, from, peer, frame }. AHPC_RECORD=<file> is\n the same thing from a shell.\n\nAppearance\n --theme <name> workbench, paper-light, ...\n --shell <name> The shell layout\n --screen <name> Which screen to open on\n --bood Let the creature loose on the whole screen. It\n keeps off the composer and off anything asking\n a question, and alt+g turns it off again.\n --session <uri> Open this session, by its URI or by an\n agent-host-session:// link\n\nStills, for a README or a test\n --static, -s One frame to stdout instead of running\n --width, -w <n> Columns\n --height <n> Rows\n --unicode <level> ascii, bmp, full\n --colors <n> 0, 4, 8 or 24\n --svg <file> Write the still as SVG here\n --tick <ms> Milliseconds per scripted word\n --settled Run the script out before the frame\n --pump <n> Or exactly this many scripted words\n --say <text> Say this on the open session first\n --approve Answer the confirmation the script stops at\n --answer ...and then the question\n\n --version, -v What version this is\n --help, -h This\n\nCommands\n ahpc <command> ... Drive a host without the screen. 'ahpc help' lists\n them: sessions, prompts, approvals, terminals.\n";
|
|
83
85
|
export declare function parse(argv: string[]): Options;
|
|
84
86
|
/**
|
|
85
87
|
* The screen.
|