@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +296 -0
  3. package/dist/src/ahp/channels.d.ts +146 -0
  4. package/dist/src/ahp/channels.js +423 -0
  5. package/dist/src/ahp/connection.d.ts +656 -0
  6. package/dist/src/ahp/connection.js +1 -0
  7. package/dist/src/ahp/fake.d.ts +69 -0
  8. package/dist/src/ahp/fake.js +1996 -0
  9. package/dist/src/ahp/live.d.ts +160 -0
  10. package/dist/src/ahp/live.js +2641 -0
  11. package/dist/src/ahp/operate.d.ts +36 -0
  12. package/dist/src/ahp/operate.js +56 -0
  13. package/dist/src/ahp/publish.d.ts +39 -0
  14. package/dist/src/ahp/publish.js +370 -0
  15. package/dist/src/ahp/status.d.ts +23 -0
  16. package/dist/src/ahp/status.js +23 -0
  17. package/dist/src/ahp/types.d.ts +745 -0
  18. package/dist/src/ahp/types.js +27 -0
  19. package/dist/src/app.d.ts +54 -0
  20. package/dist/src/app.js +369 -0
  21. package/dist/src/blocks.d.ts +88 -0
  22. package/dist/src/blocks.js +102 -0
  23. package/dist/src/cli/main.d.ts +13 -0
  24. package/dist/src/cli/main.js +1537 -0
  25. package/dist/src/cli/render.d.ts +43 -0
  26. package/dist/src/cli/render.js +91 -0
  27. package/dist/src/config.d.ts +63 -0
  28. package/dist/src/config.js +45 -0
  29. package/dist/src/connect.d.ts +53 -0
  30. package/dist/src/connect.js +69 -0
  31. package/dist/src/control.d.ts +189 -0
  32. package/dist/src/control.js +2241 -0
  33. package/dist/src/diff.d.ts +44 -0
  34. package/dist/src/diff.js +111 -0
  35. package/dist/src/flags.d.ts +28 -0
  36. package/dist/src/flags.js +76 -0
  37. package/dist/src/main.d.ts +10 -0
  38. package/dist/src/main.js +54 -0
  39. package/dist/src/schedule.d.ts +46 -0
  40. package/dist/src/schedule.js +140 -0
  41. package/dist/src/screens.d.ts +79 -0
  42. package/dist/src/screens.js +1322 -0
  43. package/dist/src/state.d.ts +355 -0
  44. package/dist/src/state.js +531 -0
  45. package/dist/src/terminal.d.ts +44 -0
  46. package/dist/src/terminal.js +101 -0
  47. package/dist/src/tui.d.ts +92 -0
  48. package/dist/src/tui.js +372 -0
  49. package/dist/src/view/automations.d.ts +31 -0
  50. package/dist/src/view/automations.js +76 -0
  51. package/dist/src/view/bood/art.d.ts +90 -0
  52. package/dist/src/view/bood/art.js +152 -0
  53. package/dist/src/view/bood/beetle.d.ts +7 -0
  54. package/dist/src/view/bood/beetle.js +123 -0
  55. package/dist/src/view/bood/bunny.d.ts +8 -0
  56. package/dist/src/view/bood/bunny.js +228 -0
  57. package/dist/src/view/bood/cat.d.ts +9 -0
  58. package/dist/src/view/bood/cat.js +358 -0
  59. package/dist/src/view/bood/crab.d.ts +7 -0
  60. package/dist/src/view/bood/crab.js +121 -0
  61. package/dist/src/view/bood/index.d.ts +29 -0
  62. package/dist/src/view/bood/index.js +25 -0
  63. package/dist/src/view/bood/motion.d.ts +178 -0
  64. package/dist/src/view/bood/motion.js +298 -0
  65. package/dist/src/view/bood/owl.d.ts +9 -0
  66. package/dist/src/view/bood/owl.js +285 -0
  67. package/dist/src/view/bood/registry.d.ts +36 -0
  68. package/dist/src/view/bood/registry.js +218 -0
  69. package/dist/src/view/bood/render.d.ts +31 -0
  70. package/dist/src/view/bood/render.js +196 -0
  71. package/dist/src/view/bood/sprite.d.ts +18 -0
  72. package/dist/src/view/bood/sprite.js +130 -0
  73. package/dist/src/view/bood/sprout.d.ts +8 -0
  74. package/dist/src/view/bood/sprout.js +121 -0
  75. package/dist/src/view/bood/types.d.ts +158 -0
  76. package/dist/src/view/bood/types.js +57 -0
  77. package/dist/src/view/bubble.d.ts +75 -0
  78. package/dist/src/view/bubble.js +86 -0
  79. package/dist/src/view/changes.d.ts +35 -0
  80. package/dist/src/view/changes.js +22 -0
  81. package/dist/src/view/composer.d.ts +64 -0
  82. package/dist/src/view/composer.js +192 -0
  83. package/dist/src/view/controls.d.ts +44 -0
  84. package/dist/src/view/controls.js +49 -0
  85. package/dist/src/view/creature.d.ts +69 -0
  86. package/dist/src/view/creature.js +83 -0
  87. package/dist/src/view/customizations.d.ts +28 -0
  88. package/dist/src/view/customizations.js +53 -0
  89. package/dist/src/view/details.d.ts +65 -0
  90. package/dist/src/view/details.js +65 -0
  91. package/dist/src/view/filediff.d.ts +29 -0
  92. package/dist/src/view/filediff.js +24 -0
  93. package/dist/src/view/files.d.ts +31 -0
  94. package/dist/src/view/files.js +39 -0
  95. package/dist/src/view/hitl.d.ts +43 -0
  96. package/dist/src/view/hitl.js +171 -0
  97. package/dist/src/view/icons.d.ts +13 -0
  98. package/dist/src/view/icons.js +71 -0
  99. package/dist/src/view/picker.d.ts +42 -0
  100. package/dist/src/view/picker.js +71 -0
  101. package/dist/src/view/sessionhead.d.ts +41 -0
  102. package/dist/src/view/sessionhead.js +60 -0
  103. package/dist/src/view/sessions.d.ts +34 -0
  104. package/dist/src/view/sessions.js +61 -0
  105. package/dist/src/view/terminal.d.ts +30 -0
  106. package/dist/src/view/terminal.js +32 -0
  107. package/dist/src/view/toolcall.d.ts +27 -0
  108. package/dist/src/view/toolcall.js +48 -0
  109. package/dist/src/view/transcript.d.ts +50 -0
  110. package/dist/src/view/transcript.js +60 -0
  111. package/package.json +72 -0
