@softov/ahpc 0.1.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/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/src/ahp/channels.d.ts +146 -0
- package/dist/src/ahp/channels.js +423 -0
- package/dist/src/ahp/connection.d.ts +656 -0
- package/dist/src/ahp/connection.js +1 -0
- package/dist/src/ahp/fake.d.ts +69 -0
- package/dist/src/ahp/fake.js +1996 -0
- package/dist/src/ahp/live.d.ts +160 -0
- package/dist/src/ahp/live.js +2641 -0
- package/dist/src/ahp/operate.d.ts +36 -0
- package/dist/src/ahp/operate.js +56 -0
- package/dist/src/ahp/publish.d.ts +39 -0
- package/dist/src/ahp/publish.js +370 -0
- package/dist/src/ahp/status.d.ts +23 -0
- package/dist/src/ahp/status.js +23 -0
- package/dist/src/ahp/types.d.ts +745 -0
- package/dist/src/ahp/types.js +27 -0
- package/dist/src/app.d.ts +54 -0
- package/dist/src/app.js +369 -0
- package/dist/src/blocks.d.ts +88 -0
- package/dist/src/blocks.js +102 -0
- package/dist/src/cli/main.d.ts +13 -0
- package/dist/src/cli/main.js +1537 -0
- package/dist/src/cli/render.d.ts +43 -0
- package/dist/src/cli/render.js +91 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +45 -0
- package/dist/src/connect.d.ts +53 -0
- package/dist/src/connect.js +69 -0
- package/dist/src/control.d.ts +189 -0
- package/dist/src/control.js +2241 -0
- package/dist/src/diff.d.ts +44 -0
- package/dist/src/diff.js +111 -0
- package/dist/src/flags.d.ts +28 -0
- package/dist/src/flags.js +76 -0
- package/dist/src/main.d.ts +10 -0
- package/dist/src/main.js +54 -0
- package/dist/src/schedule.d.ts +46 -0
- package/dist/src/schedule.js +140 -0
- package/dist/src/screens.d.ts +79 -0
- package/dist/src/screens.js +1322 -0
- package/dist/src/state.d.ts +355 -0
- package/dist/src/state.js +531 -0
- package/dist/src/terminal.d.ts +44 -0
- package/dist/src/terminal.js +101 -0
- package/dist/src/tui.d.ts +92 -0
- package/dist/src/tui.js +372 -0
- package/dist/src/view/automations.d.ts +31 -0
- package/dist/src/view/automations.js +76 -0
- package/dist/src/view/bood/art.d.ts +90 -0
- package/dist/src/view/bood/art.js +152 -0
- package/dist/src/view/bood/beetle.d.ts +7 -0
- package/dist/src/view/bood/beetle.js +123 -0
- package/dist/src/view/bood/bunny.d.ts +8 -0
- package/dist/src/view/bood/bunny.js +228 -0
- package/dist/src/view/bood/cat.d.ts +9 -0
- package/dist/src/view/bood/cat.js +358 -0
- package/dist/src/view/bood/crab.d.ts +7 -0
- package/dist/src/view/bood/crab.js +121 -0
- package/dist/src/view/bood/index.d.ts +29 -0
- package/dist/src/view/bood/index.js +25 -0
- package/dist/src/view/bood/motion.d.ts +178 -0
- package/dist/src/view/bood/motion.js +298 -0
- package/dist/src/view/bood/owl.d.ts +9 -0
- package/dist/src/view/bood/owl.js +285 -0
- package/dist/src/view/bood/registry.d.ts +36 -0
- package/dist/src/view/bood/registry.js +218 -0
- package/dist/src/view/bood/render.d.ts +31 -0
- package/dist/src/view/bood/render.js +196 -0
- package/dist/src/view/bood/sprite.d.ts +18 -0
- package/dist/src/view/bood/sprite.js +130 -0
- package/dist/src/view/bood/sprout.d.ts +8 -0
- package/dist/src/view/bood/sprout.js +121 -0
- package/dist/src/view/bood/types.d.ts +158 -0
- package/dist/src/view/bood/types.js +57 -0
- package/dist/src/view/bubble.d.ts +75 -0
- package/dist/src/view/bubble.js +86 -0
- package/dist/src/view/changes.d.ts +35 -0
- package/dist/src/view/changes.js +22 -0
- package/dist/src/view/composer.d.ts +64 -0
- package/dist/src/view/composer.js +192 -0
- package/dist/src/view/controls.d.ts +44 -0
- package/dist/src/view/controls.js +49 -0
- package/dist/src/view/creature.d.ts +69 -0
- package/dist/src/view/creature.js +83 -0
- package/dist/src/view/customizations.d.ts +28 -0
- package/dist/src/view/customizations.js +53 -0
- package/dist/src/view/details.d.ts +65 -0
- package/dist/src/view/details.js +65 -0
- package/dist/src/view/filediff.d.ts +29 -0
- package/dist/src/view/filediff.js +24 -0
- package/dist/src/view/files.d.ts +31 -0
- package/dist/src/view/files.js +39 -0
- package/dist/src/view/hitl.d.ts +43 -0
- package/dist/src/view/hitl.js +171 -0
- package/dist/src/view/icons.d.ts +13 -0
- package/dist/src/view/icons.js +71 -0
- package/dist/src/view/picker.d.ts +42 -0
- package/dist/src/view/picker.js +71 -0
- package/dist/src/view/sessionhead.d.ts +41 -0
- package/dist/src/view/sessionhead.js +60 -0
- package/dist/src/view/sessions.d.ts +34 -0
- package/dist/src/view/sessions.js +61 -0
- package/dist/src/view/terminal.d.ts +30 -0
- package/dist/src/view/terminal.js +32 -0
- package/dist/src/view/toolcall.d.ts +27 -0
- package/dist/src/view/toolcall.js +48 -0
- package/dist/src/view/transcript.d.ts +50 -0
- package/dist/src/view/transcript.js +60 -0
- package/package.json +72 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The protocol, as a client reads it.
|
|
3
|
+
*
|
|
4
|
+
* These are AHP's own names for AHP's own shapes - `SessionSummary.status` is
|
|
5
|
+
* the bitset the host sends, `responseParts` is one ordered stream, a pending
|
|
6
|
+
* input is either a tool confirmation or a question. Renaming them here would
|
|
7
|
+
* only mean translating twice, and the whole point of the example is to find
|
|
8
|
+
* out which components the *protocol's* shapes need.
|
|
9
|
+
*
|
|
10
|
+
* It is a subset: what a chat client has to render. The authority is the
|
|
11
|
+
* `@agent-host-protocol` package's own `src/types/`.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Status is activity and client flags in one number.
|
|
15
|
+
*
|
|
16
|
+
* `InputNeeded` carries `InProgress`, so it has to be tested first - a turn
|
|
17
|
+
* waiting on a confirmation otherwise reads as merely running, and nobody
|
|
18
|
+
* goes to answer it.
|
|
19
|
+
*/
|
|
20
|
+
export const SessionFlag = {
|
|
21
|
+
Idle: 1,
|
|
22
|
+
Error: 2,
|
|
23
|
+
InProgress: 8,
|
|
24
|
+
InputNeeded: 24,
|
|
25
|
+
IsRead: 32,
|
|
26
|
+
IsArchived: 64,
|
|
27
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Disposable, TextUIApp } from '@textui/core';
|
|
2
|
+
import type { HostConnection } from './ahp/connection.js';
|
|
3
|
+
export interface ChatOptions {
|
|
4
|
+
builtins?: boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Keys from the config file, over the ones this client ships with.
|
|
7
|
+
*
|
|
8
|
+
* A chord to a command id, or to `null` to take the chord away. Read once
|
|
9
|
+
* at startup: a binding is registered when the controller is, and this
|
|
10
|
+
* client has no reload.
|
|
11
|
+
*/
|
|
12
|
+
keys?: Record<string, string | null>;
|
|
13
|
+
/**
|
|
14
|
+
* The header trades its own name for a seven-cell creature, on a session.
|
|
15
|
+
*
|
|
16
|
+
* Off unless asked for, and it is the config file that asks - `boodInline`
|
|
17
|
+
* in `~/.config/ahpc/config.json`.
|
|
18
|
+
*/
|
|
19
|
+
boodInline?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* The creature roams the whole application, on the floating layer.
|
|
22
|
+
*
|
|
23
|
+
* Off unless asked for - `boodFloat` in the config file. A terminal has no
|
|
24
|
+
* per-cell transparency, so a figure that goes everywhere writes over what
|
|
25
|
+
* it stands on: it keeps off the composer and off anything opened on a
|
|
26
|
+
* layer, and a screen whose content runs to the bottom gets a cat on it.
|
|
27
|
+
*/
|
|
28
|
+
boodFloat?: boolean;
|
|
29
|
+
/** The host. Omit for the scripted one, which is what the tests use. */
|
|
30
|
+
host?: HostConnection & {
|
|
31
|
+
pump?(): boolean;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Where a new session works, before anybody chooses otherwise.
|
|
35
|
+
*
|
|
36
|
+
* A path on the **host**, which is not this machine unless the host happens
|
|
37
|
+
* to be. Pass an empty string to say there is none and let the host decide;
|
|
38
|
+
* left out entirely it is the directory this process started in, which is
|
|
39
|
+
* the defensible guess only for a host that is this process.
|
|
40
|
+
*/
|
|
41
|
+
workspace?: string;
|
|
42
|
+
/**
|
|
43
|
+
* How wide the terminal has to be before the catalogue shows both panes.
|
|
44
|
+
*
|
|
45
|
+
* Narrower than this and the detail pane starts hidden - left opens it,
|
|
46
|
+
* right puts it away - because a split that leaves both halves truncated is
|
|
47
|
+
* worse than either half whole. Settable here so it can be tuned without a
|
|
48
|
+
* rebuild; `140` is where a session list stops cutting its titles with a
|
|
49
|
+
* detail pane beside it.
|
|
50
|
+
*/
|
|
51
|
+
splitAt?: number;
|
|
52
|
+
}
|
|
53
|
+
export declare function registerChat(app: TextUIApp, options?: ChatOptions): Disposable;
|
|
54
|
+
export { CONTROLLER } from './control.js';
|
package/dist/src/app.js
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "@textui/core/jsx-runtime";
|
|
2
|
+
import { createBag, defineComponent, useApp, useTheme, useStoreSubtree, useStoreValue, } from '@textui/core';
|
|
3
|
+
import { KeyHints, Row, registerBuiltins } from '@textui/widgets';
|
|
4
|
+
import { CONTROLLER, createController } from './control.js';
|
|
5
|
+
import { fakeHost } from './ahp/fake.js';
|
|
6
|
+
import { BOOD, BOOD_FLOAT, BOOD_FLOOR, BOOD_INLINE, FOCUS, HOST, HOST_ERROR, INPUT, INPUT_STATUS, OPEN, RUNNING, SCREEN, SESSIONS, SPLIT_AT, SPLIT_DEFAULT, STATUS, WORKSPACE, boodFloor, openSession, workspaceName, } from './state.js';
|
|
7
|
+
import { decodeStatus } from './ahp/status.js';
|
|
8
|
+
import { AutomationsScreen, ChangesScreen, ChatScreen, FilesScreen, HostsScreen, McpScreen, NewAutomationScreen, NewSessionScreen, SessionsScreen, TerminalScreen, SettingsScreen, SkillsScreen, } from './screens.js';
|
|
9
|
+
import { ChatBubble, ReasoningBlock, StreamingText } from './view/bubble.js';
|
|
10
|
+
import { ChatComposer } from './view/composer.js';
|
|
11
|
+
import { ChatHitl } from './view/hitl.js';
|
|
12
|
+
import { ChatTranscript } from './view/transcript.js';
|
|
13
|
+
import { BoodSprite, Creature, moodOf, pickBood } from './view/creature.js';
|
|
14
|
+
import { ChangesList } from './view/changes.js';
|
|
15
|
+
import { FileList } from './view/files.js';
|
|
16
|
+
import { AutomationList } from './view/automations.js';
|
|
17
|
+
import { ConnectionBadge, SessionList } from './view/sessions.js';
|
|
18
|
+
import { ToolCallRow } from './view/toolcall.js';
|
|
19
|
+
/**
|
|
20
|
+
* A chat client for an agent host.
|
|
21
|
+
*
|
|
22
|
+
* What it is here to find out is which components a chat application needs
|
|
23
|
+
* that a terminal UI catalog does not already have. The answer, by the end of
|
|
24
|
+
* writing it, is in `README.md` - and the shape of the answer is that the
|
|
25
|
+
* conversation is one component the catalog cannot stand in for, the composer
|
|
26
|
+
* is a second, and everything else is composition.
|
|
27
|
+
*
|
|
28
|
+
* The application takes a host rather than making one, so a test can mount it
|
|
29
|
+
* against a scripted one and drive time by hand.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* The creature, over the whole application.
|
|
33
|
+
*
|
|
34
|
+
* Mounted on the `floating` layer rather than inside a screen, so navigating
|
|
35
|
+
* moves the screen out from under it instead of unmounting it: escape from a
|
|
36
|
+
* session leaves the figure exactly where it was standing. This is the only
|
|
37
|
+
* place that knows both the store and the bood, which is what keeps
|
|
38
|
+
* `view/bood/` from having an opinion about sessions.
|
|
39
|
+
*
|
|
40
|
+
* The floor is the top of whatever the screen keeps at the bottom. A screen
|
|
41
|
+
* with a composer keeps a lot more of it than one with only a status bar, so
|
|
42
|
+
* the inset follows the screen rather than being one number that is wrong on
|
|
43
|
+
* half of them.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* What the bottom keeps when nothing down there has said otherwise.
|
|
47
|
+
*
|
|
48
|
+
* The hints row, the shell's own edge, and the closing border of whatever
|
|
49
|
+
* panel is there. Standing on that border put the figure through it and
|
|
50
|
+
* through the row above it; a row clear of it is the ground.
|
|
51
|
+
*/
|
|
52
|
+
const BOTTOM_CHROME = 3;
|
|
53
|
+
const BoodOverlay = defineComponent('BoodOverlay', () => {
|
|
54
|
+
const app = useApp();
|
|
55
|
+
const name = useStoreValue(BOOD);
|
|
56
|
+
const roaming = useStoreValue(BOOD_FLOAT, false) ?? false;
|
|
57
|
+
useStoreSubtree(BOOD_FLOOR);
|
|
58
|
+
// Whatever is standing at the bottom says where it starts; the creature gets
|
|
59
|
+
// the row above the highest of them. Nothing down there and it takes the
|
|
60
|
+
// bottom of the terminal, less the chrome that is on every screen.
|
|
61
|
+
const standing = boodFloor(app.store);
|
|
62
|
+
const status = useStoreValue(STATUS, 1) ?? 1;
|
|
63
|
+
useStoreSubtree(SESSIONS);
|
|
64
|
+
const session = openSession(app.store);
|
|
65
|
+
if (!roaming || !name)
|
|
66
|
+
return null;
|
|
67
|
+
/*
|
|
68
|
+
* Out of the way of anything that was opened on purpose.
|
|
69
|
+
*
|
|
70
|
+
* A terminal has no per-cell transparency, so the figure writes over
|
|
71
|
+
* whatever it is standing on - which is a fair price for a mascot wandering
|
|
72
|
+
* across a list, and not a fair price for a menu. The completion popup is a
|
|
73
|
+
* layer, the palette is a layer, a dialog is a layer: if anything else is
|
|
74
|
+
* open, the creature is not on top of it.
|
|
75
|
+
*/
|
|
76
|
+
if (app.layers.entries().some((entry) => entry.id !== 'bood'))
|
|
77
|
+
return null;
|
|
78
|
+
return (_jsx(BoodSprite, { name: name, mood: session ? moodOf(decodeStatus(status).activity) : 'happy', floor: standing !== undefined ? standing - 1 : undefined, inset: BOTTOM_CHROME }));
|
|
79
|
+
});
|
|
80
|
+
const Header = defineComponent('ChatHeader', () => {
|
|
81
|
+
const app = useApp();
|
|
82
|
+
const theme = useTheme();
|
|
83
|
+
const host = useStoreValue(HOST);
|
|
84
|
+
const status = useStoreValue(STATUS, 1) ?? 1;
|
|
85
|
+
// Subscribed, not asked. `screens.current()` is a method call in the middle
|
|
86
|
+
// of a render: it reads the right answer once and nothing tells this to look
|
|
87
|
+
// again, so a surface that navigating does not remount keeps the last
|
|
88
|
+
// screen's chrome for ever.
|
|
89
|
+
// Subscribed so the title follows what is open, without the header being
|
|
90
|
+
// remounted to notice.
|
|
91
|
+
useStoreValue(SCREEN, null);
|
|
92
|
+
useStoreValue(OPEN, null);
|
|
93
|
+
// And to the summaries themselves. `openSession` is a plain read, so a
|
|
94
|
+
// title or a status arriving from the host changed the store and left this
|
|
95
|
+
// row showing what it said when the session was opened - which is why the
|
|
96
|
+
// header only caught up when navigating away and back remounted it.
|
|
97
|
+
useStoreSubtree(SESSIONS);
|
|
98
|
+
const session = openSession(app.store);
|
|
99
|
+
const decoded = decodeStatus(status);
|
|
100
|
+
const bood = useStoreValue(BOOD);
|
|
101
|
+
const boodInline = useStoreValue(BOOD_INLINE, false) ?? false;
|
|
102
|
+
// Only on a session, because the seven cells are carrying that session's
|
|
103
|
+
// state - on the catalogue there is no one thing for them to be about, and
|
|
104
|
+
// a creature that means nothing where the name used to be is a worse trade.
|
|
105
|
+
// Truthiness, not a comparison with undefined: nothing open is `null` here,
|
|
106
|
+
// and the row beside this one has always tested it the same way.
|
|
107
|
+
const wearsBood = boodInline && Boolean(session);
|
|
108
|
+
return (
|
|
109
|
+
// Only the title gives way. Everything else on this row is fixed-width and
|
|
110
|
+
// says what the application *is* - a header that truncates its own name to
|
|
111
|
+
// "Assist…" in order to fit more of a session title has given up the one
|
|
112
|
+
// part that is the same on every screen. The workspace yields after the
|
|
113
|
+
// title, and the status glyph never does: it is one cell and it is the
|
|
114
|
+
// thing the row is scanned for.
|
|
115
|
+
_jsxs(Row, { gap: 1, children: [wearsBood
|
|
116
|
+
? _jsx(Creature, { name: bood, form: "inline", mood: moodOf(decoded.activity), shrink: 0 })
|
|
117
|
+
: _jsx("text", { content: "Assistant", bold: true, fg: "accent", shrink: 0 }), _jsx("text", { content: theme.glyphs.separator, fg: "subtle", shrink: 0 }), session ? (_jsxs(_Fragment, { children: [_jsx("text", { content: theme.glyphs[decoded.glyph], fg: decoded.tone, shrink: 0 }), _jsx("text", { content: session.title, flex: 1, truncate: "end" }), _jsx("text", { content: workspaceName(session.workingDirectories[0]), fg: "muted", shrink: 4, truncate: "end" })] })) : (_jsx("text", { content: host?.url ?? 'no host', fg: "muted", flex: 1, truncate: "end" }))] }));
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* The keys, as the reader sees them.
|
|
121
|
+
*
|
|
122
|
+
* They change with where the focus is, because they *are* different there:
|
|
123
|
+
* while the composer has the keyboard, every letter is a letter.
|
|
124
|
+
*/
|
|
125
|
+
const Hints = defineComponent('ChatHints', (props) => {
|
|
126
|
+
const theme = useTheme();
|
|
127
|
+
/**
|
|
128
|
+
* Which key the footer names for a newline.
|
|
129
|
+
*
|
|
130
|
+
* `alt+enter`, which is `ESC CR` and the one encoding every terminal can
|
|
131
|
+
* express. The field also takes `ctrl+enter`, and that is deliberately not
|
|
132
|
+
* named here: it has three encodings - a bare LF, xterm's `CSI 27;5;13~`,
|
|
133
|
+
* kitty's `CSI 13;5u` - and a terminal that sends plain CR for it is
|
|
134
|
+
* sending the enter key, which nothing downstream can tell apart. VS Code's
|
|
135
|
+
* terminal is one such, so naming it offers a key that does not exist.
|
|
136
|
+
*
|
|
137
|
+
* One key in the row rather than two: the row elides every hint on a narrow
|
|
138
|
+
* terminal, and a hint nobody can read costs the same as a wrong one.
|
|
139
|
+
*/
|
|
140
|
+
const newline = 'alt+enter';
|
|
141
|
+
const waiting = useStoreValue(INPUT, null);
|
|
142
|
+
const upDown = `${theme.glyphs.arrowUp}${theme.glyphs.arrowDown}`;
|
|
143
|
+
const leftRight = `${theme.glyphs.arrowLeft}${theme.glyphs.arrowRight}`;
|
|
144
|
+
// Which keys exist is a property of where you are, not of what is open: a
|
|
145
|
+
// session stays open while its changes are on screen, and `i write` there
|
|
146
|
+
// is an offer nothing honours.
|
|
147
|
+
const screen = useStoreValue(SCREEN, 'sessions') ?? 'sessions';
|
|
148
|
+
// Not `status > 1`: the bitset carries "a client has read this" in the same
|
|
149
|
+
// number, so an idle session somebody looked at is 33 and every hint would
|
|
150
|
+
// read "stop".
|
|
151
|
+
const running = useStoreValue(RUNNING, false) ?? false;
|
|
152
|
+
// Where the keyboard is decides what the keys mean. While the composer has
|
|
153
|
+
// it, escape leaves the field; from the transcript, escape leaves the
|
|
154
|
+
// screen - and a hint row that said one of those in both places is wrong
|
|
155
|
+
// half the time.
|
|
156
|
+
const focused = useStoreValue(FOCUS, null);
|
|
157
|
+
const composing = focused === 'chat.composer';
|
|
158
|
+
// A question is not a confirmation, and the keys are not the same either.
|
|
159
|
+
// Offering "a approve" over an elicitation is the same mistake as rendering
|
|
160
|
+
// one as the other, made in the one row that is supposed to explain it.
|
|
161
|
+
if (screen === 'chat' && waiting?.kind === 'toolConfirmation') {
|
|
162
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
163
|
+
{ keys: 'a', label: 'approve' },
|
|
164
|
+
{ keys: 'd', label: 'deny' },
|
|
165
|
+
{ keys: '1-9', label: 'option' },
|
|
166
|
+
{ keys: 'esc', label: 'read' },
|
|
167
|
+
] }));
|
|
168
|
+
}
|
|
169
|
+
if (screen === 'chat' && waiting) {
|
|
170
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
171
|
+
{ keys: 'tab', label: 'next question' },
|
|
172
|
+
{ keys: 'space', label: 'choose' },
|
|
173
|
+
{ keys: 'enter', label: 'send answers' },
|
|
174
|
+
{ keys: 'esc', label: 'read' },
|
|
175
|
+
] }));
|
|
176
|
+
}
|
|
177
|
+
if (screen === 'chat') {
|
|
178
|
+
return (_jsx(KeyHints, { ...props, hints: composing
|
|
179
|
+
? [
|
|
180
|
+
{ keys: 'enter', label: 'send' },
|
|
181
|
+
{ keys: newline, label: 'newline' },
|
|
182
|
+
{ keys: 'esc', label: 'read' },
|
|
183
|
+
{ keys: 'ctrl+c', label: running ? 'stop' : 'quit' },
|
|
184
|
+
]
|
|
185
|
+
: [
|
|
186
|
+
{ keys: upDown, label: 'move' },
|
|
187
|
+
{ keys: 'enter', label: 'expand' },
|
|
188
|
+
{ keys: 'i', label: 'write' },
|
|
189
|
+
{ keys: 'G', label: 'follow' },
|
|
190
|
+
{ keys: 'c', label: 'changes' },
|
|
191
|
+
{ keys: 'k', label: 'skills' },
|
|
192
|
+
{ keys: 'esc', label: 'back' },
|
|
193
|
+
// The same key, and it says which: while a turn is running it
|
|
194
|
+
// stops it, and when none is it leaves. A hint that always read
|
|
195
|
+
// "stop" is wrong most of the time.
|
|
196
|
+
{ keys: 'ctrl+c', label: running ? 'stop' : 'quit' },
|
|
197
|
+
] }));
|
|
198
|
+
}
|
|
199
|
+
if (screen === 'new') {
|
|
200
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
201
|
+
{ keys: 'enter', label: 'start' },
|
|
202
|
+
{ keys: newline, label: 'newline' },
|
|
203
|
+
{ keys: 'tab', label: 'options' },
|
|
204
|
+
{ keys: 'esc', label: 'sessions' },
|
|
205
|
+
{ keys: 'ctrl+c', label: 'quit' },
|
|
206
|
+
] }));
|
|
207
|
+
}
|
|
208
|
+
// On the terminal, `ctrl+c` is the shell's - it is how a command is
|
|
209
|
+
// stopped, and saying "quit" there is how somebody closes the application
|
|
210
|
+
// trying to interrupt a `ping`.
|
|
211
|
+
if (screen === 'terminal') {
|
|
212
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
213
|
+
{ keys: 'enter', label: 'run' },
|
|
214
|
+
{ keys: 'ctrl+c', label: 'interrupt' },
|
|
215
|
+
{ keys: 'esc', label: 'back' },
|
|
216
|
+
{ keys: 'ctrl+p', label: 'commands' },
|
|
217
|
+
] }));
|
|
218
|
+
}
|
|
219
|
+
// The list screens. `tab move` is a form's answer and these are
|
|
220
|
+
// lists: what moves is the cursor, and enter is what a row is for.
|
|
221
|
+
if (screen === 'changes' || screen === 'skills' || screen === 'mcp' || screen === 'files'
|
|
222
|
+
|| screen === 'automations') {
|
|
223
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
224
|
+
{ keys: upDown, label: 'move' },
|
|
225
|
+
{ keys: 'enter', label: screen === 'changes' || screen === 'files' ? 'open' : screen === 'automations' ? 'run' : 'on / off' },
|
|
226
|
+
// Only where they do something. A hint for a key that is inert on
|
|
227
|
+
// this screen is worse than no hint.
|
|
228
|
+
...(screen === 'changes'
|
|
229
|
+
? [{ keys: ']', label: 'changeset' }, { keys: 'r', label: 'read' }, { keys: 'x', label: 'do' }]
|
|
230
|
+
: []),
|
|
231
|
+
// Named only where they do something. `enter` already runs one, so
|
|
232
|
+
// what is left is the switch and the one that does not come back.
|
|
233
|
+
...(screen === 'automations'
|
|
234
|
+
? [{ keys: 'n', label: 'new' }, { keys: 'e', label: 'on / off' }, { keys: 'd', label: 'forget' }]
|
|
235
|
+
: []),
|
|
236
|
+
{ keys: 'esc', label: 'back' },
|
|
237
|
+
{ keys: 'ctrl+p', label: 'commands' },
|
|
238
|
+
{ keys: 'ctrl+c', label: 'quit' },
|
|
239
|
+
] }));
|
|
240
|
+
}
|
|
241
|
+
if (screen !== 'sessions') {
|
|
242
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
243
|
+
{ keys: 'tab', label: 'move' },
|
|
244
|
+
{ keys: 'esc', label: 'back' },
|
|
245
|
+
{ keys: 'ctrl+p', label: 'commands' },
|
|
246
|
+
{ keys: 'ctrl+c', label: 'quit' },
|
|
247
|
+
] }));
|
|
248
|
+
}
|
|
249
|
+
return (_jsx(KeyHints, { ...props, hints: [
|
|
250
|
+
{ keys: upDown, label: 'move' },
|
|
251
|
+
// Which one it is depends on where the detail pane is, and the pane is
|
|
252
|
+
// right there on the screen saying so. Naming both is what fits.
|
|
253
|
+
{ keys: leftRight, label: 'detail' },
|
|
254
|
+
{ keys: 'enter', label: 'open' },
|
|
255
|
+
{ keys: 'n', label: 'new' },
|
|
256
|
+
// `a` archives and `delete` disposes, and neither is here: the row is
|
|
257
|
+
// one line and naming `ctrl+f` cost it the room. Both are a letter
|
|
258
|
+
// away in the palette, and the filter is the one nothing else offers.
|
|
259
|
+
{ keys: 'ctrl+f', label: 'filter' },
|
|
260
|
+
{ keys: 'ctrl+p', label: 'commands' },
|
|
261
|
+
{ keys: 'ctrl+c', label: 'quit' },
|
|
262
|
+
] }));
|
|
263
|
+
});
|
|
264
|
+
const Status = defineComponent('ChatStatus', () => {
|
|
265
|
+
const screen = useStoreValue(SCREEN, 'sessions');
|
|
266
|
+
// What the host last refused, where a person is already looking. A refusal
|
|
267
|
+
// that only reaches a log is a client that appears to have ignored the key
|
|
268
|
+
// you pressed.
|
|
269
|
+
//
|
|
270
|
+
// Unless the row above the composer is already saying it, which it does
|
|
271
|
+
// when the refusal was of an answer given to the block waiting there. Said
|
|
272
|
+
// twice on a narrow terminal it is one sentence in red on two of twenty
|
|
273
|
+
// rows, and the second one adds nothing to the first.
|
|
274
|
+
const said = useStoreValue(INPUT_STATUS, null) ?? null;
|
|
275
|
+
const refusal = useStoreValue(HOST_ERROR, null) ?? null;
|
|
276
|
+
const error = said?.state === 'failed' && said.text === refusal ? null : refusal;
|
|
277
|
+
return (_jsxs(Row, { gap: 2, children: [error
|
|
278
|
+
? _jsx("text", { content: error, fg: "danger", flex: 1, truncate: "end" })
|
|
279
|
+
: _jsx(Hints, { flex: 1 }), _jsx("text", { content: screen ?? '-', fg: "muted" })] }));
|
|
280
|
+
});
|
|
281
|
+
export function registerChat(app, options = {}) {
|
|
282
|
+
const bag = createBag();
|
|
283
|
+
if (options.builtins !== false)
|
|
284
|
+
bag.add(registerBuiltins(app));
|
|
285
|
+
const host = options.host ?? fakeHost();
|
|
286
|
+
const controller = createController(app, host, options.keys);
|
|
287
|
+
app.store.set(WORKSPACE, options.workspace ?? process.cwd());
|
|
288
|
+
app.store.set(SPLIT_AT, options.splitAt ?? SPLIT_DEFAULT);
|
|
289
|
+
// Once, here, rather than per mount: two surfaces drawing two different
|
|
290
|
+
// animals would read as two mascots rather than one that gets about.
|
|
291
|
+
app.store.set(BOOD, pickBood());
|
|
292
|
+
app.store.set(BOOD_INLINE, options.boodInline ?? false);
|
|
293
|
+
app.store.set(BOOD_FLOAT, options.boodFloat ?? false);
|
|
294
|
+
bag.add(controller);
|
|
295
|
+
bag.add(app.services.provide(CONTROLLER, controller));
|
|
296
|
+
for (const [component, render] of [
|
|
297
|
+
['ChatBubble', ChatBubble],
|
|
298
|
+
['StreamingText', StreamingText],
|
|
299
|
+
['ReasoningBlock', ReasoningBlock],
|
|
300
|
+
['ToolCallRow', ToolCallRow],
|
|
301
|
+
['ChatTranscript', ChatTranscript],
|
|
302
|
+
['ChatComposer', ChatComposer],
|
|
303
|
+
['ChatHitl', ChatHitl],
|
|
304
|
+
['SessionList', SessionList],
|
|
305
|
+
['ConnectionBadge', ConnectionBadge],
|
|
306
|
+
['ChangesList', ChangesList],
|
|
307
|
+
['FileList', FileList],
|
|
308
|
+
['AutomationList', AutomationList],
|
|
309
|
+
['SessionsScreen', SessionsScreen],
|
|
310
|
+
['ChatScreen', ChatScreen],
|
|
311
|
+
['NewSessionScreen', NewSessionScreen],
|
|
312
|
+
['TerminalScreen', TerminalScreen],
|
|
313
|
+
['ChangesScreen', ChangesScreen],
|
|
314
|
+
['FilesScreen', FilesScreen],
|
|
315
|
+
['AutomationsScreen', AutomationsScreen],
|
|
316
|
+
['NewAutomationScreen', NewAutomationScreen],
|
|
317
|
+
['SettingsScreen', SettingsScreen],
|
|
318
|
+
['HostsScreen', HostsScreen],
|
|
319
|
+
['SkillsScreen', SkillsScreen],
|
|
320
|
+
['McpScreen', McpScreen],
|
|
321
|
+
['BoodOverlay', BoodOverlay],
|
|
322
|
+
['ChatHeader', Header],
|
|
323
|
+
['ChatStatus', Status],
|
|
324
|
+
['ChatHints', Hints],
|
|
325
|
+
]) {
|
|
326
|
+
bag.add(app.components.register({
|
|
327
|
+
component,
|
|
328
|
+
category: 'template',
|
|
329
|
+
renderer: { kind: 'function', render: render },
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
// Over every screen, for as long as the application is running.
|
|
333
|
+
bag.add(app.layers.open({
|
|
334
|
+
id: 'bood',
|
|
335
|
+
layer: 'floating',
|
|
336
|
+
node: { component: 'BoodOverlay' },
|
|
337
|
+
position: { kind: 'screen', rect: { x: 0, y: 0 } },
|
|
338
|
+
}));
|
|
339
|
+
bag.add(app.surfaces.open({ surface: 'header', key: 'title', target: { component: 'ChatHeader' } }));
|
|
340
|
+
bag.add(app.surfaces.open({ surface: 'status', key: 'status', target: { component: 'ChatStatus' } }));
|
|
341
|
+
for (const screen of [
|
|
342
|
+
{ id: 'sessions', component: 'SessionsScreen' },
|
|
343
|
+
// Kept alive: coming back from the changes list to a conversation that
|
|
344
|
+
// had scrolled itself back to the top is losing your place in a document
|
|
345
|
+
// that is still being written.
|
|
346
|
+
{ id: 'chat', component: 'ChatScreen', keepAlive: true },
|
|
347
|
+
{ id: 'new', component: 'NewSessionScreen' },
|
|
348
|
+
{ id: 'changes', component: 'ChangesScreen' },
|
|
349
|
+
{ id: 'files', component: 'FilesScreen' },
|
|
350
|
+
{ id: 'automations', component: 'AutomationsScreen' },
|
|
351
|
+
{ id: 'automation.new', component: 'NewAutomationScreen' },
|
|
352
|
+
{ id: 'settings', component: 'SettingsScreen' },
|
|
353
|
+
{ id: 'hosts', component: 'HostsScreen' },
|
|
354
|
+
{ id: 'skills', component: 'SkillsScreen' },
|
|
355
|
+
{ id: 'mcp', component: 'McpScreen' },
|
|
356
|
+
// Kept alive: a shell you scrolled back through and then left is a shell
|
|
357
|
+
// that should still be where you left it.
|
|
358
|
+
{ id: 'terminal', component: 'TerminalScreen', keepAlive: true },
|
|
359
|
+
]) {
|
|
360
|
+
bag.add(app.screens.register(screen));
|
|
361
|
+
}
|
|
362
|
+
void controller.refresh();
|
|
363
|
+
// The composer, with nothing open. A client whose first screen is a
|
|
364
|
+
// catalogue makes "talk to an agent" a two-step errand; the first message is
|
|
365
|
+
// what creates the session, so the field is what the application opens on.
|
|
366
|
+
app.screens.reset('new');
|
|
367
|
+
return bag;
|
|
368
|
+
}
|
|
369
|
+
export { CONTROLLER } from './control.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { QueuedMessage, ToolCall, Turn } from './ahp/types.js';
|
|
2
|
+
/**
|
|
3
|
+
* A conversation, flattened into the rows a viewport scrolls.
|
|
4
|
+
*
|
|
5
|
+
* A turn is not a box. It is a run of rows - a header, some prose, a tool
|
|
6
|
+
* call, more prose - and the transcript has to be able to put its cursor on
|
|
7
|
+
* one of them, measure it, and scroll to it. Nesting each turn inside a
|
|
8
|
+
* container would mean the transcript could only ever scroll to a whole turn,
|
|
9
|
+
* and a turn can be four hundred rows long.
|
|
10
|
+
*
|
|
11
|
+
* The order is the host's order. `responseParts` interleaves prose and calls
|
|
12
|
+
* in one stream, and "let me search for those" means something before the
|
|
13
|
+
* searches and nothing after them.
|
|
14
|
+
*/
|
|
15
|
+
export type Block = {
|
|
16
|
+
kind: 'said';
|
|
17
|
+
id: string;
|
|
18
|
+
turnId: string;
|
|
19
|
+
text: string;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'header';
|
|
22
|
+
id: string;
|
|
23
|
+
turnId: string;
|
|
24
|
+
model?: string;
|
|
25
|
+
/**
|
|
26
|
+
* What the turn was asked for besides the model, in the host's words.
|
|
27
|
+
*
|
|
28
|
+
* The values rather than the keys: `thinkingLevel` is one host's name for
|
|
29
|
+
* a property whose *answers* are what a person reads, and a header that
|
|
30
|
+
* spelled out the key would be twice as long and no clearer.
|
|
31
|
+
*/
|
|
32
|
+
settings?: string;
|
|
33
|
+
meta: string;
|
|
34
|
+
state: Turn['state'];
|
|
35
|
+
} | {
|
|
36
|
+
kind: 'prose';
|
|
37
|
+
id: string;
|
|
38
|
+
turnId: string;
|
|
39
|
+
content: string;
|
|
40
|
+
streaming: boolean;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'reasoning';
|
|
43
|
+
id: string;
|
|
44
|
+
turnId: string;
|
|
45
|
+
content: string;
|
|
46
|
+
streaming: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'notice';
|
|
49
|
+
id: string;
|
|
50
|
+
turnId: string;
|
|
51
|
+
content: string;
|
|
52
|
+
} | {
|
|
53
|
+
kind: 'failure';
|
|
54
|
+
id: string;
|
|
55
|
+
turnId: string;
|
|
56
|
+
content: string;
|
|
57
|
+
resumable: boolean;
|
|
58
|
+
} | {
|
|
59
|
+
kind: 'tool';
|
|
60
|
+
id: string;
|
|
61
|
+
turnId: string;
|
|
62
|
+
call: ToolCall;
|
|
63
|
+
} | {
|
|
64
|
+
kind: 'queued';
|
|
65
|
+
id: string;
|
|
66
|
+
messageId: string;
|
|
67
|
+
text: string;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Everything in a block that a person could be looking for.
|
|
71
|
+
*
|
|
72
|
+
* A tool call is its name, its command and what came back, because all three
|
|
73
|
+
* are things somebody searches a transcript for - the file a command touched
|
|
74
|
+
* is in the output and nowhere else. A header is the model and the settings,
|
|
75
|
+
* which is how "where did I switch to opus" is answered.
|
|
76
|
+
*/
|
|
77
|
+
export declare function blockText(block: Block): string;
|
|
78
|
+
/**
|
|
79
|
+
* Where in the conversation a query appears, as block indices in order.
|
|
80
|
+
*
|
|
81
|
+
* Case-insensitive, and a blank query matches nothing rather than everything:
|
|
82
|
+
* a find with no term is a find that has not been typed yet, and lighting up
|
|
83
|
+
* every block for it is the opposite of what the box is for.
|
|
84
|
+
*/
|
|
85
|
+
export declare function findBlocks(blocks: Block[], query: string): number[];
|
|
86
|
+
/** Blocks the cursor stops on: the ones that do something when activated. */
|
|
87
|
+
export declare function selectable(block: Block): boolean;
|
|
88
|
+
export declare function toBlocks(turns: Turn[], queued?: QueuedMessage[]): Block[];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything in a block that a person could be looking for.
|
|
3
|
+
*
|
|
4
|
+
* A tool call is its name, its command and what came back, because all three
|
|
5
|
+
* are things somebody searches a transcript for - the file a command touched
|
|
6
|
+
* is in the output and nowhere else. A header is the model and the settings,
|
|
7
|
+
* which is how "where did I switch to opus" is answered.
|
|
8
|
+
*/
|
|
9
|
+
export function blockText(block) {
|
|
10
|
+
switch (block.kind) {
|
|
11
|
+
case 'said':
|
|
12
|
+
case 'queued':
|
|
13
|
+
return block.text;
|
|
14
|
+
case 'prose':
|
|
15
|
+
case 'reasoning':
|
|
16
|
+
case 'notice':
|
|
17
|
+
case 'failure':
|
|
18
|
+
return block.content;
|
|
19
|
+
case 'header':
|
|
20
|
+
return [block.model, block.settings, block.meta].filter(Boolean).join(' ');
|
|
21
|
+
case 'tool':
|
|
22
|
+
return [
|
|
23
|
+
block.call.name, block.call.toolName, block.call.input,
|
|
24
|
+
block.call.intention, block.call.outcome, block.call.output,
|
|
25
|
+
...(block.call.files ?? []),
|
|
26
|
+
].filter(Boolean).join(' ');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Where in the conversation a query appears, as block indices in order.
|
|
31
|
+
*
|
|
32
|
+
* Case-insensitive, and a blank query matches nothing rather than everything:
|
|
33
|
+
* a find with no term is a find that has not been typed yet, and lighting up
|
|
34
|
+
* every block for it is the opposite of what the box is for.
|
|
35
|
+
*/
|
|
36
|
+
export function findBlocks(blocks, query) {
|
|
37
|
+
const needle = query.trim().toLowerCase();
|
|
38
|
+
if (needle === '')
|
|
39
|
+
return [];
|
|
40
|
+
const found = [];
|
|
41
|
+
blocks.forEach((block, index) => {
|
|
42
|
+
if (blockText(block).toLowerCase().includes(needle))
|
|
43
|
+
found.push(index);
|
|
44
|
+
});
|
|
45
|
+
return found;
|
|
46
|
+
}
|
|
47
|
+
/** Blocks the cursor stops on: the ones that do something when activated. */
|
|
48
|
+
export function selectable(block) {
|
|
49
|
+
// A queued message among them, because taking one back is something you do
|
|
50
|
+
// to it - and the cursor is how anything in the transcript is reached.
|
|
51
|
+
return block.kind === 'tool' || block.kind === 'reasoning' || block.kind === 'queued';
|
|
52
|
+
}
|
|
53
|
+
export function toBlocks(turns, queued = []) {
|
|
54
|
+
const blocks = [];
|
|
55
|
+
for (const turn of turns) {
|
|
56
|
+
if (turn.role === 'user') {
|
|
57
|
+
blocks.push({ kind: 'said', id: `${turn.id}:said`, turnId: turn.id, text: turn.message ?? '' });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const running = turn.state === 'running';
|
|
61
|
+
blocks.push({
|
|
62
|
+
kind: 'header',
|
|
63
|
+
id: `${turn.id}:head`,
|
|
64
|
+
turnId: turn.id,
|
|
65
|
+
...(turn.model ? { model: turn.model.id } : {}),
|
|
66
|
+
...(turn.model?.config ? { settings: Object.values(turn.model.config).join(' · ') } : {}),
|
|
67
|
+
meta: running ? 'running' : turn.elapsedMs ? `${(turn.elapsedMs / 1000).toFixed(1)}s` : '',
|
|
68
|
+
state: turn.state,
|
|
69
|
+
});
|
|
70
|
+
turn.parts.forEach((part, index) => {
|
|
71
|
+
const last = index === turn.parts.length - 1;
|
|
72
|
+
switch (part.kind) {
|
|
73
|
+
case 'markdown':
|
|
74
|
+
blocks.push({ kind: 'prose', id: part.id, turnId: turn.id, content: part.content, streaming: running && last });
|
|
75
|
+
break;
|
|
76
|
+
case 'reasoning':
|
|
77
|
+
blocks.push({ kind: 'reasoning', id: part.id, turnId: turn.id, content: part.content, streaming: running && last });
|
|
78
|
+
break;
|
|
79
|
+
case 'systemNotification':
|
|
80
|
+
blocks.push({ kind: 'notice', id: part.id, turnId: turn.id, content: part.content });
|
|
81
|
+
break;
|
|
82
|
+
case 'toolCall':
|
|
83
|
+
blocks.push({ kind: 'tool', id: part.id, turnId: turn.id, call: part.call });
|
|
84
|
+
break;
|
|
85
|
+
case 'error':
|
|
86
|
+
blocks.push({ kind: 'failure', id: part.id, turnId: turn.id, content: part.message, resumable: part.resumable });
|
|
87
|
+
break;
|
|
88
|
+
default:
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// What the person typed while the agent was busy. Below the conversation
|
|
94
|
+
// because that is when it will be said, and visibly not sent yet.
|
|
95
|
+
// Keyed by the host's own id, not by position: the queue is the host's, the
|
|
96
|
+
// head leaves it whenever the running turn ends, and an index would name a
|
|
97
|
+
// different message every time one did.
|
|
98
|
+
for (const message of queued) {
|
|
99
|
+
blocks.push({ kind: 'queued', id: `queued:${message.id}`, messageId: message.id, text: message.text });
|
|
100
|
+
}
|
|
101
|
+
return blocks;
|
|
102
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Every command, and the argv reading that picks one. */
|
|
2
|
+
export declare const HELP = "ahpc - drive an agent host from a shell\n\n ahpc [--host ws://\u2026] <command> [args] the screen is 'ahpc' with no command\n\nSessions\n session list the catalogue, newest first [--archived] [--json]\n session show <uri> what the host says about one [--full] [--json]\n session new start one [--agent P] [--cwd DIR] [--set k=v]\u2026 [--json]\n session rm <uri> dispose it\n session history <uri> its turns [--all] [--full] [--json]\n session config <uri> the schema and what is in force [--json]\n session set <uri> <k> <v> change one config key\n session read <uri> mark read [--unread]\n session archive <uri> put it away [--undo]\n session customizations <uri> skills, prompts, agents, servers [--json]\n session export <uri> the whole session as one document\n [--json] [--markdown]\n session toggle <uri> <id> turn one on [--off]\n\nTurns\n prompt <uri> <text> say it and stream the answer [--model M] [--json]\n exec <text> a session, one turn, and dispose it\n [--agent P] [--cwd DIR] [--model M] [--json]\n cancel <uri> stop the running turn\n queue <uri> <text> say it after the one running [--model M]\n unqueue <uri> <id> take it back\n\nAnswering\n watch <uri> BLOCK until something wants a person, print, exit\n [--until turn|input|idle] [--timeout S] [--json]\n confirm <uri> <toolCallId> approve a tool call [--deny] [--option ID]\n answer <uri> <requestId> answer a question [--field k=v]\u2026 [--reject]\n\nChats\n chat list <uri> the conversations in a session [--json]\n chat new <uri> [text] another one beside it\n chat rm <chatUri> close one\n\nThe harness\n agents what it serves, and each one's models [--json]\n models every model, by harness [--json]\n commands what a slash offers [--json]\n customizations skills, prompts, agents and MCP servers,\n before any session exists [--kind k] [--json]\n completions <uri> <text> what the host would complete [--offset N] [--json]\n\nChanges and files\n changes <uri> the files a session touched [--json]\n [--list] [--scope s] [--<variable> v]\n [--reviewed f] [--unreviewed f]\n [--operations] what may be done to it\n [--run id] [--file f] [--yes] do one of them\n [--list] every changeset it offers\n [--scope <name>] one of them, e.g. turn\n [--turnId <id>] what a chosen scope still needs\n [--reviewed <file>] tick one off, repeatable\n [--unreviewed <file>] and clear one\n content <uri> <file> one of them, in full\n resource list <uri> a directory the host serves [--json]\n resource read <uri> a file on the host\n resource stat <uri> what it is, without reading it [--json]\n resource write <uri> [file] from a file, or from stdin [--create-only]\n guarded by the file's etag unless [--force]\n resource rm <uri> delete it [--recursive]\n resource mkdir <uri> make a directory\n resource mv <uri> <to> move it [--fail-if-exists]\n resource cp <uri> <to> copy it [--fail-if-exists]\n\nAutomations\n automation list what runs on its own [--json]\n automation show <uri> one of them [--json]\n automation triggers what this host can trigger on [--json]\n automation runs <uri> its history, every page [--json]\n automation run <uri> start it now\n automation enable <uri> switch it on\n automation disable <uri> switch it off\n automation rm <uri> forget it\n\nThe host's own log\n logs what the daemon is saying [--level L] [--follow]\n\nSigning in\n auth what this host protects [--json]\n auth <resource> push a token [--token T] [--expires-in S]\n or set AHPC_TOKEN_<RESOURCE>, or pipe one in\n\nTerminals\n terminal list what is running [--json]\n terminal new open a shell [--cwd DIR] [--name N]\n terminal rm <uri> kill it\n terminal send <uri> <text> type into it\n terminal watch <uri> follow its output [--timeout S]\n\nRecording\n AHPC_RECORD=<file> append every frame, both directions, for\n 'npm run wire' to check against the protocol\n\nAnything else\n dispatch <uri> <type> send one action verbatim [--field k=v]\u2026 [--chat]\n status what this client is connected to [--json]\n help this\n\nThe host\n --host <url> ws://host:port, or AHPC_HOST, or the config file\n --token <tkn> a bearer token for it, or AHPC_TOKEN, or the config file\n --config-file read this instead of the one below\n (none) the scripted host, which needs nothing installed\n\nConfiguration\n config where the file is, and what is in force [--json]\n\nOutput is for reading. --json is the same answer for a program.\n";
|
|
3
|
+
/** A message for the person, not a stack trace. */
|
|
4
|
+
export declare class Fault extends Error {
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* One command, and then the process is done.
|
|
8
|
+
*
|
|
9
|
+
* A switch rather than a registry: every branch is a few lines against
|
|
10
|
+
* `HostConnection`, and the shape of the whole surface being readable in one
|
|
11
|
+
* file is worth more than the indirection a registry would buy.
|
|
12
|
+
*/
|
|
13
|
+
export declare function cli(command: string, rest: string[]): Promise<number>;
|