@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
package/dist/src/blocks.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Block } from '@textui/chat';
|
|
2
|
+
import type { QueuedMessage, Turn } from './ahp/types.js';
|
|
3
|
+
export type { Block } from '@textui/chat';
|
|
4
|
+
export { selectable } from '@textui/chat';
|
|
2
5
|
/**
|
|
3
6
|
* A conversation, flattened into the rows a viewport scrolls.
|
|
4
7
|
*
|
|
@@ -12,77 +15,4 @@ import type { QueuedMessage, ToolCall, Turn } from './ahp/types.js';
|
|
|
12
15
|
* in one stream, and "let me search for those" means something before the
|
|
13
16
|
* searches and nothing after them.
|
|
14
17
|
*/
|
|
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
18
|
export declare function toBlocks(turns: Turn[], queued?: QueuedMessage[]): Block[];
|
package/dist/src/blocks.js
CHANGED
|
@@ -1,55 +1,17 @@
|
|
|
1
|
+
export { selectable } from '@textui/chat';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
+
* A conversation, flattened into the rows a viewport scrolls.
|
|
3
4
|
*
|
|
4
|
-
* A
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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.
|
|
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.
|
|
31
10
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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.
|
|
35
14
|
*/
|
|
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
15
|
export function toBlocks(turns, queued = []) {
|
|
54
16
|
const blocks = [];
|
|
55
17
|
for (const turn of turns) {
|
package/dist/src/cli/main.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
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
|
|
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 --wire <file> append every frame, both directions, as JSON\n lines: { at, from, peer, frame }, for\n 'npm run wire' to check against the protocol.\n AHPC_RECORD=<file> is the same, from a shell\n wire <file> watch a capture as it is written, from either\n end: one row per frame, a row opens to the\n frame. Off a terminal, one line per frame\n [--follow] [--filter text] [--json]\n\nServing these sessions to something else\n mcp MCP on stdin and stdout, for a client that\n launches this process\n serve the same tools on a socket, shared\n [--serve-host H] [--serve-port N] [--serve-token T]\n [--serve-origin URL]\u2026 a browser page allowed in\n /mcp is MCP; /api/<tool> is plain JSON\n both take [--mcp-tools G,\u2026] extra tool groups to serve, on top of the\n sessions ones: resources, terminals,\n automations, changes\n\nAnything else\n dispatch <uri> <type> send one action verbatim [--field k=v]\u2026 [--chat]\n <uri>, anywhere above a session URI, or the agent-host-session://\n link the host's session tools answer with\n status what this client is connected to [--json]\n --version what version this is\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
3
|
/** A message for the person, not a stack trace. */
|
|
4
4
|
export declare class Fault extends Error {
|
|
5
5
|
}
|
package/dist/src/cli/main.js
CHANGED
|
@@ -5,6 +5,7 @@ import { configPath, loadConfig } from '../config.js';
|
|
|
5
5
|
import { ago, archived, branch, json, line, mark, project, table } from './render.js';
|
|
6
6
|
import { operate } from '../ahp/operate.js';
|
|
7
7
|
import { SWITCHES } from '../flags.js';
|
|
8
|
+
import { parseSessionLink, sessionOfLink } from '../links.js';
|
|
8
9
|
import { spoken, turn as runTurn, until } from '../wait.js';
|
|
9
10
|
import { GROUPS, served } from '../mcp/tools.js';
|
|
10
11
|
import { SERVER } from '../mcp/serve.js';
|
|
@@ -104,8 +105,14 @@ Terminals
|
|
|
104
105
|
terminal watch <uri> follow its output [--timeout S]
|
|
105
106
|
|
|
106
107
|
Recording
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
--wire <file> append every frame, both directions, as JSON
|
|
109
|
+
lines: { at, from, peer, frame }, for
|
|
110
|
+
'npm run wire' to check against the protocol.
|
|
111
|
+
AHPC_RECORD=<file> is the same, from a shell
|
|
112
|
+
wire <file> watch a capture as it is written, from either
|
|
113
|
+
end: one row per frame, a row opens to the
|
|
114
|
+
frame. Off a terminal, one line per frame
|
|
115
|
+
[--follow] [--filter text] [--json]
|
|
109
116
|
|
|
110
117
|
Serving these sessions to something else
|
|
111
118
|
mcp MCP on stdin and stdout, for a client that
|
|
@@ -120,6 +127,8 @@ Serving these sessions to something else
|
|
|
120
127
|
|
|
121
128
|
Anything else
|
|
122
129
|
dispatch <uri> <type> send one action verbatim [--field k=v]… [--chat]
|
|
130
|
+
<uri>, anywhere above a session URI, or the agent-host-session://
|
|
131
|
+
link the host's session tools answer with
|
|
123
132
|
status what this client is connected to [--json]
|
|
124
133
|
--version what version this is
|
|
125
134
|
help this
|
|
@@ -147,6 +156,24 @@ class Args {
|
|
|
147
156
|
constructor(rest) {
|
|
148
157
|
this.rest = rest;
|
|
149
158
|
}
|
|
159
|
+
/** Every positional that is a link, replaced by what it names. */
|
|
160
|
+
async resolveLinks(resolve) {
|
|
161
|
+
const next = [...this.rest];
|
|
162
|
+
for (let i = 0; i < next.length; i++) {
|
|
163
|
+
const word = next[i];
|
|
164
|
+
if (word.startsWith('--')) {
|
|
165
|
+
if (!SWITCHES.has(word))
|
|
166
|
+
i++;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (!/^agent-host-session:\/\//i.test(word))
|
|
170
|
+
continue;
|
|
171
|
+
const found = await resolve(word);
|
|
172
|
+
if (found !== undefined)
|
|
173
|
+
next[i] = found;
|
|
174
|
+
}
|
|
175
|
+
this.rest = next;
|
|
176
|
+
}
|
|
150
177
|
/** The nth thing that is not a flag or a flag's value. */
|
|
151
178
|
positional(index) {
|
|
152
179
|
const found = [];
|
|
@@ -209,6 +236,7 @@ const where = (args) => {
|
|
|
209
236
|
// write into it. Both off unless asked for.
|
|
210
237
|
...(args.value('--publish') ? { publish: args.value('--publish') } : {}),
|
|
211
238
|
...(args.has('--publish-writable') ? { publishWritable: true } : {}),
|
|
239
|
+
...(args.value('--wire') ? { wire: args.value('--wire') } : {}),
|
|
212
240
|
};
|
|
213
241
|
};
|
|
214
242
|
/** A URI the command needs, said plainly when it is missing. */
|
|
@@ -314,8 +342,53 @@ export async function cli(command, rest) {
|
|
|
314
342
|
table(rows);
|
|
315
343
|
return 0;
|
|
316
344
|
}
|
|
345
|
+
/*
|
|
346
|
+
* A capture is a file, not a host.
|
|
347
|
+
*
|
|
348
|
+
* `ahpc wire out.jsonl` reads what `--wire` wrote - here or on the host -
|
|
349
|
+
* and needs no connection to do it; needing one would make it useless on
|
|
350
|
+
* exactly the capture of a host that would not answer.
|
|
351
|
+
*/
|
|
352
|
+
if (command === 'wire') {
|
|
353
|
+
const file = args.positional(0);
|
|
354
|
+
if (file === undefined)
|
|
355
|
+
throw new Fault('wire wants a file: ahpc wire <file>');
|
|
356
|
+
const settings = loadConfig('ahpc', args.value('--config-file'));
|
|
357
|
+
const { wireTui } = await import('../wiretui.js');
|
|
358
|
+
const theme = args.value('--theme') ?? settings.theme;
|
|
359
|
+
const shell = args.value('--shell') ?? settings.shell;
|
|
360
|
+
const filter = args.value('--filter');
|
|
361
|
+
await wireTui({
|
|
362
|
+
file,
|
|
363
|
+
follow: args.has('--follow'),
|
|
364
|
+
json: wants,
|
|
365
|
+
...(filter === undefined ? {} : { filter }),
|
|
366
|
+
...(theme === undefined ? {} : { theme }),
|
|
367
|
+
...(shell === undefined ? {} : { shell }),
|
|
368
|
+
});
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
317
371
|
const host = await connect(where(args));
|
|
318
372
|
try {
|
|
373
|
+
/*
|
|
374
|
+
* A link where a URI is wanted.
|
|
375
|
+
*
|
|
376
|
+
* The reference host's session tools answer with `agent-host-session://`
|
|
377
|
+
* links, and a person pasting one into `session show` should get the
|
|
378
|
+
* session it names rather than a refusal about the scheme. Resolved once,
|
|
379
|
+
* against the catalogue, before any command reads its arguments; a link
|
|
380
|
+
* nobody on this host answers to is a sentence rather than a lookup that
|
|
381
|
+
* silently found nothing.
|
|
382
|
+
*/
|
|
383
|
+
await args.resolveLinks(async (link) => {
|
|
384
|
+
const parsed = parseSessionLink(link);
|
|
385
|
+
if (parsed === undefined)
|
|
386
|
+
return undefined;
|
|
387
|
+
const row = sessionOfLink(parsed, await host.listSessions());
|
|
388
|
+
if (row === undefined)
|
|
389
|
+
throw new Fault(`No session on this host matches ${link}.`);
|
|
390
|
+
return row.resource;
|
|
391
|
+
});
|
|
319
392
|
switch (command) {
|
|
320
393
|
case 'status': {
|
|
321
394
|
const rows = await host.listSessions().catch(() => []);
|
package/dist/src/connect.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface Where {
|
|
|
29
29
|
publish?: string;
|
|
30
30
|
/** Whether the published directory may be written to. Read-only otherwise. */
|
|
31
31
|
publishWritable?: boolean;
|
|
32
|
+
/** A file every frame is appended to, both directions, as JSON lines. */
|
|
33
|
+
wire?: string;
|
|
32
34
|
}
|
|
33
35
|
/**
|
|
34
36
|
* Where a refusal goes before there is an application to put it in.
|
package/dist/src/connect.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function connect(options) {
|
|
|
27
27
|
return await liveHost({
|
|
28
28
|
url: options.host,
|
|
29
29
|
...(options.token ? { token: options.token } : {}),
|
|
30
|
+
...(options.wire ? { wire: options.wire } : {}),
|
|
30
31
|
onRefusal: (_uri, message) => sink.report(message),
|
|
31
32
|
onLimit: (message) => sink.report(message),
|
|
32
33
|
// Work the host is doing under a token of its own. Reported while it
|
package/dist/src/control.d.ts
CHANGED
|
@@ -17,6 +17,12 @@ import type { Agent, Answer, Automation, Changeset, ChangesetScope, ChatSource,
|
|
|
17
17
|
export interface Controller {
|
|
18
18
|
refresh(): Promise<void>;
|
|
19
19
|
open(uri: SessionUri): void;
|
|
20
|
+
/**
|
|
21
|
+
* Open what an `agent-host-session://` link names: the session, and the
|
|
22
|
+
* chat in it when the link says one. False when this host has no such
|
|
23
|
+
* session, said out loud as well.
|
|
24
|
+
*/
|
|
25
|
+
openLink(link: string): Promise<boolean>;
|
|
20
26
|
close(): void;
|
|
21
27
|
/** Send, or queue when a turn is already running. */
|
|
22
28
|
send(text: string): void;
|
package/dist/src/control.js
CHANGED
|
@@ -3,12 +3,13 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { confirm } from '@textui/widgets';
|
|
7
|
-
import { findBlocks,
|
|
6
|
+
import { confirm, pick } from '@textui/widgets';
|
|
7
|
+
import { findBlocks, valueIcon } from '@textui/chat';
|
|
8
|
+
import { toBlocks } from './blocks.js';
|
|
9
|
+
import { chatMatches, linksIn, parseSessionLink, sessionOfLink } from './links.js';
|
|
8
10
|
import { operate } from './ahp/operate.js';
|
|
9
11
|
import { createTerminals } from './terminal.js';
|
|
10
12
|
import { SessionFlag } from './ahp/types.js';
|
|
11
|
-
import { valueIcon } from './view/icons.js';
|
|
12
13
|
import { ARCHIVED, BOOD_FLOAT, CAN_ADD_CHAT, CAN_FORK, CAN_SIDE_CHAT, CHAT_URI, CHATS, CUSTOMIZATIONS, CURSOR, DRAFT, EXPANDED, FILTER, FIND, FINDING, FIND_AT, HAS_CHATS, HOST, HOST_ERROR, INPUT, MODEL, MODEL_CONFIG, OPEN_TERMINAL, AUTOMATIONS, AUTOMATION_ROW, CHANGES as CHANGES_AT_PATH, CHANGE_AT, CHANGE_ROW, CHANGE_SCOPES, FILES_AT, FILES_OPEN, OPEN, OPEN_FILE, PROVIDER, MARKDOWN, QUEUE, RUNNING, SCREEN, SELECTED, SETTINGS, SIDEBAR, SPLIT_AT, SPLIT_DEFAULT, TURNS, WORKSPACE, applyEvent, inputRefused, pendingInput, queue, reportHostError, sendingInput, sessions, turns, writeSessions, writeStatus, } from './state.js';
|
|
13
14
|
export const CONTROLLER = serviceKey('chat.controller');
|
|
14
15
|
/** The command that asks about one config key. Registered when a host offers it. */
|
|
@@ -427,6 +428,50 @@ export function createController(app, host, bindings) {
|
|
|
427
428
|
if (next)
|
|
428
429
|
controller.openChat(next);
|
|
429
430
|
},
|
|
431
|
+
async openLink(link) {
|
|
432
|
+
const parsed = parseSessionLink(link);
|
|
433
|
+
if (parsed === undefined) {
|
|
434
|
+
failed(new Error(`${link} is not an agent-host-session:// link`));
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
437
|
+
// The catalogue as held, then as the host has it now: a session the
|
|
438
|
+
// agent made a moment ago may not have reached the list yet.
|
|
439
|
+
let row = sessionOfLink(parsed, sessions(app.store));
|
|
440
|
+
if (row === undefined) {
|
|
441
|
+
await controller.refresh();
|
|
442
|
+
row = sessionOfLink(parsed, sessions(app.store));
|
|
443
|
+
}
|
|
444
|
+
if (row === undefined) {
|
|
445
|
+
failed(new Error(`No session on this host matches ${link}`));
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
controller.open(row.resource);
|
|
449
|
+
if (parsed.chatId === undefined)
|
|
450
|
+
return true;
|
|
451
|
+
/*
|
|
452
|
+
* The chat, once the session has said which chats it has.
|
|
453
|
+
*
|
|
454
|
+
* The list arrives with the snapshot, a moment after `open`, and a chat
|
|
455
|
+
* URI is not something to build here - the host names them. Waited for,
|
|
456
|
+
* briefly; a chat the session no longer has is the session itself,
|
|
457
|
+
* which is the reference window's answer too.
|
|
458
|
+
*/
|
|
459
|
+
const wanted = parsed.chatId;
|
|
460
|
+
for (let i = 0; i < 40; i++) {
|
|
461
|
+
if (app.store.get(OPEN) !== row.resource)
|
|
462
|
+
return true;
|
|
463
|
+
// Not before the default chat is known: `open` sets it when the
|
|
464
|
+
// detail lands, and a switch made earlier is a switch it undoes.
|
|
465
|
+
const settled = app.store.get(CHAT_URI) !== null;
|
|
466
|
+
const chat = (app.store.get(CHATS) ?? []).find((entry) => chatMatches(entry.resource, wanted));
|
|
467
|
+
if (settled && chat !== undefined) {
|
|
468
|
+
controller.openChat(chat.resource);
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
await new Promise((resolve) => { setTimeout(resolve, 50); });
|
|
472
|
+
}
|
|
473
|
+
return true;
|
|
474
|
+
},
|
|
430
475
|
open(uri) {
|
|
431
476
|
// Closing drops this consumer only. Unsubscribing the channel to shed a
|
|
432
477
|
// duplicate is what silently kills the stream everything else reads.
|
|
@@ -805,6 +850,29 @@ connection) {
|
|
|
805
850
|
*/
|
|
806
851
|
const provider = () => app.store.get(PROVIDER) ?? 'claude';
|
|
807
852
|
const agent = () => known.agents.find((found) => found.provider === provider());
|
|
853
|
+
/**
|
|
854
|
+
* The keys of a session's config that are about its directory.
|
|
855
|
+
*
|
|
856
|
+
* The host's own, named in the reference client's `sessionConfigKeys.ts`:
|
|
857
|
+
* whether to work in a worktree, from which branch, and how that worktree
|
|
858
|
+
* is made. An answer to any of them is an answer about one repository.
|
|
859
|
+
*/
|
|
860
|
+
const ABOUT_WHERE = ['isolation', 'branch', 'worktreeBranchPrefix', 'worktreeBranchTrack', 'worktreeCreateNewBranch', 'worktreeIncludeFiles'];
|
|
861
|
+
/**
|
|
862
|
+
* Point the next session somewhere else.
|
|
863
|
+
*
|
|
864
|
+
* What was answered about the old directory goes with it: `branch: main`
|
|
865
|
+
* chosen in one repository would be sent back to the host as the answer
|
|
866
|
+
* for the next, and the host echoes what it is told - so the composer
|
|
867
|
+
* would show a branch the new repository may not have, and start from it.
|
|
868
|
+
* The rest of the answers are about the harness, not the directory, and
|
|
869
|
+
* stay.
|
|
870
|
+
*/
|
|
871
|
+
const workspace = (path) => {
|
|
872
|
+
app.store.set(WORKSPACE, path);
|
|
873
|
+
const held = app.store.get(SETTINGS) ?? {};
|
|
874
|
+
app.store.set(SETTINGS, Object.fromEntries(Object.entries(held).filter(([key]) => !ABOUT_WHERE.includes(key))));
|
|
875
|
+
};
|
|
808
876
|
const listAgents = async () => {
|
|
809
877
|
try {
|
|
810
878
|
known.agents = await controller.agents();
|
|
@@ -1078,6 +1146,39 @@ connection) {
|
|
|
1078
1146
|
slots: ['palette'],
|
|
1079
1147
|
run: () => { app.screens.reset('new'); app.focus.focus('chat.composer'); },
|
|
1080
1148
|
},
|
|
1149
|
+
{
|
|
1150
|
+
/*
|
|
1151
|
+
* A link in the transcript, followed.
|
|
1152
|
+
*
|
|
1153
|
+
* The reference host's session tools answer with an
|
|
1154
|
+
* `agent-host-session://` link, and its window makes one a click. A
|
|
1155
|
+
* terminal has no click, so the links in the open transcript are the
|
|
1156
|
+
* choices, and one chosen opens the session or the chat it names. One
|
|
1157
|
+
* typed is followed the same way, which is how a link from somewhere
|
|
1158
|
+
* else gets opened.
|
|
1159
|
+
*/
|
|
1160
|
+
id: 'chat.openLink',
|
|
1161
|
+
title: 'Open a session link',
|
|
1162
|
+
category: 'Session',
|
|
1163
|
+
description: 'Follow an agent-host-session:// link in this transcript',
|
|
1164
|
+
slots: ['palette'],
|
|
1165
|
+
when: `${OPEN}`,
|
|
1166
|
+
args: [{
|
|
1167
|
+
name: 'link',
|
|
1168
|
+
type: 'string',
|
|
1169
|
+
required: true,
|
|
1170
|
+
description: 'The link to follow',
|
|
1171
|
+
choices: () => linksIn(app.store.get(TURNS) ?? []).map((found) => ({
|
|
1172
|
+
value: found.link,
|
|
1173
|
+
label: found.link.replace(/^agent-host-session:\/\//, ''),
|
|
1174
|
+
...(found.context ? { description: found.context } : {}),
|
|
1175
|
+
})),
|
|
1176
|
+
}],
|
|
1177
|
+
run: (args) => {
|
|
1178
|
+
void controller.openLink(String(args.link ?? '')).then((opened) => { if (opened)
|
|
1179
|
+
app.screens.push('chat'); });
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1081
1182
|
{
|
|
1082
1183
|
id: 'go.changes',
|
|
1083
1184
|
title: 'What this session changed',
|
|
@@ -1610,7 +1711,7 @@ connection) {
|
|
|
1610
1711
|
id: 'terminal.new',
|
|
1611
1712
|
title: 'Open a terminal',
|
|
1612
1713
|
category: 'Terminal',
|
|
1613
|
-
description: 'Start a shell on the host, in
|
|
1714
|
+
description: 'Start a shell on the host, in the workspace directory',
|
|
1614
1715
|
slots: ['palette'],
|
|
1615
1716
|
run: () => {
|
|
1616
1717
|
const where = app.store.get(WORKSPACE);
|
|
@@ -1684,13 +1785,41 @@ connection) {
|
|
|
1684
1785
|
id: 'compose.workspace',
|
|
1685
1786
|
title: 'Workspace',
|
|
1686
1787
|
category: 'Compose',
|
|
1687
|
-
description: '
|
|
1788
|
+
description: 'Choose the directory the session works in, from the host\'s own',
|
|
1789
|
+
slots: ['palette'],
|
|
1790
|
+
when: `!${OPEN}`,
|
|
1791
|
+
/*
|
|
1792
|
+
* Picked by looking, the way the reference client picks a folder: it
|
|
1793
|
+
* lists the host's directories over `resourceList`, and so does this,
|
|
1794
|
+
* through the `file:` provider `registerChat` mounts. The dialog is
|
|
1795
|
+
* textui's, which walks the resource registry and never the disk this
|
|
1796
|
+
* client is on - a path typed blind was the only way before, and it
|
|
1797
|
+
* had to be a path on a machine you could not see.
|
|
1798
|
+
*
|
|
1799
|
+
* Started where the chip already points, when the host will list it;
|
|
1800
|
+
* a directory off the catalogue otherwise, which is somewhere on the
|
|
1801
|
+
* host's machine that is known to exist.
|
|
1802
|
+
*/
|
|
1803
|
+
run: async () => {
|
|
1804
|
+
const held = app.store.get(WORKSPACE) ?? '';
|
|
1805
|
+
const at = `file://${held}`;
|
|
1806
|
+
const served = sessions(app.store).flatMap((one) => one.workingDirectories)[0];
|
|
1807
|
+
const listable = held !== '' && await controller.files(at).then(() => true, () => false);
|
|
1808
|
+
const start = listable ? at : served ?? at;
|
|
1809
|
+
const picked = await pick(app, { start, wants: 'directory', title: 'Workspace', placeholder: 'Filter this directory…' });
|
|
1810
|
+
if (picked !== null)
|
|
1811
|
+
workspace(decodeURIComponent(picked.replace(/^file:\/\//, '')));
|
|
1812
|
+
},
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
id: 'compose.workspace.path',
|
|
1816
|
+
title: 'Workspace path',
|
|
1817
|
+
category: 'Compose',
|
|
1818
|
+
description: 'Type the directory the session works in',
|
|
1688
1819
|
slots: ['palette'],
|
|
1689
1820
|
when: `!${OPEN}`,
|
|
1690
|
-
//
|
|
1691
|
-
//
|
|
1692
|
-
// `choices` function later and the same chip becomes a list of
|
|
1693
|
-
// workspaces without anything else changing.
|
|
1821
|
+
// For a host that lists nothing - one without the `resource*` family -
|
|
1822
|
+
// and for a path already known, which is quicker typed than walked to.
|
|
1694
1823
|
args: [{
|
|
1695
1824
|
name: 'path',
|
|
1696
1825
|
type: 'string',
|
|
@@ -1700,7 +1829,7 @@ connection) {
|
|
|
1700
1829
|
run: (args) => {
|
|
1701
1830
|
const path = String(args.path ?? '').trim();
|
|
1702
1831
|
if (path)
|
|
1703
|
-
|
|
1832
|
+
workspace(path);
|
|
1704
1833
|
},
|
|
1705
1834
|
},
|
|
1706
1835
|
{
|
|
@@ -2195,6 +2324,7 @@ function shipped() {
|
|
|
2195
2324
|
// out of it is escape - the pair that makes every other letter reachable.
|
|
2196
2325
|
{ keys: 'c', commandId: 'go.changes', scopeId: CHAT_SCOPE },
|
|
2197
2326
|
{ keys: 'f', commandId: 'go.files', scopeId: CHAT_SCOPE },
|
|
2327
|
+
{ keys: 'l', commandId: 'chat.openLink', scopeId: CHAT_SCOPE },
|
|
2198
2328
|
/*
|
|
2199
2329
|
* On the changes screen, and nowhere else.
|
|
2200
2330
|
*
|
package/dist/src/flags.js
CHANGED
|
@@ -27,7 +27,7 @@ export const COMMANDS = new Set([
|
|
|
27
27
|
'agents', 'models', 'commands', 'customizations', 'completions', 'changes', 'content',
|
|
28
28
|
'prompt', 'exec', 'cancel', 'queue', 'unqueue',
|
|
29
29
|
'watch', 'confirm', 'answer', 'dispatch',
|
|
30
|
-
'mcp', 'serve',
|
|
30
|
+
'mcp', 'serve', 'wire',
|
|
31
31
|
]);
|
|
32
32
|
/**
|
|
33
33
|
* Every flag that takes no value, in either front end.
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
import type { SessionSummary, Turn } from './ahp/types.js';
|
|
14
|
+
export interface SessionLink {
|
|
15
|
+
provider: string;
|
|
16
|
+
id: string;
|
|
17
|
+
chatId?: string;
|
|
18
|
+
turnId?: string;
|
|
19
|
+
}
|
|
20
|
+
/** The link read, or undefined for text that is not one. */
|
|
21
|
+
export declare function parseSessionLink(text: string): SessionLink | undefined;
|
|
22
|
+
/** The id inside a session URI, whatever scheme it is under: `ahp-session:/x` and `claude:/x` are both `x`. */
|
|
23
|
+
export declare const idOf: (uri: string) => string;
|
|
24
|
+
/**
|
|
25
|
+
* The row a link names.
|
|
26
|
+
*
|
|
27
|
+
* Matched on the id and the provider, whichever scheme the row is under: a
|
|
28
|
+
* session this client started is `ahp-session:/` and one read off the host's
|
|
29
|
+
* catalogue is `<provider>:/`, and the link says neither.
|
|
30
|
+
*/
|
|
31
|
+
export declare const sessionOfLink: (link: SessionLink, rows: SessionSummary[]) => SessionSummary | undefined;
|
|
32
|
+
/** One link found in a transcript, and where. */
|
|
33
|
+
export interface FoundLink {
|
|
34
|
+
link: string;
|
|
35
|
+
/** What was around it, for the row that offers it. */
|
|
36
|
+
context: string;
|
|
37
|
+
turnId: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Every link in the transcript, once each, in the order they appear.
|
|
41
|
+
*
|
|
42
|
+
* Read from what a person can see: the prose, the tool calls' outcomes and
|
|
43
|
+
* outputs, and the host's notices. A tool answering `create_session` puts the
|
|
44
|
+
* link in its output, which is where somebody reading the transcript finds it.
|
|
45
|
+
*/
|
|
46
|
+
export declare function linksIn(turns: Turn[]): FoundLink[];
|
|
47
|
+
/**
|
|
48
|
+
* Whether a chat URI is the chat a link names.
|
|
49
|
+
*
|
|
50
|
+
* The id is the URI's authority where it has one - `ahp-chat://<chatId>/<session>`,
|
|
51
|
+
* which is how the reference host and ahpd spell a chat - and the path
|
|
52
|
+
* otherwise, `ahp-chat:/<id>`, which is the protocol's own shape.
|
|
53
|
+
*/
|
|
54
|
+
export declare const chatMatches: (chatUri: string, chatId: string) => boolean;
|