@@ -0,0 +1,43 @@
1
+ /** How the CLI puts things on stdout. */
2
+ import type { SessionSummary } from '../ahp/types.js';
3
+ /**
4
+ * Structured output, when it is asked for.
5
+ *
6
+ * Two spaces, because the reader of `--json` is as often a person scrolling as
7
+ * a program piping - and a program does not mind the whitespace.
8
+ */
9
+ export declare const json: (data: unknown) => void;
10
+ export declare const line: (text?: string) => void;
11
+ /**
12
+ * How long ago, in the units a person would say it in.
13
+ *
14
+ * A timestamp answers "when"; a list is read for "how long since", and the
15
+ * two are not the same question.
16
+ */
17
+ export declare const ago: (iso: string) => string;
18
+ /**
19
+ * The status bitset, as the word and the shape that carry it.
20
+ *
21
+ * Both, deliberately: the glyph is what a list is scanned by and the word is
22
+ * what survives being read aloud, grepped, or piped somewhere colourless.
23
+ */
24
+ export declare const mark: (status: number) => string;
25
+ /**
26
+ * What a host says about a project, or what its path implies.
27
+ *
28
+ * The same fallback the catalogue draws, so a row here and a row there name
29
+ * the same session the same way.
30
+ */
31
+ export declare const project: (session: SessionSummary) => string;
32
+ /** The branch a host reports under `_meta.git`, if it reports one. */
33
+ export declare const branch: (session: SessionSummary) => string;
34
+ /**
35
+ * Rows as columns, sized to what is actually in them.
36
+ *
37
+ * Padding to a fixed width is what turns a list of long paths into a list of
38
+ * ragged ones; the widest cell in a column is the only honest width for it.
39
+ * The last column is never padded, since nothing follows it to line up with.
40
+ */
41
+ export declare const table: (rows: string[][]) => void;
42
+ /** Whether a session was put away, which a listing hides unless asked. */
43
+ export declare const archived: (status: number) => boolean;
@@ -0,0 +1,91 @@
1
+ /** How the CLI puts things on stdout. */
2
+ import { decodeStatus } from '../ahp/status.js';
3
+ /**
4
+ * Structured output, when it is asked for.
5
+ *
6
+ * Two spaces, because the reader of `--json` is as often a person scrolling as
7
+ * a program piping - and a program does not mind the whitespace.
8
+ */
9
+ export const json = (data) => {
10
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
11
+ };
12
+ export const line = (text = '') => { process.stdout.write(`${text}\n`); };
13
+ /**
14
+ * How long ago, in the units a person would say it in.
15
+ *
16
+ * A timestamp answers "when"; a list is read for "how long since", and the
17
+ * two are not the same question.
18
+ */
19
+ export const ago = (iso) => {
20
+ const seconds = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
21
+ if (!Number.isFinite(seconds))
22
+ return '';
23
+ if (seconds < 60)
24
+ return `${seconds}s ago`;
25
+ if (seconds < 3600)
26
+ return `${Math.round(seconds / 60)}m ago`;
27
+ if (seconds < 86400)
28
+ return `${Math.round(seconds / 3600)}h ago`;
29
+ return `${Math.round(seconds / 86400)}d ago`;
30
+ };
31
+ /**
32
+ * The glyph each activity keeps when there is no colour.
33
+ *
34
+ * `decodeStatus` names the shape rather than the character, because the TUI
35
+ * draws it from a theme. A pipe has no theme, so the characters are here.
36
+ */
37
+ const GLYPH = {
38
+ bulletFilled: '●',
39
+ bulletHalf: '◐',
40
+ bulletHollow: '○',
41
+ cross: '✗',
42
+ };
43
+ /**
44
+ * The status bitset, as the word and the shape that carry it.
45
+ *
46
+ * Both, deliberately: the glyph is what a list is scanned by and the word is
47
+ * what survives being read aloud, grepped, or piped somewhere colourless.
48
+ */
49
+ export const mark = (status) => {
50
+ const state = decodeStatus(status);
51
+ return `${GLYPH[state.glyph] ?? '·'} ${state.label.toLowerCase()}`;
52
+ };
53
+ /**
54
+ * What a host says about a project, or what its path implies.
55
+ *
56
+ * The same fallback the catalogue draws, so a row here and a row there name
57
+ * the same session the same way.
58
+ */
59
+ export const project = (session) => session.project?.displayName
60
+ || (session.workingDirectories[0] ?? '').replace(/^file:\/\//, '').split('/').filter(Boolean).pop()
61
+ || '';
62
+ /** The branch a host reports under `_meta.git`, if it reports one. */
63
+ export const branch = (session) => {
64
+ const git = session._meta?.git;
65
+ if (typeof git !== 'object' || git === null)
66
+ return '';
67
+ const found = git.branch;
68
+ return typeof found === 'string' ? found : '';
69
+ };
70
+ /**
71
+ * Rows as columns, sized to what is actually in them.
72
+ *
73
+ * Padding to a fixed width is what turns a list of long paths into a list of
74
+ * ragged ones; the widest cell in a column is the only honest width for it.
75
+ * The last column is never padded, since nothing follows it to line up with.
76
+ */
77
+ export const table = (rows) => {
78
+ if (rows.length === 0)
79
+ return;
80
+ const widths = [];
81
+ for (const row of rows) {
82
+ row.forEach((cell, index) => {
83
+ widths[index] = Math.max(widths[index] ?? 0, cell.length);
84
+ });
85
+ }
86
+ for (const row of rows) {
87
+ line(row.map((cell, index) => (index === row.length - 1 ? cell : cell.padEnd(widths[index] ?? 0))).join(' ').trimEnd());
88
+ }
89
+ };
90
+ /** Whether a session was put away, which a listing hides unless asked. */
91
+ export const archived = (status) => decodeStatus(status).archived;
@@ -0,0 +1,63 @@
1
+ /** What this client was told before anybody typed a flag. */
2
+ /** What a config file may say. Every key is what a flag would have said. */
3
+ export interface Config {
4
+ /** A live agent host, `ws://host:port`. */
5
+ host?: string;
6
+ /** A bearer token for it. */
7
+ token?: string;
8
+ /** The theme to open on. */
9
+ theme?: string;
10
+ /** The shell layout. */
11
+ shell?: string;
12
+ /**
13
+ * Keys, over the ones this client ships with.
14
+ *
15
+ * A chord to a command id - `"ctrl+g": "editor.open"` - or to `null`, which
16
+ * takes the chord away and binds nothing. Naming a chord replaces every
17
+ * default on it, including one registered against a single screen, so a
18
+ * chord is either yours or ours and never half of each.
19
+ *
20
+ * `ahpc config` lists the command ids. A chord bound to a name no command
21
+ * answers to is reported at startup rather than dropped, because a binding
22
+ * that silently does nothing is indistinguishable from one that never
23
+ * loaded.
24
+ */
25
+ keys?: Record<string, string | null>;
26
+ /**
27
+ * Trade the header's own name for a seven-cell creature, on an open session.
28
+ *
29
+ * Off unless it is asked for. The header's leftmost cell is the one part of
30
+ * the row that is the same on every screen, and a client that gives it away
31
+ * by default has decided something about itself on somebody else's behalf.
32
+ */
33
+ boodInline?: boolean;
34
+ /**
35
+ * Let the creature loose on the whole application.
36
+ *
37
+ * Off unless it is asked for, and the reason is the one thing a terminal
38
+ * cannot do: there is no per-cell transparency, so a figure that goes
39
+ * everywhere writes over whatever it is standing on. It keeps clear of the
40
+ * composer, which says how tall it is, and of anything opened on a layer -
41
+ * but a screen whose content runs to the bottom will have a cat on it.
42
+ */
43
+ boodFloat?: boolean;
44
+ }
45
+ /**
46
+ * Where a tool's configuration lives.
47
+ *
48
+ * XDG, and the environment variable first: `$XDG_CONFIG_HOME` is what a person
49
+ * sets when their configuration is not in `~/.config`, and a tool that reads
50
+ * the fallback anyway is a tool that ignores them.
51
+ */
52
+ export declare const configHome: () => string;
53
+ /** This tool's own file. */
54
+ export declare const configPath: (tool: string) => string;
55
+ /**
56
+ * Read it, or answer that there was nothing to read.
57
+ *
58
+ * A file that is not there is not an error - most people have none. A file
59
+ * that is there and is broken *is* one, and says so rather than starting with
60
+ * defaults somebody did not choose: silently ignoring a config somebody wrote
61
+ * is worse than refusing to start.
62
+ */
63
+ export declare function loadConfig(tool: string, named?: string): Config;
@@ -0,0 +1,45 @@
1
+ /** What this client was told before anybody typed a flag. */
2
+ import { readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ /**
6
+ * Where a tool's configuration lives.
7
+ *
8
+ * XDG, and the environment variable first: `$XDG_CONFIG_HOME` is what a person
9
+ * sets when their configuration is not in `~/.config`, and a tool that reads
10
+ * the fallback anyway is a tool that ignores them.
11
+ */
12
+ export const configHome = () => process.env.XDG_CONFIG_HOME || join(homedir(), '.config');
13
+ /** This tool's own file. */
14
+ export const configPath = (tool) => join(configHome(), tool, 'config.json');
15
+ /**
16
+ * Read it, or answer that there was nothing to read.
17
+ *
18
+ * A file that is not there is not an error - most people have none. A file
19
+ * that is there and is broken *is* one, and says so rather than starting with
20
+ * defaults somebody did not choose: silently ignoring a config somebody wrote
21
+ * is worse than refusing to start.
22
+ */
23
+ export function loadConfig(tool, named) {
24
+ const path = named ?? configPath(tool);
25
+ let text;
26
+ try {
27
+ text = readFileSync(path, 'utf8');
28
+ }
29
+ catch {
30
+ // Only a file that was *asked for* by name is worth complaining about.
31
+ if (named === undefined)
32
+ return {};
33
+ throw new Error(`No configuration at ${named}`);
34
+ }
35
+ try {
36
+ const found = JSON.parse(text);
37
+ if (typeof found !== 'object' || found === null || Array.isArray(found)) {
38
+ throw new Error('it is not an object');
39
+ }
40
+ return found;
41
+ }
42
+ catch (error) {
43
+ throw new Error(`${path} could not be read: ${error instanceof Error ? error.message : String(error)}`);
44
+ }
45
+ }
@@ -0,0 +1,53 @@
1
+ /** Which host this run talks to, for either front end. */
2
+ import type { HostConnection } from './ahp/connection.js';
3
+ /**
4
+ * Enough of the options to choose a host, and nothing about drawing one.
5
+ *
6
+ * The CLI and the TUI ask the same question and must get the same answer, so
7
+ * the choice lives here rather than in either of them. Nothing in this file
8
+ * imports a renderer, which is what lets `ahpc session list` connect a socket
9
+ * without loading a screen.
10
+ */
11
+ export interface Where {
12
+ /** A live agent host, `ws://host:port`. */
13
+ host?: string;
14
+ /** A bearer token for it. */
15
+ token?: string;
16
+ /** Where the agent works - a path on the *host*, not on this machine. */
17
+ path?: string;
18
+ /**
19
+ * A directory on *this* machine to serve back, under `virtual://<clientId>/`.
20
+ *
21
+ * The authority is this connection's own id, because that is what a host
22
+ * routes on: it reads the authority out of the URI and matches it against
23
+ * the connection that published it.
24
+ *
25
+ * AHP is symmetrical and a host may read from a client-published URI. Absent
26
+ * means nothing is served and every such request is refused, which is the
27
+ * default because publishing by accident is worse than not publishing.
28
+ */
29
+ publish?: string;
30
+ /** Whether the published directory may be written to. Read-only otherwise. */
31
+ publishWritable?: boolean;
32
+ }
33
+ /**
34
+ * Where a refusal goes before there is an application to put it in.
35
+ *
36
+ * The host is built first - it has to be, the application is registered
37
+ * against it - so its callbacks are given a box to write into and the box is
38
+ * filled once there is a store. Until then a refusal goes to stderr, which is
39
+ * where a connection that fails during the handshake belongs anyway, and where
40
+ * it stays for the whole of a CLI run.
41
+ */
42
+ export declare const sink: {
43
+ report(message: string): void;
44
+ };
45
+ /**
46
+ * The host this run talks to.
47
+ *
48
+ * The one place the choice is made, and the only place either implementation
49
+ * is named. A live connection is asked for by URL; anything else is the script.
50
+ */
51
+ export declare function connect(options: Where): Promise<HostConnection & {
52
+ pump?(): boolean;
53
+ }>;
@@ -0,0 +1,69 @@
1
+ /** Which host this run talks to, for either front end. */
2
+ import { MissingProtocolPackage, liveHost } from './ahp/live.js';
3
+ import { fakeHost } from './ahp/fake.js';
4
+ import { publish } from './ahp/publish.js';
5
+ /**
6
+ * Where a refusal goes before there is an application to put it in.
7
+ *
8
+ * The host is built first - it has to be, the application is registered
9
+ * against it - so its callbacks are given a box to write into and the box is
10
+ * filled once there is a store. Until then a refusal goes to stderr, which is
11
+ * where a connection that fails during the handshake belongs anyway, and where
12
+ * it stays for the whole of a CLI run.
13
+ */
14
+ export const sink = {
15
+ report: (message) => process.stderr.write(`${message}\n`),
16
+ };
17
+ /**
18
+ * The host this run talks to.
19
+ *
20
+ * The one place the choice is made, and the only place either implementation
21
+ * is named. A live connection is asked for by URL; anything else is the script.
22
+ */
23
+ export async function connect(options) {
24
+ if (!options.host)
25
+ return fakeHost();
26
+ try {
27
+ return await liveHost({
28
+ url: options.host,
29
+ ...(options.token ? { token: options.token } : {}),
30
+ onRefusal: (_uri, message) => sink.report(message),
31
+ onLimit: (message) => sink.report(message),
32
+ // Work the host is doing under a token of its own. Reported while it
33
+ // runs and not on the frame that closes it - the finish is the thing
34
+ // that happened, and it is what the screen shows next.
35
+ onProgress: (_token, message) => { if (message !== null)
36
+ sink.report(message); },
37
+ /*
38
+ * The host wants signing into something.
39
+ *
40
+ * Said in the words a person can act on, which means naming the
41
+ * resource and the variable that would satisfy it. `expired` is called
42
+ * out because the answer is different: a new credential, not the one
43
+ * that was just refused.
44
+ */
45
+ onAuthRequired: (resources, why) => {
46
+ const names = resources.map((one) => one.resource).join(', ');
47
+ sink.report(why === 'expired'
48
+ ? `The token for ${names} has expired. Sign in again: ahpc auth ${resources[0]?.resource ?? ''}`
49
+ : `${names} needs signing in to: ahpc auth ${resources[0]?.resource ?? ''}`);
50
+ },
51
+ // What this client serves back. Nothing unless a directory was named:
52
+ // the protocol is symmetrical, and a client that published by default
53
+ // would be one that hands its disk to any host it connects to.
54
+ ...(options.publish !== undefined
55
+ ? { publish: publish({ root: options.publish, ...(options.publishWritable ? { writable: true } : {}) }) }
56
+ : {}),
57
+ onState: (state) => { if (state === 'offline')
58
+ sink.report('The host stopped answering'); },
59
+ });
60
+ }
61
+ catch (error) {
62
+ if (error instanceof MissingProtocolPackage) {
63
+ process.stderr.write(`${error.message}\n`);
64
+ process.exit(1);
65
+ }
66
+ process.stderr.write(`Could not reach ${options.host}: ${String(error)}\n`);
67
+ process.exit(1);
68
+ }
69
+ }
@@ -0,0 +1,189 @@
1
+ import type { Disposable, ServiceKey, TextUIApp } from '@textui/core';
2
+ import type { HostConnection } from './ahp/connection.js';
3
+ import type { Terminals } from './terminal.js';
4
+ import type { Agent, Answer, Automation, Changeset, ChangesetScope, ChatSource, Completion, ConfigProperty, ContentRef, Customization, FileContent, ResourceEntry, SessionConfig, SessionDetail, SessionUri } from './ahp/types.js';
5
+ /**
6
+ * The control side: what the application can do, and what it does when the
7
+ * host says something.
8
+ *
9
+ * Split from the views because these are the two things that change for
10
+ * different reasons. A new screen is a rendering change; answering a new kind
11
+ * of request is a change here. Keeping them in one file is what makes a small
12
+ * protocol change touch every component that draws a bubble.
13
+ *
14
+ * Everything a key can do is a command, so the palette gets it for free and
15
+ * nothing is reachable one way only.
16
+ */
17
+ export interface Controller {
18
+ refresh(): Promise<void>;
19
+ open(uri: SessionUri): void;
20
+ close(): void;
21
+ /** Send, or queue when a turn is already running. */
22
+ send(text: string): void;
23
+ /** Take a queued message back, while it is still waiting. */
24
+ unqueue(id: string): void;
25
+ stop(): void;
26
+ approve(optionId?: string): void;
27
+ deny(): void;
28
+ answer(answers: Record<string, Answer>, accepted?: boolean): void;
29
+ setArchived(uri: SessionUri, archived: boolean): void;
30
+ /** Put the bold back, or take it away. A client flag, not activity. */
31
+ setRead(uri: SessionUri, read: boolean): void;
32
+ /** Say that the host refused something, wherever it was noticed. */
33
+ report(error: unknown): void;
34
+ /**
35
+ * End the session on the host.
36
+ *
37
+ * Not `dispose`. This object is also a `Disposable` - it owns the
38
+ * subscription, the commands and the keys - and `Object.assign` put that
39
+ * `dispose()` straight over the top of this one. Confirming "delete this
40
+ * session" therefore tore down the whole controller instead: every command
41
+ * and every keybinding went with it, and the application stopped answering
42
+ * the keyboard entirely. The protocol's own name for this is
43
+ * `disposeSession`, and using it means the two can never collide again.
44
+ */
45
+ disposeSession(uri: SessionUri): Promise<void>;
46
+ create(options: {
47
+ provider: string;
48
+ workingDirectory?: string;
49
+ first?: string;
50
+ }): Promise<SessionUri>;
51
+ /** The harnesses this host advertises, and the models each offers. */
52
+ agents(): Promise<Agent[]>;
53
+ /** The session channel's own state: its chat, its lifecycle, its settings. */
54
+ detail(uri: SessionUri): Promise<SessionDetail>;
55
+ config(uri: SessionUri): Promise<SessionConfig>;
56
+ /** Register a command per question the chosen model asks about itself. */
57
+ offerModel(properties: ConfigProperty[]): void;
58
+ /** Put what is being typed where other clients on this chat can see it. */
59
+ draft(text: string): void;
60
+ /** What the host handed this session: plugins, skills, MCP servers. */
61
+ customizations(uri: SessionUri): Promise<Customization[]>;
62
+ /**
63
+ * Terminals, as operations.
64
+ *
65
+ * Its own object because a terminal is not a session and shares nothing with
66
+ * one: it belongs to the host, outlives any turn, and what a screen needs
67
+ * from it is a different set of verbs.
68
+ */
69
+ readonly terminals: Terminals;
70
+ /** What the host offers to complete what is being typed. */
71
+ completions(channel: string, text: string, offset?: number): Promise<Completion[]>;
72
+ /** Read a different chat in the session already open. */
73
+ openChat(chat: string): void;
74
+ /** Open another chat in it, and read that. */
75
+ createChat(first?: string, source?: ChatSource): Promise<void>;
76
+ /** Close one, and read whatever is left. */
77
+ disposeChat(chat: string): Promise<void>;
78
+ /** What a slash offers before any session exists. */
79
+ harnessCommands(): Promise<Customization[]>;
80
+ /** Turn one on or off. The host decides and tells everyone watching. */
81
+ setCustomizationEnabled(uri: SessionUri, id: string, enabled: boolean): void;
82
+ /**
83
+ * One directory of the host's filesystem.
84
+ *
85
+ * The *host's*, which is the whole reason this goes through the connection
86
+ * rather than through `node:fs`: the daemon may be on another machine, and
87
+ * the project a session is working in is over there.
88
+ *
89
+ * Empty for a host that serves none - `createHost` takes its filesystem as a
90
+ * port and one given none answers `-32601`, which a client reads as nothing
91
+ * to browse rather than as a failure.
92
+ */
93
+ files(uri: string): Promise<ResourceEntry[]>;
94
+ /** Watch a directory, where the host serves watches. Releasing closes it. */
95
+ watchFiles(uri: string, changed: () => void): Promise<{
96
+ close(): void;
97
+ }>;
98
+ /**
99
+ * Every automation the host holds.
100
+ *
101
+ * Rejects, rather than answering empty, for a host that serves none: an
102
+ * empty list is a host with a clock and nothing on it, and the two want
103
+ * different words on the screen.
104
+ */
105
+ automations(): Promise<Automation[]>;
106
+ /** Told when one moves, including one that fired while nobody was looking. */
107
+ onAutomations(observer: () => void): {
108
+ close(): void;
109
+ };
110
+ /** Write a new one, and answer with the URI the host gave it. */
111
+ createAutomation(definition: Record<string, unknown>): Promise<string>;
112
+ /** Start one now, whatever its schedule says. */
113
+ runAutomation(uri: string): Promise<void>;
114
+ /** Switch one on or off. */
115
+ setAutomationEnabled(uri: string, enabled: boolean): Promise<void>;
116
+ /** Forget one. */
117
+ removeAutomation(uri: string): Promise<void>;
118
+ /** One file's bytes, by URI on the host. */
119
+ file(uri: string): Promise<{
120
+ data: string;
121
+ encoding: string;
122
+ contentType?: string;
123
+ }>;
124
+ /**
125
+ * Which changesets this session offers.
126
+ *
127
+ * Asked once per session rather than derived: a host that advertises none
128
+ * has none, and a client that assumed the four the protocol names would
129
+ * offer a picker full of things to be refused.
130
+ */
131
+ changesets(uri: SessionUri): Promise<ChangesetScope[]>;
132
+ /** One of them, by the URI its template became. Absent is whichever the host would pick. */
133
+ changesAt(uri: SessionUri, changeset?: string): Promise<Changeset>;
134
+ /**
135
+ * Tick a file off, or take the tick back.
136
+ *
137
+ * Fire-and-forget: the host keeps the flag and tells every client watching,
138
+ * so what redraws this screen is the changeset coming back, not this call.
139
+ */
140
+ review(changeset: string, files: string[], reviewed: boolean): void;
141
+ /** One file out of a changeset, fetched. Nothing calls it until a row opens. */
142
+ content(ref: ContentRef): Promise<FileContent>;
143
+ /**
144
+ * Pull the page of history before the turns on screen.
145
+ *
146
+ * Answers whether there is still more behind it, so a transcript that has
147
+ * reached the beginning stops asking.
148
+ */
149
+ loadOlderTurns(uri: SessionUri): Promise<boolean>;
150
+ /**
151
+ * What the host will answer questions about, here and now.
152
+ *
153
+ * One call, because "here and now" has two answers and the caller should not
154
+ * have to know which: an open session describes itself, and a session that
155
+ * does not exist yet is what `resolveSessionConfig` is for. Asking also
156
+ * *registers* a command per property the schema offers, so the control row
157
+ * and the palette are showing the host's own questions rather than the ones
158
+ * this client was written knowing about.
159
+ */
160
+ settings(): Promise<SessionConfig>;
161
+ setConfig(uri: SessionUri, key: string, value: string): void;
162
+ /** Drive the scripted host. A real connection has a socket instead. */
163
+ pump(): boolean;
164
+ }
165
+ export declare const CONTROLLER: ServiceKey<Controller>;
166
+ export declare const settingCommand: (key: string) => string;
167
+ /** One per property of the chosen model's own schema. */
168
+ export declare const modelCommand: (key: string) => string;
169
+ /**
170
+ * The two focus scopes, and why single-letter keys need them.
171
+ *
172
+ * The runtime already offers a key to the focused node before any keybinding,
173
+ * so `q` typed into the composer is a letter and not a quit. What a scope adds
174
+ * is *where a key exists at all*: `d` disposes a session on the catalogue and
175
+ * means nothing in a conversation, and a binding that exists on both screens
176
+ * is one that fires on the wrong one.
177
+ *
178
+ * A scope is active while the component that declares it is mounted, so the
179
+ * screens are what turn these on and off.
180
+ */
181
+ export declare const SESSIONS_SCOPE = "chat.sessions";
182
+ export declare const CHAT_SCOPE = "chat.conversation";
183
+ export declare const SKILLS_SCOPE = "chat.skills";
184
+ export declare const MCP_SCOPE = "chat.mcp";
185
+ export declare const CHANGES_SCOPE = "chat.changes";
186
+ export declare const AUTOMATIONS_SCOPE = "chat.automations";
187
+ export declare function createController(app: TextUIApp, host: HostConnection & {
188
+ pump?(): boolean;
189
+ }, bindings?: Record<string, string | null>): Controller & Disposable;