@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,69 @@
1
+ /**
2
+ * A small drawn thing, above the invitation to say something.
3
+ *
4
+ * An empty screen is the one place a client can afford a figure, and a
5
+ * different one each time is the difference between an application that has a
6
+ * mascot and one that has a habit.
7
+ *
8
+ * The drawings moved to [`bood/`](./bood/index.ts) - one creature per file,
9
+ * registered by name, in three sizes and five moods. This file is the door
10
+ * they were already behind, and it stays: `<Creature mood="happy" />` on the
11
+ * empty screen means what it always meant.
12
+ *
13
+ * Two rules, and both of them came from watching art break in a terminal.
14
+ *
15
+ * Plain ASCII, every character. Not one glyph is one whose width the terminal
16
+ * gets to decide - box drawing and the half-block set are the ones that get
17
+ * eaten by a CJK font setting, and art that is one cell wider on somebody
18
+ * else's machine does not look narrow, it looks broken. `registerCreature`
19
+ * checks this rather than trusting it.
20
+ *
21
+ * No shared frame, no shared face window, no shared size at full size. The
22
+ * figures run from seven cells wide to seventeen and the outline *is* the
23
+ * animal. A creature that fits a template is a template wearing a hat - which
24
+ * is exactly what `block` and `inline` are, and why they are drawn separately
25
+ * instead of being the big one shrunk.
26
+ */
27
+ import type { Mood } from './bood/index.js';
28
+ import type { Activity } from '../ahp/status.js';
29
+ export { BoodSprite, Creature, MOODS, FORMS, creatureFrames, creatureMotion, creatureSize, drawCreature, livelyNames, } from './bood/index.js';
30
+ export type { CreatureProps, Form, Mood } from './bood/index.js';
31
+ /**
32
+ * What the figure is doing, from what the session is doing.
33
+ *
34
+ * The moods were always meant to be read off real state rather than chosen, and
35
+ * this is the only place the two vocabularies meet: five drawn moods, four
36
+ * activities the protocol actually reports. `sad` has no source and is drawn
37
+ * anyway, because a mood with nothing to trigger it is a drawing waiting for a
38
+ * state rather than a bug - and inventing a session state to justify it would
39
+ * be the tail wagging the dog.
40
+ *
41
+ * `input` is thinking rather than happy: a session waiting on a person has
42
+ * stopped, and the figure standing still and looking at you is the reading
43
+ * that matches the status row above it.
44
+ */
45
+ export declare function moodOf(activity: Activity): Mood;
46
+ /**
47
+ * Say which row this component starts at, so nothing stands on top of it.
48
+ *
49
+ * Its top row rather than its height: the row is where the thing actually is,
50
+ * and a height has to be added to a guess about everything below it to mean
51
+ * anything. Published rather than assumed because it moves - the composer
52
+ * grows a slash menu upward and grows again with a wrapped draft, and the
53
+ * block that asks about a tool is only there while something is waiting.
54
+ * Cleared on the way out, or a screen that had one would keep making room for
55
+ * it after it had gone.
56
+ */
57
+ export declare function useFloorTop(key: string, row: number): void;
58
+ /**
59
+ * One animal per run, rather than one per component that draws one.
60
+ *
61
+ * Picked from the ones drawn moving, because the catalogue's is going to walk
62
+ * and the header's is the same creature seen at seven cells. Two screens
63
+ * showing two different animals would read as two mascots.
64
+ */
65
+ export declare function pickBood(): string;
66
+ /** The names, in roster order. */
67
+ export declare const CREATURES: readonly string[];
68
+ /** How tall the tallest of them is, for anyone deciding whether there is room. */
69
+ export declare const CREATURE_HEIGHT: number;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * A small drawn thing, above the invitation to say something.
3
+ *
4
+ * An empty screen is the one place a client can afford a figure, and a
5
+ * different one each time is the difference between an application that has a
6
+ * mascot and one that has a habit.
7
+ *
8
+ * The drawings moved to [`bood/`](./bood/index.ts) - one creature per file,
9
+ * registered by name, in three sizes and five moods. This file is the door
10
+ * they were already behind, and it stays: `<Creature mood="happy" />` on the
11
+ * empty screen means what it always meant.
12
+ *
13
+ * Two rules, and both of them came from watching art break in a terminal.
14
+ *
15
+ * Plain ASCII, every character. Not one glyph is one whose width the terminal
16
+ * gets to decide - box drawing and the half-block set are the ones that get
17
+ * eaten by a CJK font setting, and art that is one cell wider on somebody
18
+ * else's machine does not look narrow, it looks broken. `registerCreature`
19
+ * checks this rather than trusting it.
20
+ *
21
+ * No shared frame, no shared face window, no shared size at full size. The
22
+ * figures run from seven cells wide to seventeen and the outline *is* the
23
+ * animal. A creature that fits a template is a template wearing a hat - which
24
+ * is exactly what `block` and `inline` are, and why they are drawn separately
25
+ * instead of being the big one shrunk.
26
+ */
27
+ import { BOOD, boodHeight, livelyNames } from './bood/index.js';
28
+ import { useApp, useEffect } from '@textui/core';
29
+ import { boodFloorFor } from '../state.js';
30
+ export { BoodSprite, Creature, MOODS, FORMS, creatureFrames, creatureMotion, creatureSize, drawCreature, livelyNames, } from './bood/index.js';
31
+ /**
32
+ * What the figure is doing, from what the session is doing.
33
+ *
34
+ * The moods were always meant to be read off real state rather than chosen, and
35
+ * this is the only place the two vocabularies meet: five drawn moods, four
36
+ * activities the protocol actually reports. `sad` has no source and is drawn
37
+ * anyway, because a mood with nothing to trigger it is a drawing waiting for a
38
+ * state rather than a bug - and inventing a session state to justify it would
39
+ * be the tail wagging the dog.
40
+ *
41
+ * `input` is thinking rather than happy: a session waiting on a person has
42
+ * stopped, and the figure standing still and looking at you is the reading
43
+ * that matches the status row above it.
44
+ */
45
+ export function moodOf(activity) {
46
+ return activity === 'running' ? 'executing'
47
+ : activity === 'error' ? 'error'
48
+ : activity === 'input' ? 'thinking'
49
+ : 'happy';
50
+ }
51
+ /**
52
+ * Say which row this component starts at, so nothing stands on top of it.
53
+ *
54
+ * Its top row rather than its height: the row is where the thing actually is,
55
+ * and a height has to be added to a guess about everything below it to mean
56
+ * anything. Published rather than assumed because it moves - the composer
57
+ * grows a slash menu upward and grows again with a wrapped draft, and the
58
+ * block that asks about a tool is only there while something is waiting.
59
+ * Cleared on the way out, or a screen that had one would keep making room for
60
+ * it after it had gone.
61
+ */
62
+ export function useFloorTop(key, row) {
63
+ const app = useApp();
64
+ useEffect(() => {
65
+ app.store.set(boodFloorFor(key), row);
66
+ return () => app.store.set(boodFloorFor(key), 0);
67
+ }, [key, row]);
68
+ }
69
+ /**
70
+ * One animal per run, rather than one per component that draws one.
71
+ *
72
+ * Picked from the ones drawn moving, because the catalogue's is going to walk
73
+ * and the header's is the same creature seen at seven cells. Two screens
74
+ * showing two different animals would read as two mascots.
75
+ */
76
+ export function pickBood() {
77
+ const names = livelyNames();
78
+ return names[Math.floor(Math.random() * names.length)];
79
+ }
80
+ /** The names, in roster order. */
81
+ export const CREATURES = BOOD.map((creature) => creature.name);
82
+ /** How tall the tallest of them is, for anyone deciding whether there is room. */
83
+ export const CREATURE_HEIGHT = boodHeight('draw');
@@ -0,0 +1,28 @@
1
+ import type { BoxProps, RenderOutput } from '@textui/core';
2
+ import type { Customization } from '../ahp/types.js';
3
+ /**
4
+ * What the host handed this session, as a list.
5
+ *
6
+ * One component for both panels, because a skill and an MCP server differ in
7
+ * what there is to say about them and not in how they are read: a name, where
8
+ * it came from, whether it is on, and - when the host has something to say
9
+ * about why it is not working - what it said. Two components would be the same
10
+ * two-line row twice with a different word in the third column.
11
+ *
12
+ * The list is flat and the container is the `from` column rather than a parent
13
+ * row, which is the arrangement the protocol's nesting does not survive
14
+ * anyway: an MCP server can be contributed by a plugin *or* by the host
15
+ * directly, and a tree would have to draw the second at a depth that means
16
+ * nothing. Sorting is the caller's.
17
+ */
18
+ export interface CustomizationListProps extends BoxProps {
19
+ items: Customization[];
20
+ /** Enter on a row. Absent leaves the list read-only. */
21
+ onToggle?(item: Customization): void;
22
+ onSelect?(id: string): void;
23
+ selectedId?: string | null;
24
+ emptyMessage?: string;
25
+ focusId?: string;
26
+ autoFocus?: boolean;
27
+ }
28
+ export declare const CustomizationList: (props: CustomizationListProps) => RenderOutput;
@@ -0,0 +1,53 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, stringWidth, useTheme } from '@textui/core';
3
+ import { Column, List, Marquee, Row } from '@textui/widgets';
4
+ /** What an MCP server's state means, in a word and a colour. */
5
+ const MCP = {
6
+ starting: { label: 'starting', tone: 'warning' },
7
+ ready: { label: 'ready', tone: 'success' },
8
+ // Not an error: the server is reachable and nobody has signed in, which is
9
+ // something a person can go and fix rather than something that broke.
10
+ authRequired: { label: 'sign in', tone: 'warning' },
11
+ error: { label: 'failed', tone: 'danger' },
12
+ stopped: { label: 'stopped', tone: 'muted' },
13
+ };
14
+ export const CustomizationList = defineComponent('CustomizationList', (props) => {
15
+ const { items, onToggle, onSelect, selectedId, emptyMessage, focusId, autoFocus, ...rest } = props;
16
+ const theme = useTheme();
17
+ const dot = ` ${theme.glyphs.separator} `;
18
+ const byId = new Map(items.map((item) => [item.id, item]));
19
+ const rows = items.map((item) => {
20
+ const mcp = item.state ? MCP[item.state] : null;
21
+ // The switch first, so the answer to "is this on" is in the same column
22
+ // on every row and findable without reading any of them.
23
+ const icon = item.enabled ? theme.glyphs.checkboxOn : theme.glyphs.checkboxOff;
24
+ return {
25
+ id: item.id,
26
+ icon,
27
+ label: item.name,
28
+ description: [
29
+ item.from ?? '',
30
+ item.description ?? '',
31
+ // Last, because it is the one that is usually not there and the one
32
+ // worth reading when it is.
33
+ item.problem ?? '',
34
+ ].filter(Boolean).join(dot),
35
+ meta: mcp?.label ?? (item.enabled ? '' : 'off'),
36
+ tone: (item.problem && !mcp
37
+ ? 'danger'
38
+ : mcp?.tone ?? (item.enabled ? 'muted' : 'subtle')),
39
+ };
40
+ });
41
+ return (_jsx(List, { items: rows, itemHeight: 2, renderItem: (row, state) => {
42
+ const item = byId.get(row.id);
43
+ return (_jsxs(Column, { children: [_jsxs(Row, { gap: 1, children: [_jsx("text", { content: row.icon ?? '', ...(state.selected ? {} : { fg: item?.enabled ? 'success' : 'subtle' }), shrink: 0 }), _jsx(Marquee, { content: row.label, active: state.selected && state.focused, flex: 1 }), _jsx("text", { content: row.meta ?? '', ...(state.selected ? {} : { fg: row.tone }), shrink: 0 })] }), _jsxs(Row, { children: [_jsx("text", { content: ' '.repeat(stringWidth(row.icon ?? '') + 1), shrink: 0 }), _jsx(Marquee, { content: row.description ?? '', active: state.selected && state.focused, ...(state.selected ? {} : { fg: 'muted' }), flex: 1 })] })] }));
44
+ }, ...(selectedId ? { selectedId } : {}), emptyMessage: emptyMessage ?? 'The host contributed none', ...(onSelect ? { onSelect } : {}), ...(onToggle
45
+ ? {
46
+ onActivate: (id) => {
47
+ const item = byId.get(id);
48
+ if (item)
49
+ onToggle(item);
50
+ },
51
+ }
52
+ : {}), ...(focusId ? { focusId } : {}), ...(autoFocus ? { autoFocus: true } : {}), ...rest }));
53
+ });
@@ -0,0 +1,65 @@
1
+ import type { BoxProps, RenderOutput, SemanticVariant } from '@textui/core';
2
+ /**
3
+ * A property list you can walk, and take a value out of.
4
+ *
5
+ * The catalogue's detail pane is where the identifiers live - a session URI, a
6
+ * chat URI - and an identifier you cannot read in full or paste anywhere is
7
+ * decoration. `KeyValue` draws the same pairs and is static: nothing selects a
8
+ * row, so nothing can be copied and nothing can be shown untruncated.
9
+ *
10
+ * So the selected row is the one that gets the room. Every other row is one
11
+ * line with its value truncated, and the selected row wraps its value across
12
+ * as many lines as it needs - which costs nothing when the value is short and
13
+ * is the whole answer when it is a URI in a 36-column pane. `enter` puts it on
14
+ * the clipboard.
15
+ *
16
+ * This is a finding, not a flourish: it is the third component this example
17
+ * wanted that the catalog does not have.
18
+ */
19
+ export interface DetailField {
20
+ id: string;
21
+ label: string;
22
+ value: string;
23
+ tone?: SemanticVariant;
24
+ /** Shown instead of the value when there is none, in the subtle tone. */
25
+ absent?: string;
26
+ }
27
+ export interface SessionDetailsProps extends BoxProps {
28
+ fields: DetailField[];
29
+ focusId?: string;
30
+ /**
31
+ * Width of the label column, where a caller wants to fix it.
32
+ *
33
+ * Left off, it is the widest label there is. It used to be a constant on
34
+ * the grounds that the labels were ours, and they are not: a session's
35
+ * settings and a model's options are named by whichever host is answering,
36
+ * in words this client does not choose, and the constant was one character
37
+ * wider than the longest label anybody had thought of.
38
+ */
39
+ labelWidth?: number;
40
+ /**
41
+ * Take the keyboard on the frame this mounts, out of whatever holds it.
42
+ *
43
+ * Not `autoFocus`, which claims focus rather than taking it: a pane that
44
+ * appears *because* a key asked for it has to end up with the cursor, and
45
+ * the thing it is taking the cursor from - the session list - is in the
46
+ * same focus scope and already has it.
47
+ *
48
+ * It has to be done here rather than by the screen that mounts this,
49
+ * because a focusable registers in its own effect: on the render that opens
50
+ * the pane, the id does not exist yet and the screen's `focus()` is a call
51
+ * that quietly returns false.
52
+ */
53
+ claim?: boolean;
54
+ /**
55
+ * Which rows show their value whole.
56
+ *
57
+ * `selected` is the default and the one that keeps the pane a list: every
58
+ * row is one line, and the row you have stopped on wraps to as many as its
59
+ * value needs. `all` wraps every row, which is what you want when the
60
+ * values *are* the point - a pane of URIs where the answer is on the third
61
+ * one down and reading it should not mean walking there first.
62
+ */
63
+ values?: 'selected' | 'all';
64
+ }
65
+ export declare const SessionDetails: (props: SessionDetailsProps) => RenderOutput;
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, useClipboard, useEffect, useFocus, useInput, useState, useTheme, } from '@textui/core';
3
+ import { Column, Row } from '@textui/widgets';
4
+ export const SessionDetails = defineComponent('SessionDetails', (props) => {
5
+ const { fields, focusId, labelWidth, claim, values = 'selected', ...rest } = props;
6
+ // Floored at the eleven this used to be, so nothing that already fitted
7
+ // moves, and capped so one verbose title cannot take the pane from the
8
+ // values it is there to label.
9
+ const column = labelWidth ?? Math.min(20, Math.max(11, ...fields.map((field) => field.label.length)));
10
+ const theme = useTheme();
11
+ const clipboard = useClipboard();
12
+ const focus = useFocus({ ...(focusId ? { id: focusId } : {}) });
13
+ const [index, setIndex] = useState(0);
14
+ const [copied, setCopied] = useState(null);
15
+ // The selection is an index into a list that changes with the selected
16
+ // session, so it is clamped on the way out rather than reset on the way in.
17
+ const at = Math.max(0, Math.min(index, fields.length - 1));
18
+ // After the registration above, which is the whole point of it being here.
19
+ useEffect(() => { if (claim)
20
+ focus.focus(); }, [claim]);
21
+ useInput((event) => {
22
+ if (fields.length === 0)
23
+ return false;
24
+ switch (event.name) {
25
+ case 'up':
26
+ setIndex(Math.max(0, at - 1));
27
+ setCopied(null);
28
+ return true;
29
+ case 'down':
30
+ setIndex(Math.min(fields.length - 1, at + 1));
31
+ setCopied(null);
32
+ return true;
33
+ case 'home':
34
+ setIndex(0);
35
+ setCopied(null);
36
+ return true;
37
+ case 'end':
38
+ setIndex(fields.length - 1);
39
+ setCopied(null);
40
+ return true;
41
+ case 'enter': {
42
+ const field = fields[at];
43
+ if (!field || !field.value)
44
+ return true;
45
+ // OSC 52 where the terminal takes it, and the store either way - so
46
+ // a test can assert what was copied without a terminal at all.
47
+ clipboard.write(field.value);
48
+ setCopied(field.id);
49
+ return true;
50
+ }
51
+ default: return false;
52
+ }
53
+ }, { focusId: focus.id });
54
+ return (_jsx(Column, { ...rest, id: focus.id, children: fields.map((field, i) => {
55
+ const active = i === at;
56
+ const selected = active && focus.focused;
57
+ const whole = values === 'all' || active;
58
+ return (
59
+ // `align="start"` because a wrapped value makes the row two lines
60
+ // tall and the label is one: centred, it drifted down to sit
61
+ // beside the *second* line of a URI, with nothing at all beside
62
+ // the first. A label names the row it starts.
63
+ _jsxs(Row, { gap: 1, align: "start", children: [_jsx("text", { content: active ? theme.glyphs.chevronRight : ' ', fg: selected ? 'accent' : 'subtle', shrink: 0 }), _jsx("text", { content: field.label, width: column, shrink: 0, fg: "muted", truncate: "end" }), _jsx("text", { content: field.value || field.absent || '-', flex: 1, ...(whole ? { wrap: 'word' } : { truncate: 'end' }), ...(field.value ? {} : { fg: 'subtle' }), ...(field.tone && field.value ? { fg: field.tone } : {}), ...(selected ? { bold: true } : {}) }), copied === field.id ? _jsx("text", { content: "copied", fg: "success", shrink: 0 }) : null] }, field.id));
64
+ }) }));
65
+ });
@@ -0,0 +1,29 @@
1
+ import type { BoxProps, RenderOutput } from '@textui/core';
2
+ import type { DiffResult } from '../diff.js';
3
+ /**
4
+ * One file out of a changeset, both sides of it.
5
+ *
6
+ * Unified rather than side by side, and not for want of a splitter: a terminal
7
+ * that a changeset list already shares with a session pane has sixty columns
8
+ * left, and eighty characters of source in thirty is two columns of nothing
9
+ * legible. Unified spends the width on the line instead.
10
+ *
11
+ * Every row is exactly one row tall. The scroll position is a row count, so a
12
+ * line that wrapped would put the gutter numbers out of step with what is on
13
+ * screen - which is the same invariant the markdown layout keeps, for the same
14
+ * reason. Long lines are clipped, and the file is there to be read rather than
15
+ * edited.
16
+ */
17
+ export interface FileDiffProps extends BoxProps {
18
+ /** The file, as the changeset names it. */
19
+ path: string;
20
+ diff: DiffResult;
21
+ /** Absent `before` is a creation, absent `after` a deletion. */
22
+ kind: 'new' | 'edited' | 'deleted';
23
+ /** Set instead of a diff when the bytes are not text. */
24
+ binary?: {
25
+ bytes: number;
26
+ contentType?: string;
27
+ };
28
+ }
29
+ export declare const FileDiff: (props: FileDiffProps) => RenderOutput;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
2
+ import { defineComponent, useTheme } from '@textui/core';
3
+ import { Column, EmptyState, Row, ScrollView } from '@textui/widgets';
4
+ export const FileDiff = defineComponent('FileDiff', (props) => {
5
+ const { path, diff, kind, binary, ...rest } = props;
6
+ const theme = useTheme();
7
+ if (binary) {
8
+ return (_jsx(EmptyState, { title: "Not text", message: `${binary.contentType ?? 'Binary'}, ${binary.bytes} bytes. There is nothing to show a line at a time.`, ...rest }));
9
+ }
10
+ if (diff.tooLarge) {
11
+ return (_jsx(EmptyState, { title: "Too big to line up", message: `${diff.tooLarge.lines} lines between the two sides, over the ${diff.tooLarge.limit} this will compare. Open it in an editor.`, ...rest }));
12
+ }
13
+ if (diff.rows.length === 0) {
14
+ return _jsx(EmptyState, { title: "Nothing between the two", message: "Both sides of this file are the same.", ...rest });
15
+ }
16
+ // The widest line number either side will need, so the gutter does not
17
+ // change width partway down the file.
18
+ const width = String(Math.max(...diff.rows.map((row) => Math.max(row.before ?? 0, row.after ?? 0)))).length;
19
+ return (_jsxs(Column, { ...rest, children: [_jsxs(Row, { gap: 1, children: [_jsx("text", { content: kind === 'new' ? '+' : kind === 'deleted' ? '-' : theme.glyphs.chevronRight, fg: kind === 'new' ? 'success' : kind === 'deleted' ? 'danger' : 'muted', shrink: 0 }), _jsx("text", { content: path, flex: 1, truncate: "start" }), _jsx("text", { content: `+${diff.added}`, fg: "success", shrink: 0 }), _jsx("text", { content: `-${diff.removed}`, fg: "danger", shrink: 0 })] }), _jsx(ScrollView, { flex: 1, focusId: "changes.file", children: _jsx(Column, { children: diff.rows.map((row, index) => (_jsxs(Row, { gap: 1, children: [_jsx("text", { content: String(row.before ?? '').padStart(width), fg: "subtle", shrink: 0 }), _jsx("text", { content: String(row.after ?? '').padStart(width), fg: "subtle", shrink: 0 }), _jsx("text", { content: row.kind === 'added' ? '+' : row.kind === 'removed' ? '-' : ' ', fg: row.kind === 'added' ? 'success' : row.kind === 'removed' ? 'danger' : 'muted', shrink: 0 }), _jsx("text", { content: row.text, wrap: "none", truncate: "end", flex: 1, ...(row.kind === 'added'
20
+ ? { fg: 'success' }
21
+ : row.kind === 'removed'
22
+ ? { fg: 'danger' }
23
+ : {}) })] }, index))) }) })] }));
24
+ });
@@ -0,0 +1,31 @@
1
+ import type { BoxProps, RenderOutput } from '@textui/core';
2
+ import type { ResourceEntry } from '../ahp/types.js';
3
+ /**
4
+ * One directory of the host's filesystem.
5
+ *
6
+ * The *host's*, which is the only thing about this screen worth stating twice:
7
+ * the daemon may be on another machine, and the tree being walked is the one
8
+ * the agent is working in rather than the one this terminal is sitting on. A
9
+ * client that listed its own directory would be showing the right shape and
10
+ * the wrong files.
11
+ *
12
+ * A `List` for the same reason the changeset is one - the selection, the keys,
13
+ * the window and the highlight are the list's - and one directory at a time
14
+ * rather than a tree, because `resourceList` answers about one directory and
15
+ * expanding a node would be a request per node with nothing to show meanwhile.
16
+ */
17
+ export interface FileListProps extends BoxProps {
18
+ /** Where this is looking, as a `file://` URI on the host. */
19
+ at: string;
20
+ entries: ResourceEntry[];
21
+ loading?: boolean;
22
+ /** What the host said when it would not answer. */
23
+ failure?: string;
24
+ /** Enter on a row: a directory to descend into, or a file to open. */
25
+ onOpen?(entry: ResourceEntry): void;
26
+ /** Up out of this directory. Absent at the top of what the host serves. */
27
+ onUp?(): void;
28
+ focusId?: string;
29
+ autoFocus?: boolean;
30
+ }
31
+ export declare const FileList: (props: FileListProps) => RenderOutput;
@@ -0,0 +1,39 @@
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
+ export const FileList = defineComponent('FileList', (props) => {
5
+ const { at, entries, loading, failure, onOpen, onUp, focusId, autoFocus, ...rest } = props;
6
+ const theme = useTheme();
7
+ if (failure !== undefined) {
8
+ // The host's own words. A host that serves no filesystem answers
9
+ // `-32601`, and a blank pane would be this client looking broken for
10
+ // something the host said plainly.
11
+ return _jsx(EmptyState, { title: "The host would not list it", message: failure, ...rest });
12
+ }
13
+ if (loading === true)
14
+ return _jsx(EmptyState, { title: "Reading the directory", ...rest });
15
+ const items = [
16
+ // The way up, as a row rather than a key, because it is where a person
17
+ // looks for it and because a key that only works sometimes is worse.
18
+ ...(onUp ? [{ id: '..', icon: theme.glyphs.chevronUp, label: '..', tone: 'muted' }] : []),
19
+ ...entries.map((entry) => ({
20
+ id: entry.uri,
21
+ icon: entry.kind === 'directory' ? theme.glyphs.chevronRight : ' ',
22
+ label: entry.name,
23
+ // Bytes only for files. A size against a directory is a number about
24
+ // the directory entry rather than about what is in it.
25
+ meta: entry.kind === 'directory' ? '' : String(entry.size ?? ''),
26
+ tone: (entry.kind === 'directory' ? 'accent' : 'muted'),
27
+ })),
28
+ ];
29
+ const byId = new Map(entries.map((entry) => [entry.uri, entry]));
30
+ return (_jsxs(Column, { ...rest, children: [_jsx(Marquee, { content: at.replace(/^file:\/\//, ''), truncate: "start", fg: "muted" }), _jsx(List, { items: items, flex: 1, renderItem: (item, state) => (_jsxs(Row, { gap: 1, children: [_jsx("text", { content: item.icon ?? ' ', ...(state.selected ? {} : { fg: item.tone }), shrink: 0 }), _jsx(Marquee, { content: item.label, active: state.selected && state.focused, truncate: "end", flex: 1 }), _jsx("text", { content: item.meta ?? '', fg: "muted", shrink: 0 })] })), onActivate: (id) => {
31
+ if (id === '..') {
32
+ onUp?.();
33
+ return;
34
+ }
35
+ const found = byId.get(id);
36
+ if (found)
37
+ onOpen?.(found);
38
+ }, ...(focusId ? { focusId } : {}), ...(autoFocus ? { autoFocus: true } : {}), emptyMessage: "Nothing here" })] }));
39
+ });
@@ -0,0 +1,43 @@
1
+ import type { BindingPath, BoxProps, RenderOutput } from '@textui/core';
2
+ import type { Answer, PendingInput } from '../ahp/types.js';
3
+ /**
4
+ * The block that means the agent is stopped, waiting on a person.
5
+ *
6
+ * It is the only thing on the screen that is waiting on the reader, so it
7
+ * takes the focus, traps it, and is answerable without leaving the keyboard.
8
+ * Rendered as one more bubble in the transcript it scrolls away, and a blocked
9
+ * agent that has scrolled off is a session that looks merely slow.
10
+ *
11
+ * Two kinds, and they are nothing alike:
12
+ *
13
+ * a **tool confirmation** is a yes or a no about a command, with named
14
+ * options where the host offers them;
15
+ *
16
+ * a **question** carries no tool call at all. Its prose is the request's own
17
+ * message and what is being asked is its questions. Rendering it as a
18
+ * confirmation loses the entire request - the choices vanish, the question
19
+ * is never shown, and what is left is a heading and an Approve button.
20
+ */
21
+ export declare const ANSWERS: BindingPath;
22
+ export interface ChatHitlProps extends BoxProps {
23
+ input: PendingInput;
24
+ onApprove(optionId?: string): void;
25
+ onDeny(): void;
26
+ onAnswer(answers: Record<string, Answer>, accepted: boolean): void;
27
+ /** Leave it up, but give the keyboard back. */
28
+ onEscape?(): void;
29
+ }
30
+ export declare const ChatHitl: (props: ChatHitlProps) => RenderOutput;
31
+ /**
32
+ * What came of the answer, on the row between the block and the composer.
33
+ *
34
+ * That row was a blank one, and it is where a person is already looking after
35
+ * pressing a button on the block above it. A press that reaches a host which
36
+ * then says nothing is indistinguishable from a key that was never read - so
37
+ * this says the answer has gone, and says so in red when it did not.
38
+ *
39
+ * Nothing to say is no row rather than an empty one: a status line that is
40
+ * always there is a row of chrome, and the composer is a row further from the
41
+ * conversation for the whole of every session in which nothing goes wrong.
42
+ */
43
+ export declare const ChatInputStatus: (props: BoxProps) => RenderOutput;