@softov/ahpc 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/src/ahp/fake.js +61 -14
- package/dist/src/ahp/live.d.ts +7 -0
- package/dist/src/ahp/live.js +93 -37
- package/dist/src/ahp/publish.js +13 -0
- package/dist/src/ahp/types.d.ts +20 -0
- package/dist/src/app.js +34 -7
- package/dist/src/blocks.d.ts +4 -74
- package/dist/src/blocks.js +10 -48
- package/dist/src/cli/main.d.ts +1 -1
- package/dist/src/cli/main.js +75 -2
- package/dist/src/connect.d.ts +2 -0
- package/dist/src/connect.js +1 -0
- package/dist/src/control.d.ts +6 -0
- package/dist/src/control.js +140 -10
- package/dist/src/flags.js +1 -1
- package/dist/src/links.d.ts +54 -0
- package/dist/src/links.js +120 -0
- package/dist/src/resources.d.ts +13 -0
- package/dist/src/resources.js +46 -0
- package/dist/src/screens.js +95 -50
- package/dist/src/state.d.ts +42 -0
- package/dist/src/state.js +80 -1
- package/dist/src/tui.d.ts +3 -1
- package/dist/src/tui.js +28 -4
- package/dist/src/view/creature.d.ts +0 -12
- package/dist/src/view/creature.js +0 -20
- package/dist/src/view/wire.d.ts +36 -0
- package/dist/src/view/wire.js +196 -0
- package/dist/src/wire.d.ts +70 -0
- package/dist/src/wire.js +194 -0
- package/dist/src/wiretui.d.ts +22 -0
- package/dist/src/wiretui.js +69 -0
- package/package.json +6 -5
- package/dist/src/diff.d.ts +0 -44
- package/dist/src/diff.js +0 -111
- package/dist/src/view/bubble.d.ts +0 -75
- package/dist/src/view/bubble.js +0 -86
- package/dist/src/view/composer.d.ts +0 -64
- package/dist/src/view/composer.js +0 -192
- package/dist/src/view/controls.d.ts +0 -44
- package/dist/src/view/controls.js +0 -49
- package/dist/src/view/details.d.ts +0 -65
- package/dist/src/view/details.js +0 -65
- package/dist/src/view/filediff.d.ts +0 -29
- package/dist/src/view/filediff.js +0 -24
- package/dist/src/view/hitl.d.ts +0 -43
- package/dist/src/view/hitl.js +0 -171
- package/dist/src/view/icons.d.ts +0 -13
- package/dist/src/view/icons.js +0 -71
- package/dist/src/view/picker.d.ts +0 -42
- package/dist/src/view/picker.js +0 -71
- package/dist/src/view/sessionhead.d.ts +0 -41
- package/dist/src/view/sessionhead.js +0 -60
- package/dist/src/view/sessions.d.ts +0 -34
- package/dist/src/view/sessions.js +0 -61
- package/dist/src/view/toolcall.d.ts +0 -27
- package/dist/src/view/toolcall.js +0 -48
- package/dist/src/view/transcript.d.ts +0 -50
- package/dist/src/view/transcript.js +0 -60
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
|
|
2
|
-
import { defineComponent, useEffect, useMeasure, useSize, useState, useTheme } from '@textui/core';
|
|
3
|
-
import { Column, Divider, List, TextArea } from '@textui/widgets';
|
|
4
|
-
import { ComposerBar } from './controls.js';
|
|
5
|
-
import { useFloorTop } from './creature.js';
|
|
6
|
-
/**
|
|
7
|
-
* Rows the completion menu shows at once, at most.
|
|
8
|
-
*
|
|
9
|
-
* A cap on the box's height and not on the list: what does not fit is
|
|
10
|
-
* scrolled to. The menu sits above the composer and takes its room from it,
|
|
11
|
-
* so on a short terminal this is not the number that applies - see `fits`.
|
|
12
|
-
*/
|
|
13
|
-
const VISIBLE = 8;
|
|
14
|
-
/**
|
|
15
|
-
* Rows the composer itself takes: two border, two divider, the field and the
|
|
16
|
-
* bar under it. Eight menu rows on a terminal twelve high left four for all
|
|
17
|
-
* of that and drew an empty box - a menu on top of a field with no room to
|
|
18
|
-
* type in it.
|
|
19
|
-
*/
|
|
20
|
-
const COMPOSER_ROWS = 6;
|
|
21
|
-
/** The menu's own frame, which is height the list does not get. */
|
|
22
|
-
const MENU_BORDER = 2;
|
|
23
|
-
/**
|
|
24
|
-
* Rows the menu may have here, which is whatever the composer can spare.
|
|
25
|
-
*
|
|
26
|
-
* The floor is there because a terminal can always be made too short for
|
|
27
|
-
* both; below it the composer gives way, since a menu with nothing under it
|
|
28
|
-
* is the same dead end from the other side.
|
|
29
|
-
*/
|
|
30
|
-
const fits = (height) => Math.max(3, Math.min(VISIBLE, height - COMPOSER_ROWS - MENU_BORDER));
|
|
31
|
-
export const ChatComposer = defineComponent('ChatComposer', (props) => {
|
|
32
|
-
const { value, onChange, onSubmit, onCancel, onHistory, onLeave, running, queued = 0, options = [], onOption, placeholder, commands = [], onCommand, paths = [], onPath, autoFocus, focusId = 'chat.composer', ...rest } = props;
|
|
33
|
-
const theme = useTheme();
|
|
34
|
-
// A slash menu is a completion over what is already typed, not a mode.
|
|
35
|
-
const slash = value.startsWith('/') && !value.includes(' ') ? value.slice(1).toLowerCase() : null;
|
|
36
|
-
const found = slash === null ? [] : commands
|
|
37
|
-
.filter((command) => command.id.toLowerCase().includes(slash) || command.title.toLowerCase().includes(slash))
|
|
38
|
-
// What the host contributed first. A person typing a slash into a chat
|
|
39
|
-
// is usually reaching for a skill, and the client's own commands - which
|
|
40
|
-
// are also in the palette, on their own key - would otherwise fill the
|
|
41
|
-
// rows that are visible without scrolling.
|
|
42
|
-
.sort((a, b) => (a.kind === b.kind ? 0 : a.kind === 'session' ? -1 : 1));
|
|
43
|
-
const byId = new Map(found.map((command) => [command.id, command]));
|
|
44
|
-
/*
|
|
45
|
-
* One menu, and whichever list is live fills it.
|
|
46
|
-
*
|
|
47
|
-
* The two cannot both be: a slash menu is a draft that *starts* with a
|
|
48
|
-
* slash, and a path menu is a word the caret is in that starts with an
|
|
49
|
-
* at-sign. Two menus would be two boxes above one field.
|
|
50
|
-
*/
|
|
51
|
-
const offered = found.length > 0
|
|
52
|
-
? found.map((command) => ({
|
|
53
|
-
id: command.id,
|
|
54
|
-
label: `/${command.id}`,
|
|
55
|
-
...(command.description ? { description: command.description } : {}),
|
|
56
|
-
// Where it came from, when something did: two plugins can contribute
|
|
57
|
-
// a `/review`, and the title alone does not say which this is.
|
|
58
|
-
meta: command.from ?? command.title,
|
|
59
|
-
}))
|
|
60
|
-
: paths.map((path) => ({
|
|
61
|
-
id: path.insertText,
|
|
62
|
-
label: path.insertText,
|
|
63
|
-
...(path.description ? { description: path.description } : {}),
|
|
64
|
-
}));
|
|
65
|
-
const byInsert = new Map(paths.map((path) => [path.insertText, path]));
|
|
66
|
-
/*
|
|
67
|
-
* Escape closes the menu before it does anything else.
|
|
68
|
-
*
|
|
69
|
-
* The menu is drawn from the draft, so there is no state to close - which
|
|
70
|
-
* is why escape used to pass straight through it to the field and then to
|
|
71
|
-
* the screen, and typing `/` and pressing escape left for the session
|
|
72
|
-
* list. What is remembered is the draft it was dismissed at: the menu
|
|
73
|
-
* stays shut for that exact text and comes back the moment another
|
|
74
|
-
* character makes it a different question.
|
|
75
|
-
*
|
|
76
|
-
* And it is forgotten as soon as there is no menu to dismiss. Remembering
|
|
77
|
-
* the text alone was not enough: dismissing at `/`, deleting it and typing
|
|
78
|
-
* `/` again produced the same draft, so the menu stayed shut for a
|
|
79
|
-
* question that had been asked afresh. Clearing when nothing matches ties
|
|
80
|
-
* the dismissal to one continuous menu rather than to a string that can
|
|
81
|
-
* come back.
|
|
82
|
-
*/
|
|
83
|
-
const [dismissedAt, setDismissedAt] = useState(null);
|
|
84
|
-
const empty = offered.length === 0;
|
|
85
|
-
useEffect(() => {
|
|
86
|
-
if (empty && dismissedAt !== null)
|
|
87
|
-
setDismissedAt(null);
|
|
88
|
-
}, [empty]);
|
|
89
|
-
const matches = dismissedAt === value ? [] : offered;
|
|
90
|
-
// Which completion is under the cursor. Clamped rather than reset, so a
|
|
91
|
-
// list that shrinks as more is typed keeps a valid row instead of
|
|
92
|
-
// snapping back to the top on every keystroke.
|
|
93
|
-
const [highlight, setHighlight] = useState(0);
|
|
94
|
-
const index = Math.max(0, Math.min(highlight, matches.length - 1));
|
|
95
|
-
const chosen = matches[index];
|
|
96
|
-
/*
|
|
97
|
-
* What goes after the name of the command under the cursor.
|
|
98
|
-
*
|
|
99
|
-
* A row is the name, so a command that takes an argument has nowhere in
|
|
100
|
-
* the list to say so, and `/autocompact` reads as complete when it is
|
|
101
|
-
* not. It goes on the rule under the list, where it is one line for the
|
|
102
|
-
* whole menu and changes as the highlight moves rather than being
|
|
103
|
-
* repeated down every row.
|
|
104
|
-
*/
|
|
105
|
-
const usage = chosen === undefined ? undefined : byId.get(chosen.id)?.hint;
|
|
106
|
-
const hint = usage === undefined ? undefined : `/${chosen?.id ?? ''} ${usage}`;
|
|
107
|
-
/**
|
|
108
|
-
* Up and down, while the menu is open.
|
|
109
|
-
*
|
|
110
|
-
* They arrive as `onOverflow` - the field reports the key rather than
|
|
111
|
-
* handling it once there is no row above or below the caret, which for a
|
|
112
|
-
* `/word` draft is immediately. The same pair walks the history when there
|
|
113
|
-
* is no menu, and the menu is the nearer of the two things they could
|
|
114
|
-
* mean.
|
|
115
|
-
*/
|
|
116
|
-
const step = (direction) => {
|
|
117
|
-
setHighlight((matches.length + index + direction) % matches.length);
|
|
118
|
-
};
|
|
119
|
-
// Where this box starts, so the creature has somewhere to stand that is
|
|
120
|
-
// not on it. The slash menu grows this upward, so it is read every time
|
|
121
|
-
// rather than being a number somebody wrote down once.
|
|
122
|
-
useFloorTop('composer', useMeasure().y);
|
|
123
|
-
// How tall the menu may be here. Read unconditionally: it is a hook, and
|
|
124
|
-
// the menu is drawn from a branch.
|
|
125
|
-
const rows = fits(useSize().height);
|
|
126
|
-
return (_jsxs(Column, { ...rest, gap: 0, children: [matches.length > 0 ? (
|
|
127
|
-
// The theme's border, never a named one. A hardcoded `single` draws
|
|
128
|
-
// a box-drawing frame inside an ascii one on a terminal that cannot
|
|
129
|
-
// do either, and an airy theme gets a line it deliberately does not
|
|
130
|
-
// draw anywhere else.
|
|
131
|
-
_jsxs(Column, { border: theme.border, padding: [0, 1], children: [_jsx(List, { items: matches, focusable: false, selectedId: chosen?.id,
|
|
132
|
-
/*
|
|
133
|
-
* A window over all of them, not the first six.
|
|
134
|
-
*
|
|
135
|
-
* The list scrolls to keep the selected row in view, and the
|
|
136
|
-
* selection here is driven from outside - so walking past the
|
|
137
|
-
* sixth moves the window rather than stopping. Truncating the
|
|
138
|
-
* items instead made up and down cycle the six that survived,
|
|
139
|
-
* with no way to reach a seventh: a host that answers thirty
|
|
140
|
-
* paths for `@src/` offered six of them and looked like it had
|
|
141
|
-
* no more.
|
|
142
|
-
*/
|
|
143
|
-
visibleRows: hint === undefined ? rows : rows - 1, marker: true,
|
|
144
|
-
// Not focusable, so this is the click: a completion clicked is a
|
|
145
|
-
// completion chosen, and there is nowhere for a merely
|
|
146
|
-
// highlighted row to lead.
|
|
147
|
-
onSelect: (id) => {
|
|
148
|
-
const command = byId.get(id);
|
|
149
|
-
if (command) {
|
|
150
|
-
onCommand?.(command);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const path = byInsert.get(id);
|
|
154
|
-
if (path)
|
|
155
|
-
onPath?.(path);
|
|
156
|
-
}, emptyMessage: "no command" }), hint === undefined ? null : _jsx(Divider, { label: hint })] })) : null, _jsxs(Column, { border: theme.border, children: [_jsx(Divider, { dim: true }), _jsx(TextArea, { value: value, onChange: onChange,
|
|
157
|
-
// A slash the menu matched runs here; anything else is a message,
|
|
158
|
-
// which is what lets a command the agent offers through.
|
|
159
|
-
onSubmit: (next) => {
|
|
160
|
-
const command = chosen ? byId.get(chosen.id) : undefined;
|
|
161
|
-
if (command && onCommand) {
|
|
162
|
-
onCommand(command);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
// A highlighted path completes rather than sends: enter on a
|
|
166
|
-
// menu row means "that one", and a draft half-way through a
|
|
167
|
-
// path is not a message anybody meant to send.
|
|
168
|
-
const path = chosen ? byInsert.get(chosen.id) : undefined;
|
|
169
|
-
if (path && onPath) {
|
|
170
|
-
onPath(path);
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
onSubmit(next);
|
|
174
|
-
}, onCancel: () => {
|
|
175
|
-
if (matches.length > 0) {
|
|
176
|
-
setDismissedAt(value);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
onCancel?.();
|
|
180
|
-
}, onOverflow: (direction) => {
|
|
181
|
-
if (matches.length > 0) {
|
|
182
|
-
step(direction);
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
onHistory?.(direction);
|
|
186
|
-
}, ...(onLeave ? { onEdge: (edge) => { if (edge === 'start')
|
|
187
|
-
onLeave(); } } : {}), placeholder: placeholder
|
|
188
|
-
?? (running ? 'The agent is working. Type to queue a message.' : 'Ask the agent anything…'), focusId: focusId,
|
|
189
|
-
// The caret is the one thing on this screen saying where typing
|
|
190
|
-
// goes, and this field is the point of the screen.
|
|
191
|
-
caretTone: "accent", ...(autoFocus ? { autoFocus: true } : {}) }), _jsx(Divider, { dim: true }), _jsx(ComposerBar, { options: options, onOpen: (option, anchorId) => onOption?.(option, anchorId), onSend: () => onSubmit(value), ...(running ? { running: true } : {}), queued: queued, sendDisabled: value.trim() === '' })] })] }));
|
|
192
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { BoxProps, RenderOutput } from '@textui/core';
|
|
2
|
-
/**
|
|
3
|
-
* The composer's control row: what this message will be sent as.
|
|
4
|
-
*
|
|
5
|
-
* One line under the field, and everything on it is a *current value* rather
|
|
6
|
-
* than a label - which harness, which model, how much it may do before it
|
|
7
|
-
* asks, where it works. A person can read what will happen without opening
|
|
8
|
-
* anything, and change any of it without leaving the composer.
|
|
9
|
-
*
|
|
10
|
-
* Each chip is one command's argument, asked through the palette (see
|
|
11
|
-
* `picker.ts`). That is the whole design: nothing here knows what a model or a
|
|
12
|
-
* permission mode is, so a new chip is a new command and no change to this
|
|
13
|
-
* file.
|
|
14
|
-
*/
|
|
15
|
-
export interface ComposerOption {
|
|
16
|
-
id: string;
|
|
17
|
-
/** The value, as a person reads it. Never the id the host stores. */
|
|
18
|
-
label: string;
|
|
19
|
-
/**
|
|
20
|
-
* The question, as opposed to the answer in `label`.
|
|
21
|
-
*
|
|
22
|
-
* The chip has no room for it - "Ask each time" is the whole row and the
|
|
23
|
-
* mark in front says which question it belongs to - but anything listing
|
|
24
|
-
* these somewhere with more space needs the pair, and the host's own wording
|
|
25
|
-
* for both is on the schema rather than in this file.
|
|
26
|
-
*/
|
|
27
|
-
title?: string;
|
|
28
|
-
icon?: string;
|
|
29
|
-
/** The command whose argument this chip asks about. Absent: shown, not asked. */
|
|
30
|
-
commandId?: string;
|
|
31
|
-
}
|
|
32
|
-
export interface ComposerBarProps extends BoxProps {
|
|
33
|
-
options: ComposerOption[];
|
|
34
|
-
onOpen(option: ComposerOption, anchorId: string): void;
|
|
35
|
-
onSend(): void;
|
|
36
|
-
/** A turn is running, so this message joins the queue instead. */
|
|
37
|
-
running?: boolean;
|
|
38
|
-
queued?: number;
|
|
39
|
-
sendDisabled?: boolean;
|
|
40
|
-
}
|
|
41
|
-
/** The focus id of one chip. The picker anchors to it, so it has to be knowable. */
|
|
42
|
-
export declare const chipId: (id: string) => string;
|
|
43
|
-
export declare const SEND_ID = "chat.send";
|
|
44
|
-
export declare const ComposerBar: (props: ComposerBarProps) => RenderOutput;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "@textui/core/jsx-runtime";
|
|
2
|
-
import { defineComponent, useFocus, useInput, useTheme } from '@textui/core';
|
|
3
|
-
import { Row } from '@textui/widgets';
|
|
4
|
-
/** The focus id of one chip. The picker anchors to it, so it has to be knowable. */
|
|
5
|
-
export const chipId = (id) => `chat.option.${id}`;
|
|
6
|
-
export const SEND_ID = 'chat.send';
|
|
7
|
-
export const ComposerBar = defineComponent('ComposerBar', (props) => {
|
|
8
|
-
const { options, onOpen, onSend, running, queued = 0, sendDisabled, ...rest } = props;
|
|
9
|
-
const theme = useTheme();
|
|
10
|
-
return (_jsxs(Row, { gap: 1, ...rest, children: [options.map((option, at) => (option.commandId
|
|
11
|
-
? (_jsx(Chip, { focusId: chipId(option.id),
|
|
12
|
-
// Stated, not inherited. Tab order is registration order, and
|
|
13
|
-
// which chips exist is the *host's* answer - it arrives one
|
|
14
|
-
// round trip after the row is first drawn, so the ones that were
|
|
15
|
-
// there from the start would otherwise come first however far to
|
|
16
|
-
// the right they sit. Tab would run harness, workspace, and then
|
|
17
|
-
// back to the middle.
|
|
18
|
-
order: at, label: option.label, ...(option.icon ? { icon: option.icon } : {}), onOpen: () => onOpen(option, chipId(option.id)) }, option.id))
|
|
19
|
-
: (
|
|
20
|
-
// Shown, not asked: a value that is fixed for this session is
|
|
21
|
-
// still worth reading, and a chip that opens a panel offering one
|
|
22
|
-
// choice is a worse way of saying so.
|
|
23
|
-
_jsxs(Row, { gap: 1, children: [option.icon ? _jsx("text", { content: option.icon, fg: "subtle" }) : null, _jsx("text", { content: option.label, fg: "subtle" })] }, option.id)))), _jsx("text", { content: "", flex: 1 }), queued > 0 ? _jsx("text", { content: `${queued} queued`, fg: "warning" }) : null, _jsx(Chip, { focusId: SEND_ID, order: options.length, label: running ? 'queue' : 'send', trailing: theme.glyphs.chevronRight, tone: "accent", ...(sendDisabled ? { disabled: true } : {}), onOpen: onSend })] }));
|
|
24
|
-
});
|
|
25
|
-
/**
|
|
26
|
-
* One value on the control row.
|
|
27
|
-
*
|
|
28
|
-
* Not a `Button`: a button is a verb and these are nouns, and at `size="sm"`
|
|
29
|
-
* four of them still read as four buttons rather than as one sentence about
|
|
30
|
-
* what is about to be sent. What it borrows from a button is the part that
|
|
31
|
-
* matters - it is focusable, tab reaches it, and enter opens it.
|
|
32
|
-
*/
|
|
33
|
-
const Chip = defineComponent('ComposerChip', (props) => {
|
|
34
|
-
const { focusId, order, label, icon, trailing, tone, disabled, onOpen } = props;
|
|
35
|
-
const theme = useTheme();
|
|
36
|
-
const focus = useFocus({ id: focusId, order, disabled: disabled === true });
|
|
37
|
-
useInput((event) => {
|
|
38
|
-
if (disabled)
|
|
39
|
-
return false;
|
|
40
|
-
// Down as well as enter: the panel comes up out of the chip, and reaching
|
|
41
|
-
// for it downwards is what the shape of the thing suggests.
|
|
42
|
-
if (event.name === 'enter' || event.name === 'space' || event.name === 'down') {
|
|
43
|
-
onOpen();
|
|
44
|
-
return true;
|
|
45
|
-
}
|
|
46
|
-
return false;
|
|
47
|
-
}, { focusId: focus.id, enabled: disabled !== true });
|
|
48
|
-
return (_jsxs(Row, { id: focus.id, gap: 1, padding: [0, 1], ...(focus.focused ? { bg: 'selected' } : {}), onClick: disabled ? undefined : onOpen, children: [icon ? _jsx("text", { content: icon, shrink: 0, fg: focus.focused ? 'inverted' : tone ?? 'muted' }) : null, _jsx("text", { content: label, truncate: "end", fg: focus.focused ? 'inverted' : disabled ? 'disabled' : tone ?? undefined, ...(tone ? { bold: true } : {}) }), _jsx("text", { content: trailing ?? theme.glyphs.chevronDown, shrink: 0, fg: focus.focused ? 'inverted' : 'subtle' })] }));
|
|
49
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
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;
|
package/dist/src/view/details.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
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;
|
|
@@ -1,24 +0,0 @@
|
|
|
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
|
-
});
|
package/dist/src/view/hitl.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
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;
|