@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,92 @@
|
|
|
1
|
+
import type { UnicodeLevel } from '@textui/core';
|
|
2
|
+
/**
|
|
3
|
+
* The entry point.
|
|
4
|
+
*
|
|
5
|
+
* Split from `app.tsx` so the example can be *mounted* without being *run*.
|
|
6
|
+
* What is here and not there: the terminal, the quit key, and the clock - the
|
|
7
|
+
* scripted host is driven by a timer here and by a test's own loop there,
|
|
8
|
+
* which is what makes streaming testable at all.
|
|
9
|
+
*/
|
|
10
|
+
interface Options {
|
|
11
|
+
static_: boolean;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
unicode?: UnicodeLevel;
|
|
15
|
+
colors?: number;
|
|
16
|
+
/** Milliseconds a scripted word takes to arrive. */
|
|
17
|
+
tick: number;
|
|
18
|
+
/** Run the script to the end before the first frame, for a still. */
|
|
19
|
+
settled: boolean;
|
|
20
|
+
/** Or exactly this many scripted words, for a still of a turn mid-flight. */
|
|
21
|
+
pump?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Write the still as an SVG here instead of ANSI on stdout.
|
|
24
|
+
*
|
|
25
|
+
* The form a still can be *looked at* in - a README, the docs, a pull
|
|
26
|
+
* request. An `.ans` file is only a screenshot on a terminal, so the places
|
|
27
|
+
* that most want to show what this looks like are the ones that cannot
|
|
28
|
+
* replay one.
|
|
29
|
+
*/
|
|
30
|
+
svg?: string;
|
|
31
|
+
/** Which screen a still opens on. Absent is the one the application boots to. */
|
|
32
|
+
screen?: string;
|
|
33
|
+
session?: string;
|
|
34
|
+
theme: string;
|
|
35
|
+
shell: string;
|
|
36
|
+
/** The header trades its name for a creature, on an open session. */
|
|
37
|
+
boodInline: boolean;
|
|
38
|
+
/** The creature roams the whole application. */
|
|
39
|
+
boodFloat: boolean;
|
|
40
|
+
/** Say something on the open session before the frame is taken. */
|
|
41
|
+
say?: string;
|
|
42
|
+
/** Answer the confirmation the script stops at, to reach the question. */
|
|
43
|
+
approve: boolean;
|
|
44
|
+
/** ...and then answer the question, to reach the end of the turn. */
|
|
45
|
+
answer: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* A real host: `ws://127.0.0.1:9187`, or wherever the editor advertises one.
|
|
48
|
+
*
|
|
49
|
+
* Left off, the scripted host runs - which is the point of the seam. The
|
|
50
|
+
* fake is for driving a shape on purpose (a blocked confirmation, a failing
|
|
51
|
+
* turn, a question) and the real one is for finding out what a host actually
|
|
52
|
+
* sends. Nothing above `HostConnection` knows which is which.
|
|
53
|
+
*/
|
|
54
|
+
host?: string;
|
|
55
|
+
token?: string;
|
|
56
|
+
/** Read this config instead of the one XDG names. */
|
|
57
|
+
configFile?: string;
|
|
58
|
+
/** Keys from the config file, over the ones this client ships with. */
|
|
59
|
+
keys?: Record<string, string | null>;
|
|
60
|
+
/**
|
|
61
|
+
* Where the agent works.
|
|
62
|
+
*
|
|
63
|
+
* **A path on the host, not on this machine** - the same thing the
|
|
64
|
+
* `compose.workspace` command says, and the reason there is one flag rather
|
|
65
|
+
* than two. It defaults to nothing at all: the host is somewhere else, its
|
|
66
|
+
* filesystem is not this one, and a client that sent its own cwd would be
|
|
67
|
+
* naming a directory that does not exist there.
|
|
68
|
+
*/
|
|
69
|
+
path?: string;
|
|
70
|
+
/**
|
|
71
|
+
* A directory on *this* machine to serve back to the host.
|
|
72
|
+
*
|
|
73
|
+
* The screen rather than the CLI, because a published directory is only
|
|
74
|
+
* reachable while a connection is open: `ahpc session list` answers and
|
|
75
|
+
* exits, so a host has nothing left to ask. Absent means nothing is served.
|
|
76
|
+
*/
|
|
77
|
+
publish?: string;
|
|
78
|
+
/** Whether the host may write into it. Read-only otherwise. */
|
|
79
|
+
publishWritable?: boolean;
|
|
80
|
+
help: boolean;
|
|
81
|
+
}
|
|
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 --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
|
+
export declare function parse(argv: string[]): Options;
|
|
84
|
+
/**
|
|
85
|
+
* The screen.
|
|
86
|
+
*
|
|
87
|
+
* Exported rather than run, so `main.tsx` can decide between this and the CLI
|
|
88
|
+
* without either being loaded to make the choice - this file pulls in a whole
|
|
89
|
+
* renderer, and `ahpc session list` should not pay for one.
|
|
90
|
+
*/
|
|
91
|
+
export declare function tui(argv: string[]): Promise<void>;
|
|
92
|
+
export {};
|
package/dist/src/tui.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { WRITER_KEY, createApp } from '@textui/core';
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { bufferToSvg, createNodeTerminal, createWriter, renderStill, } from '@textui/terminal';
|
|
4
|
+
import { registerChat } from './app.js';
|
|
5
|
+
import { CONTROLLER } from './control.js';
|
|
6
|
+
import { connect, sink } from './connect.js';
|
|
7
|
+
import { loadConfig } from './config.js';
|
|
8
|
+
import { reportHostError } from './state.js';
|
|
9
|
+
export const USAGE = `ahpc - a terminal client for the Agent Host Protocol
|
|
10
|
+
|
|
11
|
+
ahpc [options]
|
|
12
|
+
|
|
13
|
+
The host
|
|
14
|
+
--host <url> A live agent host, ws://host:port
|
|
15
|
+
--token <tkn> A bearer token for it
|
|
16
|
+
--config-file <f> Read this instead of ~/.config/ahpc/config.json
|
|
17
|
+
(none of these) The scripted host, which needs nothing installed
|
|
18
|
+
|
|
19
|
+
Where the agent works
|
|
20
|
+
--path <dir> A path on the host, not on this machine. The host
|
|
21
|
+
has to serve it, and says so if it does not.
|
|
22
|
+
Left out, the host decides.
|
|
23
|
+
|
|
24
|
+
What this client serves back
|
|
25
|
+
--publish <dir> Serve this directory to the host under
|
|
26
|
+
virtual://<clientId>/. Nothing is served without
|
|
27
|
+
it, and every such request is refused.
|
|
28
|
+
--publish-writable Let the host write into it. Read-only otherwise.
|
|
29
|
+
Serving lasts as long as the screen does: with no
|
|
30
|
+
terminal attached this prints one frame and exits,
|
|
31
|
+
so a background shell publishes nothing.
|
|
32
|
+
|
|
33
|
+
Appearance
|
|
34
|
+
--theme <name> workbench, paper-light, ...
|
|
35
|
+
--shell <name> The shell layout
|
|
36
|
+
--screen <name> Which screen to open on
|
|
37
|
+
--bood Let the creature loose on the whole screen. It
|
|
38
|
+
keeps off the composer and off anything asking
|
|
39
|
+
a question, and alt+g turns it off again.
|
|
40
|
+
--session <uri> Open this session
|
|
41
|
+
|
|
42
|
+
Stills, for a README or a test
|
|
43
|
+
--static, -s One frame to stdout instead of running
|
|
44
|
+
--width, -w <n> Columns
|
|
45
|
+
--height <n> Rows
|
|
46
|
+
--unicode <level> ascii, bmp, full
|
|
47
|
+
--colors <n> 0, 4, 8 or 24
|
|
48
|
+
--svg <file> Write the still as SVG here
|
|
49
|
+
--tick <ms> Milliseconds per scripted word
|
|
50
|
+
--settled Run the script out before the frame
|
|
51
|
+
--pump <n> Or exactly this many scripted words
|
|
52
|
+
--say <text> Say this on the open session first
|
|
53
|
+
--approve Answer the confirmation the script stops at
|
|
54
|
+
--answer ...and then the question
|
|
55
|
+
|
|
56
|
+
--help, -h This
|
|
57
|
+
|
|
58
|
+
Commands
|
|
59
|
+
ahpc <command> ... Drive a host without the screen. 'ahpc help' lists
|
|
60
|
+
them: sessions, prompts, approvals, terminals.
|
|
61
|
+
`;
|
|
62
|
+
export function parse(argv) {
|
|
63
|
+
const options = {
|
|
64
|
+
static_: false,
|
|
65
|
+
width: process.stdout.columns ?? 100,
|
|
66
|
+
height: process.stdout.rows ?? 30,
|
|
67
|
+
tick: 40,
|
|
68
|
+
settled: false,
|
|
69
|
+
theme: 'paper',
|
|
70
|
+
shell: 'workbench',
|
|
71
|
+
boodInline: false,
|
|
72
|
+
boodFloat: false,
|
|
73
|
+
approve: false,
|
|
74
|
+
answer: false,
|
|
75
|
+
help: false,
|
|
76
|
+
};
|
|
77
|
+
for (let i = 0; i < argv.length; i++) {
|
|
78
|
+
switch (argv[i]) {
|
|
79
|
+
case '--static':
|
|
80
|
+
case '-s':
|
|
81
|
+
options.static_ = true;
|
|
82
|
+
break;
|
|
83
|
+
case '--width':
|
|
84
|
+
case '-w':
|
|
85
|
+
options.width = Number(argv[++i]);
|
|
86
|
+
break;
|
|
87
|
+
case '--height':
|
|
88
|
+
options.height = Number(argv[++i]);
|
|
89
|
+
break;
|
|
90
|
+
case '--unicode':
|
|
91
|
+
options.unicode = argv[++i];
|
|
92
|
+
break;
|
|
93
|
+
case '--colors':
|
|
94
|
+
options.colors = Number(argv[++i]);
|
|
95
|
+
break;
|
|
96
|
+
case '--tick':
|
|
97
|
+
options.tick = Number(argv[++i]);
|
|
98
|
+
break;
|
|
99
|
+
case '--settled':
|
|
100
|
+
options.settled = true;
|
|
101
|
+
break;
|
|
102
|
+
case '--pump':
|
|
103
|
+
options.pump = Number(argv[++i]);
|
|
104
|
+
break;
|
|
105
|
+
case '--svg':
|
|
106
|
+
options.svg = String(argv[++i]);
|
|
107
|
+
break;
|
|
108
|
+
case '--say':
|
|
109
|
+
options.say = String(argv[++i]);
|
|
110
|
+
break;
|
|
111
|
+
case '--approve':
|
|
112
|
+
options.approve = true;
|
|
113
|
+
break;
|
|
114
|
+
case '--answer':
|
|
115
|
+
options.answer = true;
|
|
116
|
+
break;
|
|
117
|
+
case '--screen':
|
|
118
|
+
options.screen = String(argv[++i]);
|
|
119
|
+
break;
|
|
120
|
+
case '--theme':
|
|
121
|
+
options.theme = String(argv[++i]);
|
|
122
|
+
break;
|
|
123
|
+
case '--shell':
|
|
124
|
+
options.shell = String(argv[++i]);
|
|
125
|
+
break;
|
|
126
|
+
case '--bood':
|
|
127
|
+
options.boodFloat = true;
|
|
128
|
+
break;
|
|
129
|
+
case '--session':
|
|
130
|
+
options.session = String(argv[++i]);
|
|
131
|
+
break;
|
|
132
|
+
case '--host':
|
|
133
|
+
options.host = String(argv[++i]);
|
|
134
|
+
break;
|
|
135
|
+
case '--token':
|
|
136
|
+
options.token = String(argv[++i]);
|
|
137
|
+
break;
|
|
138
|
+
case '--config-file':
|
|
139
|
+
options.configFile = String(argv[++i]);
|
|
140
|
+
break;
|
|
141
|
+
case '--path':
|
|
142
|
+
options.path = String(argv[++i]);
|
|
143
|
+
break;
|
|
144
|
+
case '--publish':
|
|
145
|
+
options.publish = String(argv[++i]);
|
|
146
|
+
break;
|
|
147
|
+
case '--publish-writable':
|
|
148
|
+
options.publishWritable = true;
|
|
149
|
+
break;
|
|
150
|
+
case '--help':
|
|
151
|
+
case '-h':
|
|
152
|
+
options.help = true;
|
|
153
|
+
break;
|
|
154
|
+
// A flag nobody reads is a flag nobody can rely on: an unknown one is
|
|
155
|
+
// said so rather than silently doing what the defaults would have done.
|
|
156
|
+
default:
|
|
157
|
+
if (argv[i]?.startsWith('-')) {
|
|
158
|
+
process.stderr.write(`Unknown option ${argv[i]}. Try --help.\n`);
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return options;
|
|
165
|
+
}
|
|
166
|
+
function overrides(options) {
|
|
167
|
+
return {
|
|
168
|
+
...(options.unicode ? { unicode: options.unicode, wideChars: options.unicode !== 'ascii' } : {}),
|
|
169
|
+
...(options.colors !== undefined ? { colorDepth: options.colors } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Where a new session works, as this client can honestly answer it.
|
|
174
|
+
*
|
|
175
|
+
* `--path` when it was given, and it is a path on the **host**. Otherwise
|
|
176
|
+
* nothing: the host's filesystem is not this one, so a path from here is one
|
|
177
|
+
* it has never heard of, and the honest answer is to let the host decide.
|
|
178
|
+
*/
|
|
179
|
+
const workspaceFor = (options) => options.path ?? '';
|
|
180
|
+
/** One frame, to stdout. The same application, against a terminal that is a size. */
|
|
181
|
+
async function still(options) {
|
|
182
|
+
const host = await connect(options);
|
|
183
|
+
const { text } = await renderStill({
|
|
184
|
+
width: options.width,
|
|
185
|
+
height: options.height,
|
|
186
|
+
capabilities: overrides(options),
|
|
187
|
+
theme: options.theme,
|
|
188
|
+
shell: options.shell,
|
|
189
|
+
onBoot: (booted) => {
|
|
190
|
+
registerChat(booted, {
|
|
191
|
+
host, workspace: workspaceFor(options),
|
|
192
|
+
boodInline: options.boodInline, boodFloat: options.boodFloat,
|
|
193
|
+
...(options.keys ? { keys: options.keys } : {}),
|
|
194
|
+
});
|
|
195
|
+
},
|
|
196
|
+
// A still of a turn mid-flight is what `--pump` is for: run a fixed number
|
|
197
|
+
// of scripted words rather than all of them, and the caret is wherever the
|
|
198
|
+
// agent had got to. `--settled` runs until the script has nothing left it
|
|
199
|
+
// can do without being answered, which is how the confirmation is reached.
|
|
200
|
+
before: (app) => {
|
|
201
|
+
const controller = app.services.require(CONTROLLER);
|
|
202
|
+
if (options.session)
|
|
203
|
+
controller.open(options.session);
|
|
204
|
+
/*
|
|
205
|
+
* Pushed when asked for, and not otherwise.
|
|
206
|
+
*
|
|
207
|
+
* `screen` used to default to `sessions` and the push was skipped for
|
|
208
|
+
* that value, so `--screen sessions` was the one screen a still could
|
|
209
|
+
* not take - the session list, which is the first thing a reader of the
|
|
210
|
+
* README wants to see. Absent means the screen the application boots
|
|
211
|
+
* to, which is what a still with no flags has always shown.
|
|
212
|
+
*/
|
|
213
|
+
if (options.screen)
|
|
214
|
+
app.screens.push(options.screen);
|
|
215
|
+
if (options.say)
|
|
216
|
+
controller.send(options.say);
|
|
217
|
+
const steps = options.pump ?? (options.settled ? 100_000 : 0);
|
|
218
|
+
for (let i = 0; i < steps; i++)
|
|
219
|
+
if (host.pump?.() !== true)
|
|
220
|
+
break;
|
|
221
|
+
if (options.approve) {
|
|
222
|
+
controller.approve();
|
|
223
|
+
for (let i = 0; i < 100_000; i++)
|
|
224
|
+
if (host.pump?.() !== true)
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (options.answer) {
|
|
228
|
+
controller.answer({ q1: { kind: 'selected', value: 'transcript-scope' } }, true);
|
|
229
|
+
for (let i = 0; i < 100_000; i++)
|
|
230
|
+
if (host.pump?.() !== true)
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
after: async (app) => {
|
|
235
|
+
if (options.svg === undefined)
|
|
236
|
+
return;
|
|
237
|
+
// The theme's own two colours, not the exporter's defaults: a cell left
|
|
238
|
+
// at the terminal default means "whatever the emulator is set to", and
|
|
239
|
+
// the honest answer for a picture of *this* application is the
|
|
240
|
+
// background it was drawn against.
|
|
241
|
+
await writeFile(options.svg, `${bufferToSvg(app.buffer(), {
|
|
242
|
+
background: app.theme.colors.canvas,
|
|
243
|
+
foreground: app.theme.colors.text,
|
|
244
|
+
title: `chat - ${options.screen ?? 'new'}`,
|
|
245
|
+
})}\n`, 'utf8');
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
// The frame is drawn, so let the host go. A live one is a socket and a
|
|
249
|
+
// local one is a subprocess, and either keeps the event loop alive after
|
|
250
|
+
// `main` has returned - a still that renders correctly and then hangs for
|
|
251
|
+
// ever, which is what every `--host` still did.
|
|
252
|
+
await host.close?.();
|
|
253
|
+
if (options.svg !== undefined) {
|
|
254
|
+
process.stderr.write(`${options.svg}\n`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
process.stdout.write(`${text}\n`);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* The screen.
|
|
261
|
+
*
|
|
262
|
+
* Exported rather than run, so `main.tsx` can decide between this and the CLI
|
|
263
|
+
* without either being loaded to make the choice - this file pulls in a whole
|
|
264
|
+
* renderer, and `ahpc session list` should not pay for one.
|
|
265
|
+
*/
|
|
266
|
+
export async function tui(argv) {
|
|
267
|
+
const options = parse(argv);
|
|
268
|
+
/*
|
|
269
|
+
* The file, under whatever was typed.
|
|
270
|
+
*
|
|
271
|
+
* Same order as the CLI uses, because the two front ends answering
|
|
272
|
+
* differently about which host to talk to is the one difference nobody
|
|
273
|
+
* would think to look for.
|
|
274
|
+
*/
|
|
275
|
+
const file = loadConfig('ahpc', options.configFile);
|
|
276
|
+
options.host = options.host ?? process.env.AHPC_HOST ?? file.host;
|
|
277
|
+
options.token = options.token ?? process.env.AHPC_TOKEN ?? file.token;
|
|
278
|
+
if (file.theme && !argv.includes('--theme'))
|
|
279
|
+
options.theme = file.theme;
|
|
280
|
+
if (file.shell && !argv.includes('--shell'))
|
|
281
|
+
options.shell = file.shell;
|
|
282
|
+
if (file.boodInline !== undefined)
|
|
283
|
+
options.boodInline = file.boodInline;
|
|
284
|
+
if (file.boodFloat !== undefined && !argv.includes('--bood'))
|
|
285
|
+
options.boodFloat = file.boodFloat;
|
|
286
|
+
if (file.keys)
|
|
287
|
+
options.keys = file.keys;
|
|
288
|
+
if (options.help) {
|
|
289
|
+
process.stdout.write(USAGE);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (options.static_ || !process.stdout.isTTY) {
|
|
293
|
+
await still(options);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const terminal = createNodeTerminal();
|
|
297
|
+
const host = await connect(options);
|
|
298
|
+
const app = createApp({
|
|
299
|
+
terminal,
|
|
300
|
+
// A starting point, not a fixture. `alt+t` and the palette change both
|
|
301
|
+
// while it runs, and the screens are the same graph under either.
|
|
302
|
+
theme: options.theme,
|
|
303
|
+
shell: options.shell,
|
|
304
|
+
session: { managed: true, altScreen: true, mouse: true, title: 'assistant' },
|
|
305
|
+
onBoot: (booted) => {
|
|
306
|
+
registerChat(booted, {
|
|
307
|
+
host, workspace: workspaceFor(options),
|
|
308
|
+
boodInline: options.boodInline, boodFloat: options.boodFloat,
|
|
309
|
+
...(options.keys ? { keys: options.keys } : {}),
|
|
310
|
+
});
|
|
311
|
+
booted.commands.register({
|
|
312
|
+
id: 'app.quit',
|
|
313
|
+
title: 'Quit',
|
|
314
|
+
slots: ['palette'],
|
|
315
|
+
run: () => void app.stop().then(() => process.exit(0)),
|
|
316
|
+
});
|
|
317
|
+
// `q` is *not* bound. The focused composer would take it first anyway,
|
|
318
|
+
// but a quit key that exists only where it happens to be unread is a
|
|
319
|
+
// quit key nobody can rely on - so it is ctrl+c and the palette.
|
|
320
|
+
//
|
|
321
|
+
// `ctrl+c` is registered for the turn *and* for this, in that order:
|
|
322
|
+
// while something is running it stops it, and when nothing is, the
|
|
323
|
+
// first binding does not apply and this one does. Cancel what is
|
|
324
|
+
// happening, or leave if nothing is - which is what the key means
|
|
325
|
+
// everywhere else.
|
|
326
|
+
booted.keybindings.register({ keys: 'ctrl+c', commandId: 'app.quit' });
|
|
327
|
+
booted.keybindings.register({ keys: 'ctrl+q', commandId: 'app.quit' });
|
|
328
|
+
/*
|
|
329
|
+
* A rebinding that names nothing is said out loud.
|
|
330
|
+
*
|
|
331
|
+
* Checked here rather than where the bindings are registered, because
|
|
332
|
+
* this is the first point at which every command exists - `app.quit`
|
|
333
|
+
* above is registered by the shell rather than by the controller, and a
|
|
334
|
+
* check that ran earlier would report it as unknown. A chord bound to a
|
|
335
|
+
* name no command answers to does nothing at all, which from the
|
|
336
|
+
* keyboard is indistinguishable from a config file that never loaded.
|
|
337
|
+
*/
|
|
338
|
+
for (const [chord, id] of Object.entries(options.keys ?? {})) {
|
|
339
|
+
if (id !== null && !booted.commands.get(id)) {
|
|
340
|
+
sink.report(`Config: ${chord} is bound to "${id}", which is not a command. Run 'ahpc config --json' for the ones there are.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
app.services.provide(WRITER_KEY, createWriter(terminal.capabilities()));
|
|
346
|
+
sink.report = (message) => reportHostError(app.store, message);
|
|
347
|
+
await app.start();
|
|
348
|
+
/**
|
|
349
|
+
* The last resort, and the reason it exists at all.
|
|
350
|
+
*
|
|
351
|
+
* A promise nobody caught ends the Node process, and this process is holding
|
|
352
|
+
* a terminal in its alternate screen with the cursor hidden and raw mode on.
|
|
353
|
+
* Exiting from there leaves a shell nobody can type into. So whatever it is,
|
|
354
|
+
* the application is stopped first - which puts the terminal back - and then
|
|
355
|
+
* the error is printed where it can be read.
|
|
356
|
+
*/
|
|
357
|
+
const bail = (label) => (error) => {
|
|
358
|
+
void app.stop().finally(() => {
|
|
359
|
+
process.stderr.write(`${label}: ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
process.on('unhandledRejection', bail('Unhandled rejection'));
|
|
364
|
+
process.on('uncaughtException', bail('Uncaught exception'));
|
|
365
|
+
// The clock, for the scripted host only. A real connection has a socket
|
|
366
|
+
// pushing actions, and everything above it cannot tell the difference -
|
|
367
|
+
// which is why this is the only line that has to know.
|
|
368
|
+
if (host.pump) {
|
|
369
|
+
const timer = setInterval(() => { host.pump?.(); }, Math.max(1, options.tick));
|
|
370
|
+
timer.unref?.();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BoxProps, RenderOutput } from '@textui/core';
|
|
2
|
+
import type { Automation } from '../ahp/types.js';
|
|
3
|
+
/**
|
|
4
|
+
* What the host will do without being asked.
|
|
5
|
+
*
|
|
6
|
+
* The catalogue is sessions somebody started; this is the other half - the
|
|
7
|
+
* ones that start themselves. Both halves are the host's, and a client that
|
|
8
|
+
* drew only the first makes a session appearing at nine in the morning look
|
|
9
|
+
* like something nobody can account for.
|
|
10
|
+
*
|
|
11
|
+
* Two facts per row and they are not the same fact. The **schedule** is what
|
|
12
|
+
* was written down and is true whatever the host does with it; **next run** is
|
|
13
|
+
* what the host will actually do. A host with no clock, an automation switched
|
|
14
|
+
* off and an expression the host could not read all say the same thing here -
|
|
15
|
+
* a schedule with nothing coming - and that is worth showing rather than
|
|
16
|
+
* hiding behind the expression.
|
|
17
|
+
*/
|
|
18
|
+
/** "in 4h 12m". A next run is only ever ahead, so there is no past tense. */
|
|
19
|
+
export declare function until(iso: string, from?: number): string;
|
|
20
|
+
/** The schedule as written, with the zone only when it is not the obvious one. */
|
|
21
|
+
export declare function scheduleOf(automation: Automation): string;
|
|
22
|
+
export interface AutomationListProps extends BoxProps {
|
|
23
|
+
automations: Automation[];
|
|
24
|
+
/** The cursor moved. What a key acting on "this one" needs. */
|
|
25
|
+
onSelect?(uri: string): void;
|
|
26
|
+
/** Enter on a row. */
|
|
27
|
+
onOpen?(uri: string): void;
|
|
28
|
+
focusId?: string;
|
|
29
|
+
autoFocus?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare const AutomationList: (props: AutomationListProps) => RenderOutput;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
|
|
2
|
+
import { defineComponent, useTheme } from '@textui/core';
|
|
3
|
+
import { Column, EmptyState, List, Marquee, Row } from '@textui/widgets';
|
|
4
|
+
/**
|
|
5
|
+
* What the host will do without being asked.
|
|
6
|
+
*
|
|
7
|
+
* The catalogue is sessions somebody started; this is the other half - the
|
|
8
|
+
* ones that start themselves. Both halves are the host's, and a client that
|
|
9
|
+
* drew only the first makes a session appearing at nine in the morning look
|
|
10
|
+
* like something nobody can account for.
|
|
11
|
+
*
|
|
12
|
+
* Two facts per row and they are not the same fact. The **schedule** is what
|
|
13
|
+
* was written down and is true whatever the host does with it; **next run** is
|
|
14
|
+
* what the host will actually do. A host with no clock, an automation switched
|
|
15
|
+
* off and an expression the host could not read all say the same thing here -
|
|
16
|
+
* a schedule with nothing coming - and that is worth showing rather than
|
|
17
|
+
* hiding behind the expression.
|
|
18
|
+
*/
|
|
19
|
+
/** "in 4h 12m". A next run is only ever ahead, so there is no past tense. */
|
|
20
|
+
export function until(iso, from = Date.now()) {
|
|
21
|
+
const ms = new Date(iso).getTime() - from;
|
|
22
|
+
if (!Number.isFinite(ms))
|
|
23
|
+
return '';
|
|
24
|
+
if (ms <= 0)
|
|
25
|
+
return 'due';
|
|
26
|
+
const minutes = Math.floor(ms / 60_000);
|
|
27
|
+
const days = Math.floor(minutes / 1440);
|
|
28
|
+
const hours = Math.floor((minutes % 1440) / 60);
|
|
29
|
+
const rest = minutes % 60;
|
|
30
|
+
if (days > 0)
|
|
31
|
+
return `in ${days}d ${hours}h`;
|
|
32
|
+
if (hours > 0)
|
|
33
|
+
return `in ${hours}h ${rest}m`;
|
|
34
|
+
return `in ${rest}m`;
|
|
35
|
+
}
|
|
36
|
+
/** The schedule as written, with the zone only when it is not the obvious one. */
|
|
37
|
+
export function scheduleOf(automation) {
|
|
38
|
+
if (!automation.schedule)
|
|
39
|
+
return 'manual only';
|
|
40
|
+
const { expression, timeZone } = automation.schedule;
|
|
41
|
+
return timeZone && timeZone !== 'UTC' ? `${expression} ${timeZone}` : expression;
|
|
42
|
+
}
|
|
43
|
+
const RUN_TONE = {
|
|
44
|
+
completed: 'success',
|
|
45
|
+
failed: 'danger',
|
|
46
|
+
cancelled: 'warning',
|
|
47
|
+
running: 'accent',
|
|
48
|
+
pending: 'muted',
|
|
49
|
+
};
|
|
50
|
+
export const AutomationList = defineComponent('AutomationList', (props) => {
|
|
51
|
+
const { automations, onSelect, onOpen, focusId, autoFocus, ...rest } = props;
|
|
52
|
+
const theme = useTheme();
|
|
53
|
+
if (automations.length === 0) {
|
|
54
|
+
return (_jsx(EmptyState, { title: "No automations", message: "This host holds none. One that fires on a schedule starts a session with nobody at the keyboard.", ...rest }));
|
|
55
|
+
}
|
|
56
|
+
const byUri = new Map(automations.map((one) => [one.resource, one]));
|
|
57
|
+
const items = automations.map((one) => ({
|
|
58
|
+
id: one.resource,
|
|
59
|
+
// Filled for one that will fire, hollow for one that will not. The
|
|
60
|
+
// switch and the clock are different questions and this answers the
|
|
61
|
+
// first; the meta answers the second.
|
|
62
|
+
icon: one.enabled ? theme.glyphs.bulletFilled : theme.glyphs.bulletHollow,
|
|
63
|
+
label: one.title,
|
|
64
|
+
meta: scheduleOf(one),
|
|
65
|
+
tone: (one.enabled ? 'default' : 'muted'),
|
|
66
|
+
}));
|
|
67
|
+
return (_jsx(List, { items: items, flex: 1, ...(focusId ? { focusId } : {}), ...(autoFocus ? { autoFocus } : {}), ...(onSelect ? { onSelect: (id) => onSelect(id) } : {}), ...(onOpen ? { onActivate: (id) => onOpen(id) } : {}), renderItem: (item, state) => {
|
|
68
|
+
const one = byUri.get(item.id);
|
|
69
|
+
const runs = one?.runs.slice(0, 3) ?? [];
|
|
70
|
+
return (_jsxs(Column, { children: [_jsxs(Row, { gap: 1, children: [_jsx("text", { content: item.icon ?? '', ...(state.selected ? {} : { fg: one?.enabled ? 'accent' : 'muted' }), shrink: 0 }), _jsx(Marquee, { content: item.label, active: state.selected && state.focused, flex: 1, ...(one?.enabled ? {} : { fg: 'muted' }) }), one?.enabled === false ? _jsx("text", { content: "off", fg: "muted", shrink: 0 }) : null] }), _jsxs(Row, { gap: 1, children: [_jsx("text", { content: " ", shrink: 0 }), _jsx("text", { content: item.meta ?? '', fg: "subtle", shrink: 0 }), _jsx("text", { content: one?.nextRunAt ? `${theme.glyphs.separator} ${until(one.nextRunAt)}` : `${theme.glyphs.separator} nothing scheduled`, ...(one?.nextRunAt ? { fg: 'info' } : { fg: 'subtle' }), shrink: 0 }), runs.map((run) => (_jsx("text", { content: run.status === 'completed'
|
|
71
|
+
? theme.glyphs.check
|
|
72
|
+
: run.status === 'failed'
|
|
73
|
+
? theme.glyphs.cross
|
|
74
|
+
: theme.glyphs.bulletFilled, fg: RUN_TONE[run.status] ?? 'muted', shrink: 0 }, run.resource)))] })] }));
|
|
75
|
+
}, ...rest }));
|
|
76
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drawing tag.
|
|
3
|
+
*
|
|
4
|
+
* A tagged template reading `strings.raw`, because this is drawing. A figure
|
|
5
|
+
* whose every backslash has to be doubled to survive the parser is one
|
|
6
|
+
* somebody will eventually get wrong, and the mistake does not look like a
|
|
7
|
+
* mistake - it looks like a slightly worse cat.
|
|
8
|
+
*
|
|
9
|
+
* art`
|
|
10
|
+
* /\_/\
|
|
11
|
+
* ( ^.^ )
|
|
12
|
+
* `
|
|
13
|
+
*
|
|
14
|
+
* The leading and trailing blank lines are the ones the backticks sit on, and
|
|
15
|
+
* they are dropped. Nothing else is: art starts at column zero in the file,
|
|
16
|
+
* because leading spaces are part of the animal and a dedent would eat them.
|
|
17
|
+
*/
|
|
18
|
+
export declare function art(strings: TemplateStringsArray): string[];
|
|
19
|
+
/** One cell's frames, however the file wrote them. One frame, or several. */
|
|
20
|
+
export declare function framesOf(cell: string[] | string[][]): string[][];
|
|
21
|
+
/**
|
|
22
|
+
* Square a form off: one width, one height, across every mood and every frame.
|
|
23
|
+
*
|
|
24
|
+
* Padded once, here, rather than by the compositor - because the compositor
|
|
25
|
+
* pads to the widest row *it* was given, and a centred figure with one short
|
|
26
|
+
* row leans. And because a frame one row taller than the next does not read as
|
|
27
|
+
* animation, it reads as the line underneath it jumping.
|
|
28
|
+
*/
|
|
29
|
+
export declare function square(cells: string[][][]): {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* A long open and a short shut.
|
|
35
|
+
*
|
|
36
|
+
* Two frames alternating at the ticker's rate is not a blink, it is a strobe.
|
|
37
|
+
* The hold lives in the data rather than in the component, because how long a
|
|
38
|
+
* cat's eyes stay open is a fact about the cat.
|
|
39
|
+
*/
|
|
40
|
+
export declare const blink: (open: string[], shut: string[]) => string[][];
|
|
41
|
+
/** Two frames, each held for two - a slow alternation inside a fast ticker. */
|
|
42
|
+
export declare const alternate: (a: string[], b: string[]) => string[][];
|
|
43
|
+
/**
|
|
44
|
+
* Fill a pose's slots: a run of `%` takes the face, a run of `#` the tell.
|
|
45
|
+
*
|
|
46
|
+
* The run is written exactly as wide as the token that fills it, so a filled
|
|
47
|
+
* row is the same length as the row it came from and nothing downstream has to
|
|
48
|
+
* re-measure. `registerCreature` checks that at registration; the padding here
|
|
49
|
+
* is what happens when somebody registers their own creature at runtime and
|
|
50
|
+
* got it wrong, and a short token is better than a row that shifts.
|
|
51
|
+
*/
|
|
52
|
+
export declare function fill(rows: string[], face: string, tell: string): string[];
|
|
53
|
+
/**
|
|
54
|
+
* What one frame is, and where the animal is inside it.
|
|
55
|
+
*
|
|
56
|
+
* The anchor is the middle of the face slot - the creature's identity point -
|
|
57
|
+
* and a figure is placed by it rather than by its left edge. That is what lets
|
|
58
|
+
* a cat turn side-on: the walking drawing is twice as wide as the front one
|
|
59
|
+
* with the head at the far end, and anchored on the face the body swings round
|
|
60
|
+
* a head that stays where it was. Anchored on the corner it would jump ten
|
|
61
|
+
* cells every time the cat turned round.
|
|
62
|
+
*
|
|
63
|
+
* A pose that pins its own face has no slot to read, so it falls back to the
|
|
64
|
+
* middle, which is where a symmetrical drawing's identity is anyway.
|
|
65
|
+
*/
|
|
66
|
+
export declare function metric(rows: string[]): {
|
|
67
|
+
width: number;
|
|
68
|
+
height: number;
|
|
69
|
+
anchor: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* The anchor a whole pose is placed by: frame zero's, shared by every frame.
|
|
73
|
+
*
|
|
74
|
+
* Per-frame anchors would hold the face still and slide everything else, which
|
|
75
|
+
* is the wrong half of the motion to believe. The cat at the keyboard bobs its
|
|
76
|
+
* head one cell between frames; taking the anchor from frame zero keeps the
|
|
77
|
+
* desk still and moves the head, which is what the drawing means.
|
|
78
|
+
*/
|
|
79
|
+
export declare function anchorOf(frames: string[][]): number;
|
|
80
|
+
/**
|
|
81
|
+
* Write a figure into a field of rows, bottom-aligned and anchored.
|
|
82
|
+
*
|
|
83
|
+
* Bottom-aligned because the feet own the y: the landing squash is a row
|
|
84
|
+
* shorter than the stand, and padding underneath would sink the creature
|
|
85
|
+
* through the floor at exactly the moment it hits it.
|
|
86
|
+
*
|
|
87
|
+
* Anything off the field is dropped rather than wrapped, so a creature halfway
|
|
88
|
+
* out of a narrow box is half a creature instead of a smear down the far side.
|
|
89
|
+
*/
|
|
90
|
+
export declare function blit(field: string[], rows: string[], x: number, y: number, anchor: number): string[];
|