@multiclawcli/dashboard 0.6.3 → 0.7.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/dist/tui/components/interaction.d.ts +44 -0
- package/dist/tui/components/overlays.d.ts +0 -17
- package/dist/tui/components/prompt.d.ts +53 -2
- package/dist/tui/interaction.d.ts +173 -0
- package/dist/tui/layout.d.ts +35 -0
- package/dist/tui.js +733 -248
- package/package.json +2 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { RuntimePermissionRequest, RuntimeQuestionRequest } from "@multiclawcli/core";
|
|
2
|
+
import { type FormField, type FormState } from "../interaction";
|
|
3
|
+
/**
|
|
4
|
+
* The bar the console docks above the prompt when an agent is blocked on the
|
|
5
|
+
* user.
|
|
6
|
+
*
|
|
7
|
+
* It is *docked* rather than a body overlay on purpose. The old screens replaced
|
|
8
|
+
* the stream with the question, which took away the one thing needed to answer
|
|
9
|
+
* it — what the agent had been doing up to the point it asked. Approval is a
|
|
10
|
+
* decision made *about* the stream, so it belongs next to it, not instead of it.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is one row per fact and clipped by the caller, which is why
|
|
13
|
+
* the row count in `interaction.ts` only has to be an upper bound.
|
|
14
|
+
*/
|
|
15
|
+
export declare function PermissionBar({ request, others, cursor, released, width, }: {
|
|
16
|
+
request: RuntimePermissionRequest;
|
|
17
|
+
/** Requests behind this one, shown so "2 waiting" is not a bare number. */
|
|
18
|
+
others: readonly RuntimePermissionRequest[];
|
|
19
|
+
/** Highlighted option, or `undefined` when nothing has been picked yet. */
|
|
20
|
+
cursor: number | undefined;
|
|
21
|
+
/** The user asked for the keyboard back; only the commands still reach it. */
|
|
22
|
+
released: boolean;
|
|
23
|
+
width: number;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* The question, as a form.
|
|
27
|
+
*
|
|
28
|
+
* The old path asked for `/respond q1 {"database":"postgres"}` — a hand-written
|
|
29
|
+
* JSON object matching a schema the user could only read in the same terminal
|
|
30
|
+
* that was now occupied by what they were typing. Here every field is a row, the
|
|
31
|
+
* focused row is the one the keystrokes go to, and choices are picked rather
|
|
32
|
+
* than spelled. The prompt below still does the typing: it already owns the
|
|
33
|
+
* caret, the wrapping and the paste handling, so a text field reuses it instead
|
|
34
|
+
* of growing a second editor.
|
|
35
|
+
*/
|
|
36
|
+
export declare function QuestionForm({ request, others, fields, form, draft, width, }: {
|
|
37
|
+
request: RuntimeQuestionRequest;
|
|
38
|
+
others: readonly RuntimeQuestionRequest[];
|
|
39
|
+
fields: FormField[];
|
|
40
|
+
form: FormState;
|
|
41
|
+
/** Text in the prompt, which is the focused text field's editor. */
|
|
42
|
+
draft: string;
|
|
43
|
+
width: number;
|
|
44
|
+
}): import("react").JSX.Element;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { RuntimePermissionRequest, RuntimeQuestionRequest } from "@multiclawcli/core";
|
|
2
1
|
import { type TurnView } from "../state";
|
|
3
2
|
interface HelpGroup {
|
|
4
3
|
title: string;
|
|
@@ -44,22 +43,6 @@ export declare function HelpOverlay({ width, maxRows }: {
|
|
|
44
43
|
width: number;
|
|
45
44
|
maxRows: number;
|
|
46
45
|
}): import("react").JSX.Element;
|
|
47
|
-
export declare function PermissionOverlay({ request, shortId, queued, width, maxRows, }: {
|
|
48
|
-
request: RuntimePermissionRequest;
|
|
49
|
-
shortId: string;
|
|
50
|
-
queued: number;
|
|
51
|
-
width: number;
|
|
52
|
-
maxRows: number;
|
|
53
|
-
}): import("react").JSX.Element;
|
|
54
|
-
export declare function questionLines(request: RuntimeQuestionRequest, width: number): string[];
|
|
55
|
-
export declare function QuestionOverlay({ request, shortId, queued, width, maxRows, offset, }: {
|
|
56
|
-
request: RuntimeQuestionRequest;
|
|
57
|
-
shortId: string;
|
|
58
|
-
queued: number;
|
|
59
|
-
width: number;
|
|
60
|
-
maxRows: number;
|
|
61
|
-
offset: number;
|
|
62
|
-
}): import("react").JSX.Element;
|
|
63
46
|
/**
|
|
64
47
|
* The finished turns, newest first.
|
|
65
48
|
*
|
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
import type { PaletteRow } from "../commands";
|
|
2
|
+
/** Columns the marker occupies, so a wrapped row can leave exactly that gap. */
|
|
3
|
+
export declare const MARKER_WIDTH: number;
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
5
|
+
* How many rows a long input may occupy.
|
|
6
|
+
*
|
|
7
|
+
* The prompt grows with the text, and the body gives up a row for each one it
|
|
8
|
+
* takes — so without a ceiling a pasted document would squeeze the stream out
|
|
9
|
+
* of existence. Past this the prompt scrolls instead of growing.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_PROMPT_ROWS = 5;
|
|
12
|
+
/** One row of the prompt, already broken to the width it will be drawn at. */
|
|
13
|
+
export interface PromptLine {
|
|
14
|
+
text: string;
|
|
15
|
+
/**
|
|
16
|
+
* Where the caret sits inside `text`, as an index into it, or `undefined`
|
|
17
|
+
* when the caret is on another row.
|
|
18
|
+
*
|
|
19
|
+
* An index rather than a column because a wide character is one index and two
|
|
20
|
+
* columns: splitting the row by column would cut a CJK character in half.
|
|
21
|
+
*/
|
|
22
|
+
caret?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Break the input into the rows it will actually be drawn on.
|
|
26
|
+
*
|
|
27
|
+
* Broken by *character*, not by word: a terminal input line has no words to
|
|
28
|
+
* respect, and typing past the last column simply continues on the next row.
|
|
29
|
+
* Word-wrapping here would move the text under the user's fingers — a space
|
|
30
|
+
* typed at the end of a row would push a whole word down and take the caret
|
|
31
|
+
* with it.
|
|
32
|
+
*
|
|
33
|
+
* Every character is kept, in order, across the rows: nothing is dropped at a
|
|
34
|
+
* break, which is what lets the caret be tracked as an index into a row.
|
|
35
|
+
*/
|
|
36
|
+
export declare function promptLines(value: string, cursor: number, width: number): PromptLine[];
|
|
37
|
+
/**
|
|
38
|
+
* How many rows the prompt is asking for, capped at what it will actually draw.
|
|
39
|
+
*
|
|
40
|
+
* Exported because the layout has to know before Ink lays anything out: the
|
|
41
|
+
* body is given whatever the chrome leaves, and an unbudgeted prompt row shows
|
|
42
|
+
* up as a stream pane that silently loses its last line.
|
|
43
|
+
*/
|
|
44
|
+
export declare function promptRowCount(value: string, cursor: number, width: number): number;
|
|
45
|
+
/**
|
|
46
|
+
* The prompt.
|
|
4
47
|
*
|
|
5
48
|
* The terminal's own cursor is hidden while the console owns the screen, so the
|
|
6
49
|
* caret has to be drawn. It sits *between* the text before and after it rather
|
|
@@ -11,12 +54,20 @@ import type { PaletteRow } from "../commands";
|
|
|
11
54
|
* caret that *added* a cell while visible would shove the placeholder one
|
|
12
55
|
* column sideways twice a second. Instead the caret is painted *onto* the ghost
|
|
13
56
|
* text's first character, which is what a real block cursor does.
|
|
57
|
+
*
|
|
58
|
+
* The rows are broken here rather than by Ink. Ink would wrap the text happily,
|
|
59
|
+
* but the caret is a *sibling* of the text, not part of it — so a wrapped line
|
|
60
|
+
* left the caret stranded at the right end of the first row while the rest of
|
|
61
|
+
* the text carried on underneath it. Breaking the line first puts the caret on
|
|
62
|
+
* the row the text actually reached.
|
|
14
63
|
*/
|
|
15
|
-
export declare function Prompt({ value, cursor, caretVisible, placeholder, }: {
|
|
64
|
+
export declare function Prompt({ value, cursor, caretVisible, placeholder, width, }: {
|
|
16
65
|
value: string;
|
|
17
66
|
cursor: number;
|
|
18
67
|
caretVisible: boolean;
|
|
19
68
|
placeholder: string;
|
|
69
|
+
/** Columns the whole prompt line may occupy, marker included. */
|
|
70
|
+
width: number;
|
|
20
71
|
}): import("react").JSX.Element;
|
|
21
72
|
/**
|
|
22
73
|
* The palette, rendered directly above the prompt.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two things an agent can stop and ask for, modelled as something the user
|
|
3
|
+
* can *point at* instead of something they have to spell out.
|
|
4
|
+
*
|
|
5
|
+
* Both used to be answered by command — `/approve p4 1`, `/respond q1
|
|
6
|
+
* {"database":"postgres"}` — which put three transformations between reading a
|
|
7
|
+
* question and answering it: remember an internal alias, map a number back onto
|
|
8
|
+
* a list rendered somewhere else, and hand-write JSON that happens to match a
|
|
9
|
+
* schema. None of those are the decision. So the rendered question becomes the
|
|
10
|
+
* thing that is operated on: the option you are looking at is the option you
|
|
11
|
+
* commit, and the field you are editing is the field that receives the
|
|
12
|
+
* keystroke.
|
|
13
|
+
*
|
|
14
|
+
* Pure on purpose. The keyboard rules are the part of this easiest to get
|
|
15
|
+
* subtly wrong — a bare key that both selects and types, a required field that
|
|
16
|
+
* silently submits empty — so they live here, where they can be tested without
|
|
17
|
+
* rendering a terminal.
|
|
18
|
+
*/
|
|
19
|
+
import type { RuntimePermissionRequest, RuntimeQuestionRequest } from "@multiclawcli/core";
|
|
20
|
+
export type PermissionOptionKind = RuntimePermissionRequest["options"][number]["kind"];
|
|
21
|
+
/** One offered permission option, with the number the bar advertises for it. */
|
|
22
|
+
export interface PermissionChoice {
|
|
23
|
+
/** The key that selects this row, as printed: "1"…"9". */
|
|
24
|
+
key: string;
|
|
25
|
+
optionId: string;
|
|
26
|
+
label: string;
|
|
27
|
+
/** A lasting consequence the label alone does not spell out. */
|
|
28
|
+
note?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Provider text, made safe to draw.
|
|
32
|
+
*
|
|
33
|
+
* Titles and tool inputs arrive from another process and can carry newlines,
|
|
34
|
+
* escapes and ANSI. Collapsing whitespace is not cosmetic here: every row of
|
|
35
|
+
* the bar is one row, so an embedded newline would either be drawn literally or
|
|
36
|
+
* silently shift every row below it.
|
|
37
|
+
*/
|
|
38
|
+
export declare function clean(value: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* The offered options, numbered in the order they were offered.
|
|
41
|
+
*
|
|
42
|
+
* Capped at nine because the key is a single digit and a tenth option would
|
|
43
|
+
* advertise a key that does not exist. The provider's own wording is kept: it
|
|
44
|
+
* is what carries the scope ("…and don't ask again for python3 - commands"),
|
|
45
|
+
* and rewriting it would be guessing at a meaning it already states.
|
|
46
|
+
*/
|
|
47
|
+
export declare function permissionChoices(request: RuntimePermissionRequest): PermissionChoice[];
|
|
48
|
+
/** How many impact rows the bar will draw, so the layout can reserve them. */
|
|
49
|
+
export declare const MAX_IMPACT_ROWS = 2;
|
|
50
|
+
/**
|
|
51
|
+
* What the tool is about to do that its own one-line title does not say.
|
|
52
|
+
*
|
|
53
|
+
* The bar draws `request.title` first: it is the provider's own summary of the
|
|
54
|
+
* call, and for a shell tool it *is* the command. What it cannot say is how much
|
|
55
|
+
* is behind that first line — the old overlay dumped `input` as JSON and clipped
|
|
56
|
+
* it after three rows, which always landed inside the escape sequences, so the
|
|
57
|
+
* one readable thing was the one thing that had already been said. The facts
|
|
58
|
+
* here are what is genuinely additional: the size of the script, the size of the
|
|
59
|
+
* edit.
|
|
60
|
+
*/
|
|
61
|
+
export declare function describeImpact(request: RuntimePermissionRequest, width: number): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Move the highlighted row, wrapping at both ends.
|
|
64
|
+
*
|
|
65
|
+
* `undefined` means nothing is highlighted yet, and that is deliberate: an
|
|
66
|
+
* `enter` on a bar that already points at something would be a one-keystroke
|
|
67
|
+
* grant for a request the user never read. Starting with no selection makes
|
|
68
|
+
* `enter` alone inert, so committing always takes an explicit move first.
|
|
69
|
+
*/
|
|
70
|
+
export declare function moveCursor(cursor: number | undefined, delta: number, total: number): number | undefined;
|
|
71
|
+
/** Jump to a numbered row, or `undefined` when the digit names nothing. */
|
|
72
|
+
export declare function jumpCursor(digit: string, total: number): number | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* The requests queued behind the one on screen, named.
|
|
75
|
+
*
|
|
76
|
+
* "2 pending" alone was the old bar's whole account of the others, which told
|
|
77
|
+
* the user that something else was waiting without telling them what — so the
|
|
78
|
+
* count could not be acted on until the front one was decided. Three is the
|
|
79
|
+
* most that stays readable on one row; the rest collapse into a count.
|
|
80
|
+
*/
|
|
81
|
+
export declare function waitingLine(rest: readonly RuntimePermissionRequest[]): string | undefined;
|
|
82
|
+
/** Rows the permission bar occupies: header, title, impact, options, hint. */
|
|
83
|
+
export declare function permissionBarRows(request: RuntimePermissionRequest, width: number, rest?: readonly RuntimePermissionRequest[]): number;
|
|
84
|
+
export type FormKind = "choice" | "multi" | "boolean" | "number" | "text";
|
|
85
|
+
export interface FormChoice {
|
|
86
|
+
value: string;
|
|
87
|
+
title: string;
|
|
88
|
+
description?: string;
|
|
89
|
+
}
|
|
90
|
+
export interface FormBounds {
|
|
91
|
+
min?: number;
|
|
92
|
+
max?: number;
|
|
93
|
+
minItems?: number;
|
|
94
|
+
maxItems?: number;
|
|
95
|
+
minLength?: number;
|
|
96
|
+
maxLength?: number;
|
|
97
|
+
}
|
|
98
|
+
export interface FormField {
|
|
99
|
+
key: string;
|
|
100
|
+
title: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
required: boolean;
|
|
103
|
+
kind: FormKind;
|
|
104
|
+
choices: FormChoice[];
|
|
105
|
+
bounds: FormBounds;
|
|
106
|
+
}
|
|
107
|
+
/** A committed field value. Numbers are held as the text typed, converted on submit. */
|
|
108
|
+
export type FormValue = string | boolean | string[];
|
|
109
|
+
export interface FormState {
|
|
110
|
+
/** Index of the focused field. */
|
|
111
|
+
field: number;
|
|
112
|
+
/** Highlighted choice per field, so moving between fields remembers it. */
|
|
113
|
+
picks: Record<string, number>;
|
|
114
|
+
values: Record<string, FormValue>;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The question's schema as a list of fields to edit.
|
|
118
|
+
*
|
|
119
|
+
* Order is the schema's own: a form that reorders its questions is asking them
|
|
120
|
+
* in a different order than the agent wrote them, and the agent wrote them in
|
|
121
|
+
* the order the answer makes sense in.
|
|
122
|
+
*/
|
|
123
|
+
export declare function formFields(request: RuntimeQuestionRequest): FormField[];
|
|
124
|
+
/** The human form of a field's limits, or `undefined` when it has none. */
|
|
125
|
+
export declare function boundsText(field: FormField): string | undefined;
|
|
126
|
+
export declare function initialForm(): FormState;
|
|
127
|
+
/** The field's value as the row draws it. */
|
|
128
|
+
export declare function formValueText(field: FormField, state: FormState): string;
|
|
129
|
+
/** The highlighted choice index, clamped to the options that exist. */
|
|
130
|
+
export declare function pickFor(state: FormState, field: FormField): number;
|
|
131
|
+
export declare function movePick(state: FormState, field: FormField, delta: number): FormState;
|
|
132
|
+
export declare function jumpPick(state: FormState, field: FormField, digit: string): FormState;
|
|
133
|
+
export declare function toggleChoice(state: FormState, field: FormField): FormState;
|
|
134
|
+
export declare function toggleBoolean(state: FormState, field: FormField): FormState;
|
|
135
|
+
/**
|
|
136
|
+
* Take the text in the prompt as the focused field's value.
|
|
137
|
+
*
|
|
138
|
+
* The prompt *is* the editor for a text field — it already owns the caret, the
|
|
139
|
+
* wrapping and the paste handling — so there is no second buffer to keep in
|
|
140
|
+
* sync. The only cost is that the value has to be lifted out of the prompt
|
|
141
|
+
* before the focus moves away, which is what this does.
|
|
142
|
+
*/
|
|
143
|
+
export declare function withDraft(fields: FormField[], state: FormState, draft: string): FormState;
|
|
144
|
+
/**
|
|
145
|
+
* Move the focus, handing back the text the new field should show.
|
|
146
|
+
*
|
|
147
|
+
* Both halves happen together because they are one operation: a caller that
|
|
148
|
+
* saved the draft but forgot to load the next field's value would silently
|
|
149
|
+
* empty a field the user had already answered.
|
|
150
|
+
*/
|
|
151
|
+
export declare function focusField(fields: FormField[], state: FormState, index: number): {
|
|
152
|
+
state: FormState;
|
|
153
|
+
draft: string;
|
|
154
|
+
};
|
|
155
|
+
/** Commit a highlighted choice into the value, for fields picked rather than typed. */
|
|
156
|
+
export declare function commitField(fields: FormField[], state: FormState): FormState;
|
|
157
|
+
/** The answer to send, with numbers parsed out of the text they were typed as. */
|
|
158
|
+
export declare function formContent(fields: FormField[], state: FormState): Record<string, string | number | boolean | string[]>;
|
|
159
|
+
export declare function formComplete(fields: FormField[], state: FormState): boolean;
|
|
160
|
+
/** Rows a field list may occupy before it scrolls. */
|
|
161
|
+
export declare const MAX_FORM_FIELDS = 6;
|
|
162
|
+
/** Rows the question form occupies: header, message, fields, detail, hint. */
|
|
163
|
+
export declare function questionBarRows(fields: FormField[]): number;
|
|
164
|
+
/** The one interaction on screen, whichever kind it is. */
|
|
165
|
+
export type InteractionLike = {
|
|
166
|
+
kind: "permission";
|
|
167
|
+
request: RuntimePermissionRequest;
|
|
168
|
+
} | {
|
|
169
|
+
kind: "question";
|
|
170
|
+
request: RuntimeQuestionRequest;
|
|
171
|
+
};
|
|
172
|
+
/** Rows the docked bar asks the layout for, whichever request is in front. */
|
|
173
|
+
export declare function interactionRows(interactions: readonly InteractionLike[], width: number): number;
|
package/dist/tui/layout.d.ts
CHANGED
|
@@ -25,6 +25,23 @@ export declare const MAX_RAIL_ROWS = 2;
|
|
|
25
25
|
export declare const MAX_MENU_ROWS = 7;
|
|
26
26
|
/** Queued follow-ups sit above the prompt; more than a few would crowd the stream. */
|
|
27
27
|
export declare const MAX_QUEUE_ROWS = 3;
|
|
28
|
+
/**
|
|
29
|
+
* The most rows the interaction bar is ever allowed to occupy.
|
|
30
|
+
*
|
|
31
|
+
* An agent blocked on a permission or a question is the one thing on screen
|
|
32
|
+
* worth acting on, so its bar is allowed more room than a queue or a palette —
|
|
33
|
+
* but not unlimited: it is docked *beside* the stream, and a bar tall enough to
|
|
34
|
+
* push the stream out would undo the reason it was docked.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MAX_INTERACTION_ROWS = 10;
|
|
37
|
+
/**
|
|
38
|
+
* The fewest rows worth docking the bar for.
|
|
39
|
+
*
|
|
40
|
+
* Header, title, one option and the hint is the least that can be read and acted
|
|
41
|
+
* on. Below that the bar would be a header with no choices under it, which is
|
|
42
|
+
* worse than leaving the stream alone and deciding by command.
|
|
43
|
+
*/
|
|
44
|
+
export declare const MIN_INTERACTION_ROWS = 4;
|
|
28
45
|
export interface LayoutInput {
|
|
29
46
|
columns: number;
|
|
30
47
|
rows: number;
|
|
@@ -36,6 +53,23 @@ export interface LayoutInput {
|
|
|
36
53
|
menuRows: number;
|
|
37
54
|
/** Rows the queued follow-ups are asking for. */
|
|
38
55
|
queueRows?: number;
|
|
56
|
+
/**
|
|
57
|
+
* Rows the interaction bar is asking for.
|
|
58
|
+
*
|
|
59
|
+
* It docks above the prompt rather than replacing the body, so its rows come
|
|
60
|
+
* out of the same budget as every other piece of chrome — an unbudgeted row
|
|
61
|
+
* here would show up as a stream pane that silently loses its last line.
|
|
62
|
+
*/
|
|
63
|
+
interactionRows?: number;
|
|
64
|
+
/**
|
|
65
|
+
* Rows the prompt is asking for.
|
|
66
|
+
*
|
|
67
|
+
* The prompt wraps, so a long input makes it grow — one row per line typed
|
|
68
|
+
* past the terminal's width. Left out, it is the one row it used to be, so a
|
|
69
|
+
* caller that has not measured the text keeps the old budget rather than
|
|
70
|
+
* silently reserving nothing.
|
|
71
|
+
*/
|
|
72
|
+
promptRows?: number;
|
|
39
73
|
/** Whether a command result is waiting to be shown. */
|
|
40
74
|
notice: boolean;
|
|
41
75
|
/** A past turn is open for reading, which costs a banner row. */
|
|
@@ -74,6 +108,7 @@ export interface Layout {
|
|
|
74
108
|
streamWidth: number;
|
|
75
109
|
menuRows: number;
|
|
76
110
|
queueRows: number;
|
|
111
|
+
interactionRows: number;
|
|
77
112
|
}
|
|
78
113
|
/**
|
|
79
114
|
* How many rows the rail occupies given the widths of the cells it draws.
|