@softov/ahpc 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/src/ahp/channels.d.ts +146 -0
- package/dist/src/ahp/channels.js +423 -0
- package/dist/src/ahp/connection.d.ts +656 -0
- package/dist/src/ahp/connection.js +1 -0
- package/dist/src/ahp/fake.d.ts +69 -0
- package/dist/src/ahp/fake.js +1996 -0
- package/dist/src/ahp/live.d.ts +160 -0
- package/dist/src/ahp/live.js +2641 -0
- package/dist/src/ahp/operate.d.ts +36 -0
- package/dist/src/ahp/operate.js +56 -0
- package/dist/src/ahp/publish.d.ts +39 -0
- package/dist/src/ahp/publish.js +370 -0
- package/dist/src/ahp/status.d.ts +23 -0
- package/dist/src/ahp/status.js +23 -0
- package/dist/src/ahp/types.d.ts +745 -0
- package/dist/src/ahp/types.js +27 -0
- package/dist/src/app.d.ts +54 -0
- package/dist/src/app.js +369 -0
- package/dist/src/blocks.d.ts +88 -0
- package/dist/src/blocks.js +102 -0
- package/dist/src/cli/main.d.ts +13 -0
- package/dist/src/cli/main.js +1537 -0
- package/dist/src/cli/render.d.ts +43 -0
- package/dist/src/cli/render.js +91 -0
- package/dist/src/config.d.ts +63 -0
- package/dist/src/config.js +45 -0
- package/dist/src/connect.d.ts +53 -0
- package/dist/src/connect.js +69 -0
- package/dist/src/control.d.ts +189 -0
- package/dist/src/control.js +2241 -0
- package/dist/src/diff.d.ts +44 -0
- package/dist/src/diff.js +111 -0
- package/dist/src/flags.d.ts +28 -0
- package/dist/src/flags.js +76 -0
- package/dist/src/main.d.ts +10 -0
- package/dist/src/main.js +54 -0
- package/dist/src/schedule.d.ts +46 -0
- package/dist/src/schedule.js +140 -0
- package/dist/src/screens.d.ts +79 -0
- package/dist/src/screens.js +1322 -0
- package/dist/src/state.d.ts +355 -0
- package/dist/src/state.js +531 -0
- package/dist/src/terminal.d.ts +44 -0
- package/dist/src/terminal.js +101 -0
- package/dist/src/tui.d.ts +92 -0
- package/dist/src/tui.js +372 -0
- package/dist/src/view/automations.d.ts +31 -0
- package/dist/src/view/automations.js +76 -0
- package/dist/src/view/bood/art.d.ts +90 -0
- package/dist/src/view/bood/art.js +152 -0
- package/dist/src/view/bood/beetle.d.ts +7 -0
- package/dist/src/view/bood/beetle.js +123 -0
- package/dist/src/view/bood/bunny.d.ts +8 -0
- package/dist/src/view/bood/bunny.js +228 -0
- package/dist/src/view/bood/cat.d.ts +9 -0
- package/dist/src/view/bood/cat.js +358 -0
- package/dist/src/view/bood/crab.d.ts +7 -0
- package/dist/src/view/bood/crab.js +121 -0
- package/dist/src/view/bood/index.d.ts +29 -0
- package/dist/src/view/bood/index.js +25 -0
- package/dist/src/view/bood/motion.d.ts +178 -0
- package/dist/src/view/bood/motion.js +298 -0
- package/dist/src/view/bood/owl.d.ts +9 -0
- package/dist/src/view/bood/owl.js +285 -0
- package/dist/src/view/bood/registry.d.ts +36 -0
- package/dist/src/view/bood/registry.js +218 -0
- package/dist/src/view/bood/render.d.ts +31 -0
- package/dist/src/view/bood/render.js +196 -0
- package/dist/src/view/bood/sprite.d.ts +18 -0
- package/dist/src/view/bood/sprite.js +130 -0
- package/dist/src/view/bood/sprout.d.ts +8 -0
- package/dist/src/view/bood/sprout.js +121 -0
- package/dist/src/view/bood/types.d.ts +158 -0
- package/dist/src/view/bood/types.js +57 -0
- package/dist/src/view/bubble.d.ts +75 -0
- package/dist/src/view/bubble.js +86 -0
- package/dist/src/view/changes.d.ts +35 -0
- package/dist/src/view/changes.js +22 -0
- package/dist/src/view/composer.d.ts +64 -0
- package/dist/src/view/composer.js +192 -0
- package/dist/src/view/controls.d.ts +44 -0
- package/dist/src/view/controls.js +49 -0
- package/dist/src/view/creature.d.ts +69 -0
- package/dist/src/view/creature.js +83 -0
- package/dist/src/view/customizations.d.ts +28 -0
- package/dist/src/view/customizations.js +53 -0
- package/dist/src/view/details.d.ts +65 -0
- package/dist/src/view/details.js +65 -0
- package/dist/src/view/filediff.d.ts +29 -0
- package/dist/src/view/filediff.js +24 -0
- package/dist/src/view/files.d.ts +31 -0
- package/dist/src/view/files.js +39 -0
- package/dist/src/view/hitl.d.ts +43 -0
- package/dist/src/view/hitl.js +171 -0
- package/dist/src/view/icons.d.ts +13 -0
- package/dist/src/view/icons.js +71 -0
- package/dist/src/view/picker.d.ts +42 -0
- package/dist/src/view/picker.js +71 -0
- package/dist/src/view/sessionhead.d.ts +41 -0
- package/dist/src/view/sessionhead.js +60 -0
- package/dist/src/view/sessions.d.ts +34 -0
- package/dist/src/view/sessions.js +61 -0
- package/dist/src/view/terminal.d.ts +30 -0
- package/dist/src/view/terminal.js +32 -0
- package/dist/src/view/toolcall.d.ts +27 -0
- package/dist/src/view/toolcall.js +48 -0
- package/dist/src/view/transcript.d.ts +50 -0
- package/dist/src/view/transcript.js +60 -0
- package/package.json +72 -0
|
@@ -0,0 +1,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;
|