@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.
@@ -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
- * The prompt line.
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;
@@ -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.