@multiclawcli/dashboard 0.3.2 → 0.5.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 +66 -2
- package/dist/tui/App.d.ts +10 -0
- package/dist/tui/commands.d.ts +122 -0
- package/dist/tui/components/chrome.d.ts +58 -0
- package/dist/tui/components/overlays.d.ts +64 -0
- package/dist/tui/components/panels.d.ts +72 -0
- package/dist/tui/components/prompt.d.ts +36 -0
- package/dist/tui/format.d.ts +65 -0
- package/dist/tui/history.d.ts +34 -0
- package/dist/tui/index.d.ts +17 -0
- package/dist/tui/layout.d.ts +87 -0
- package/dist/tui/state.d.ts +292 -0
- package/dist/tui/theme.d.ts +66 -0
- package/dist/tui.js +2076 -0
- package/package.json +20 -28
- package/components.json +0 -16
- package/index.html +0 -12
- package/postcss.config.js +0 -3
- package/src/client/App.tsx +0 -96
- package/src/client/components/AgentCard.tsx +0 -60
- package/src/client/components/LogPanel.tsx +0 -43
- package/src/client/components/PipelineBar.tsx +0 -234
- package/src/client/components/ui/badge.tsx +0 -28
- package/src/client/components/ui/button.tsx +0 -43
- package/src/client/components/ui/card.tsx +0 -22
- package/src/client/components/ui/scroll-area.tsx +0 -43
- package/src/client/components/ui/separator.tsx +0 -23
- package/src/client/hooks/useWebSocket.ts +0 -33
- package/src/client/index.css +0 -31
- package/src/client/lib/utils.ts +0 -6
- package/src/client/main.tsx +0 -8
- package/src/client/store/useOrchestrationStore.ts +0 -132
- package/src/server/DashboardServer.ts +0 -56
- package/src/server/WebSocketHub.ts +0 -35
- package/src/server/index.ts +0 -2
- package/tailwind.config.js +0 -50
- package/tsconfig.json +0 -12
- package/tsconfig.server.json +0 -9
- package/vite.config.ts +0 -25
package/README.md
CHANGED
|
@@ -3,9 +3,73 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@multiclawcli/dashboard)
|
|
4
4
|
[](https://github.com/shipengqi/multiclaw)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Interactive terminal console for [multiclaw](https://github.com/shipengqi/multiclaw) — the multi-agent
|
|
7
|
+
orchestration CLI.
|
|
7
8
|
|
|
8
|
-
This package provides the
|
|
9
|
+
This package provides the Ink-based TUI used internally by the `multiclaw` CLI when it is run with
|
|
10
|
+
no subcommand. It is not intended for direct use; install
|
|
11
|
+
[`multiclaw`](https://www.npmjs.com/package/multiclaw) instead.
|
|
12
|
+
|
|
13
|
+
## What it exports
|
|
14
|
+
|
|
15
|
+
| Export | Purpose |
|
|
16
|
+
|--------|---------|
|
|
17
|
+
| `./tui` → `startTui({ config, onTurnStart })` | Render the console and drive the turn loop |
|
|
18
|
+
| `./tui` → `tuiReducer`, `reduceEvent`, `initialState`, `appendLog`, `stripAnsi` | Pure state machine |
|
|
19
|
+
| `./tui` → `TurnView`, `TuiState`, `MAX_HISTORY` | The turn/console split that history is built on |
|
|
20
|
+
| `./tui` → `computeLayout` | Terminal geometry: which regions fit at a given size |
|
|
21
|
+
|
|
22
|
+
`reduceEvent` is a pure `(state, event) => state` function over core's `MultiClawEvent`, and
|
|
23
|
+
`computeLayout` is pure arithmetic over `(columns, rows, …)`. Both the state layer and the responsive
|
|
24
|
+
rules are therefore testable without a terminal.
|
|
25
|
+
|
|
26
|
+
## One turn, and the console around it
|
|
27
|
+
|
|
28
|
+
`TuiState` is split in two, and the split is what makes history possible:
|
|
29
|
+
|
|
30
|
+
| Type | Holds |
|
|
31
|
+
|------|-------|
|
|
32
|
+
| `TurnView` | Everything that describes *a turn*: agents, stages, focus, scroll, phase, reply |
|
|
33
|
+
| `TuiState extends TurnView` | The above, for the live turn, plus `name`, `history` and `view` |
|
|
34
|
+
|
|
35
|
+
A finished turn is just a `TurnView` kept in `history`, and every pane takes a `TurnView` — so a past
|
|
36
|
+
turn renders through exactly the same components as the live one. `view` is a *detached* copy of the
|
|
37
|
+
turn being read: `mapShown` routes `tab`, scroll and `/clear` to `view` when one is open, so browsing
|
|
38
|
+
cannot move the focus out from under a run in progress, and cannot rewrite history either.
|
|
39
|
+
|
|
40
|
+
## Source layout
|
|
41
|
+
|
|
42
|
+
| Path | Holds |
|
|
43
|
+
|------|-------|
|
|
44
|
+
| `src/tui/state.ts` | The reducer, the roster seed, the log window, the live clocks, the turn history |
|
|
45
|
+
| `src/tui/layout.ts` | Row budget and breakpoints |
|
|
46
|
+
| `src/tui/format.ts` | Duration, truncation and padding helpers |
|
|
47
|
+
| `src/tui/commands.ts` | The `/` command palette model |
|
|
48
|
+
| `src/tui/theme.ts` | Colour, glyph and spinner tokens |
|
|
49
|
+
| `src/tui/components/` | Ink components, grouped by role |
|
|
50
|
+
| `src/tui/App.tsx` | Keymap and wiring |
|
|
51
|
+
|
|
52
|
+
Coverage is scoped to the four decision-carrying modules (`state`, `layout`, `format`, `commands`).
|
|
53
|
+
The Ink components are render glue, so they are covered by frame assertions in `components.test.tsx`
|
|
54
|
+
and `App.test.tsx` rather than by line counts.
|
|
55
|
+
|
|
56
|
+
## Notes
|
|
57
|
+
|
|
58
|
+
- The console requires a TTY. Without one, `startTui` throws and the CLI points at `multiclaw run`.
|
|
59
|
+
- Rendering happens on the alternate screen buffer, which is always restored on exit.
|
|
60
|
+
- Per-agent log buffers are capped (tail window) so a long-running agent cannot exhaust memory.
|
|
61
|
+
- The prompt holds focus at all times, so only `?` and `/` are reserved as bare keys, and only while
|
|
62
|
+
the line is empty *and* the console is idle. Everything else is a named key or an ordinary
|
|
63
|
+
character. Mid-run `?` therefore types a literal `?`, because that is what the user is doing.
|
|
64
|
+
- The prompt stays live while a turn runs. Submitting queues the text, and the queue starts as the
|
|
65
|
+
next turn once the console is idle — `App.tsx` holds it in a small array, `QueueStrip` renders it,
|
|
66
|
+
and `computeLayout` charges its rows to the body.
|
|
67
|
+
- Every turn runs against the same `workDir`. Only the logs are per-run
|
|
68
|
+
(`.multiclaw/runs/run-<stamp>/logs/`), which is what lets a follow-up build on the last turn.
|
|
69
|
+
- The hint bar only lists keys that do something *right now*. A turn that ended in a reply takes the
|
|
70
|
+
body, so there is no roster to switch between and `tab` is left out rather than promised.
|
|
71
|
+
- The package name is historical: it previously shipped a React web dashboard. Renaming it would
|
|
72
|
+
invalidate the npm trusted-publisher configuration, so the name stays.
|
|
9
73
|
|
|
10
74
|
## Documentation
|
|
11
75
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EventBus, MultiClawConfig } from "@multiclawcli/core";
|
|
2
|
+
export interface AppProps {
|
|
3
|
+
config: MultiClawConfig;
|
|
4
|
+
/**
|
|
5
|
+
* Called once per turn, after that turn's orchestrator exists. The CLI uses it
|
|
6
|
+
* to attach a file log writer without the TUI depending on the CLI package.
|
|
7
|
+
*/
|
|
8
|
+
onTurnStart?: (eventBus: EventBus, logDir: string) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function App({ config, onTurnStart }: AppProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/` command palette.
|
|
3
|
+
*
|
|
4
|
+
* Slash commands are how every agent CLI in this space exposes operations that
|
|
5
|
+
* are not worth a keybinding. They also solve the console's hardest input
|
|
6
|
+
* problem: a bare key cannot be both "type this" and "do that", but a *prefix*
|
|
7
|
+
* can, because the user has already committed to a mode by typing it.
|
|
8
|
+
*/
|
|
9
|
+
/** When a command may be run. */
|
|
10
|
+
export type Availability = "always" | "running" | "idle";
|
|
11
|
+
export interface CommandSpec {
|
|
12
|
+
/** Without the leading slash. */
|
|
13
|
+
name: string;
|
|
14
|
+
/** Argument hint shown next to the name, e.g. `<agent>`. */
|
|
15
|
+
args?: string;
|
|
16
|
+
description: string;
|
|
17
|
+
availability: Availability;
|
|
18
|
+
/**
|
|
19
|
+
* What this command's first argument completes against, if anything.
|
|
20
|
+
*
|
|
21
|
+
* Declared as data rather than matched on the name in `App`, so "this command
|
|
22
|
+
* takes a completer" is one fact in one place: a second such command needs no
|
|
23
|
+
* edit to the console's key handling.
|
|
24
|
+
*/
|
|
25
|
+
completes?: "agent";
|
|
26
|
+
}
|
|
27
|
+
export declare const COMMANDS: readonly CommandSpec[];
|
|
28
|
+
export interface ParsedCommand {
|
|
29
|
+
name: string;
|
|
30
|
+
args: string[];
|
|
31
|
+
}
|
|
32
|
+
/** Commands that make sense in the current phase. */
|
|
33
|
+
export declare function availableCommands(running: boolean): CommandSpec[];
|
|
34
|
+
/**
|
|
35
|
+
* Whether a command may run in the current phase.
|
|
36
|
+
*
|
|
37
|
+
* `availableCommands` decides what the palette *offers*; this is the same rule
|
|
38
|
+
* applied to *running*, because typing a command out bypasses the palette
|
|
39
|
+
* entirely. An unknown name is not "unavailable" — it is let through so the
|
|
40
|
+
* caller's `default:` branch can report it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function canRun(name: string, running: boolean): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Why a command may not run in the current phase, or `undefined` when it may.
|
|
45
|
+
*
|
|
46
|
+
* The mirror of {@link canRun}, and the message names the phase the command
|
|
47
|
+
* *wants* rather than just saying it is unavailable: the two directions mean
|
|
48
|
+
* different things to the user. A mid-run `/replay` has to read as "wait for the
|
|
49
|
+
* turn to finish", while an idle `/cancel` has to read as "this only applies to
|
|
50
|
+
* a running turn" — one string for both would leave the user without the fact
|
|
51
|
+
* they need to act on. An unknown name has no reason; it is let through so the
|
|
52
|
+
* caller's `default:` branch can report it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function unavailableReason(name: string, running: boolean): string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Commands matching what has been typed after the slash.
|
|
57
|
+
*
|
|
58
|
+
* Name prefixes outrank description hits: someone typing `/ag` wants `/agents`,
|
|
59
|
+
* not whichever command happens to mention "agent" in its blurb.
|
|
60
|
+
*/
|
|
61
|
+
export declare function matchCommands(query: string, running: boolean): CommandSpec[];
|
|
62
|
+
/** True once the line has committed to command mode. */
|
|
63
|
+
export declare function isCommandLine(text: string): boolean;
|
|
64
|
+
/**
|
|
65
|
+
* The partial name the palette should filter on, or `undefined` when the
|
|
66
|
+
* palette should stay closed.
|
|
67
|
+
*
|
|
68
|
+
* It closes as soon as a space is typed: at that point the user is writing
|
|
69
|
+
* arguments, and a list that keeps re-filtering under their cursor is noise.
|
|
70
|
+
*/
|
|
71
|
+
export declare function menuQuery(text: string): string | undefined;
|
|
72
|
+
/** Split a submitted line into a command name and its arguments. */
|
|
73
|
+
export declare function parseCommand(text: string): ParsedCommand | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* One row of the palette, whether it lists commands or their arguments.
|
|
76
|
+
*
|
|
77
|
+
* The label carries the leading slash (commands) or is a bare id (arguments),
|
|
78
|
+
* so the renderer draws one shape for both panels and never needs to know which
|
|
79
|
+
* it is showing.
|
|
80
|
+
*/
|
|
81
|
+
export interface PaletteRow {
|
|
82
|
+
key: string;
|
|
83
|
+
label: string;
|
|
84
|
+
description: string;
|
|
85
|
+
}
|
|
86
|
+
/** The palette rows for a command list, in the order given. */
|
|
87
|
+
export declare function paletteRows(commands: readonly CommandSpec[]): PaletteRow[];
|
|
88
|
+
/**
|
|
89
|
+
* The command whose first argument is being typed, and what has been typed so
|
|
90
|
+
* far — or `undefined` when no argument completer applies.
|
|
91
|
+
*
|
|
92
|
+
* The mirror image of {@link menuQuery}: that one closes the moment a space is
|
|
93
|
+
* typed, this one opens there. They are mutually exclusive, so the two palettes
|
|
94
|
+
* can never be open at once.
|
|
95
|
+
*
|
|
96
|
+
* Only the first argument is recognised, which is all any current command needs
|
|
97
|
+
* — a command with two completable arguments would extend this, not the caller.
|
|
98
|
+
*/
|
|
99
|
+
export declare function argQuery(text: string): {
|
|
100
|
+
command: CommandSpec;
|
|
101
|
+
partial: string;
|
|
102
|
+
} | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Agent ids matching a partial id, case-insensitively, capped at `limit`.
|
|
105
|
+
*
|
|
106
|
+
* An empty partial lists the whole roster (up to the cap), so `/focus ` alone is
|
|
107
|
+
* a way to see who is available rather than an empty panel.
|
|
108
|
+
*/
|
|
109
|
+
export declare function completeAgents(partial: string, ids: readonly string[], limit?: number): string[];
|
|
110
|
+
/**
|
|
111
|
+
* Turn a typed agent id into the id it names, or the reason it does not.
|
|
112
|
+
*
|
|
113
|
+
* An exact id always wins; otherwise a unique prefix resolves, and an ambiguous
|
|
114
|
+
* prefix reports every candidate so the user can see what to disambiguate
|
|
115
|
+
* between. `/focus` and `/retry` share this so the two commands accept ids the
|
|
116
|
+
* same way.
|
|
117
|
+
*/
|
|
118
|
+
export declare function resolveAgentId(input: string, ids: readonly string[]): {
|
|
119
|
+
id: string;
|
|
120
|
+
} | {
|
|
121
|
+
error: string;
|
|
122
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { TurnView } from "../state";
|
|
3
|
+
/**
|
|
4
|
+
* Identity on the left, live context on the right — the same split htop uses.
|
|
5
|
+
*
|
|
6
|
+
* Chrome never shrinks. When the column overflows, Yoga squeezes whichever child
|
|
7
|
+
* it can, and a header that silently disappears is worse than a clipped log.
|
|
8
|
+
*/
|
|
9
|
+
export declare function Header({ name, right }: {
|
|
10
|
+
name: string;
|
|
11
|
+
right: string;
|
|
12
|
+
}): import("react").JSX.Element;
|
|
13
|
+
/** A horizontal rule. Cheap separation without spending two rows on a box border. */
|
|
14
|
+
export declare function Rule({ width }: {
|
|
15
|
+
width: number;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* The rendered width of each cell the rail draws, in rail order.
|
|
19
|
+
*
|
|
20
|
+
* The rail wraps, so its height is a function of its content — which means the
|
|
21
|
+
* row budget has to be able to see this before anything is rendered. Keeping the
|
|
22
|
+
* measurement next to the component is what stops the two drifting apart: if the
|
|
23
|
+
* rail changes shape, this is the one other place that needs the same change.
|
|
24
|
+
*/
|
|
25
|
+
export declare function railCellWidths(turn: TurnView): number[];
|
|
26
|
+
/**
|
|
27
|
+
* The team's shape at a glance: who runs before the pipeline, then each stage in
|
|
28
|
+
* order.
|
|
29
|
+
*
|
|
30
|
+
* The `──▸` connector and the ①②③ markers are borrowed from build tools and CI
|
|
31
|
+
* UIs, where "this happens, then that" is already read as a left-to-right flow.
|
|
32
|
+
*/
|
|
33
|
+
export declare function PipelineRail({ turn, spinner }: {
|
|
34
|
+
turn: TurnView;
|
|
35
|
+
spinner: string;
|
|
36
|
+
}): import("react").JSX.Element | null;
|
|
37
|
+
/**
|
|
38
|
+
* The footer.
|
|
39
|
+
*
|
|
40
|
+
* The right side lists only the keys that do something *right now*, which is how
|
|
41
|
+
* a console teaches its own keymap without printing a manual.
|
|
42
|
+
*/
|
|
43
|
+
export declare function HintBar({ status, keys }: {
|
|
44
|
+
status: ReactNode;
|
|
45
|
+
keys: string;
|
|
46
|
+
}): import("react").JSX.Element;
|
|
47
|
+
/**
|
|
48
|
+
* Follow-ups typed while the team was still working.
|
|
49
|
+
*
|
|
50
|
+
* It sits directly above the prompt because that is where the text was typed, and
|
|
51
|
+
* the rows are in run order — the first one is what happens next. The overflow
|
|
52
|
+
* count is spelled out rather than implied, since the whole point of the strip is
|
|
53
|
+
* that nothing the user typed gets silently dropped.
|
|
54
|
+
*/
|
|
55
|
+
export declare function QueueStrip({ items, max }: {
|
|
56
|
+
items: string[];
|
|
57
|
+
max?: number;
|
|
58
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { type TurnView } from "../state";
|
|
2
|
+
interface HelpGroup {
|
|
3
|
+
title: string;
|
|
4
|
+
rows: Array<[keys: string, description: string]>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Grouped the way the user meets them: what the prompt does, then the panes,
|
|
8
|
+
* then the command layer.
|
|
9
|
+
*
|
|
10
|
+
* The command layer lists only keys that are *not* slash commands — `/`, `?`,
|
|
11
|
+
* `ctrl+c` — plus a single derived line counting the commands. It deliberately
|
|
12
|
+
* does not enumerate them: the list would drift from `COMMANDS` the moment a
|
|
13
|
+
* command is added, and it would not fit (see `helpRows`). The `/` palette is
|
|
14
|
+
* built from `COMMANDS` itself, so it can never drift, and that is where the
|
|
15
|
+
* enumeration belongs.
|
|
16
|
+
*/
|
|
17
|
+
export declare const HELP_GROUPS: readonly HelpGroup[];
|
|
18
|
+
/** How many turns fit in a body of `maxRows`. Never fewer than one. */
|
|
19
|
+
export declare function historyCapacity(maxRows: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* The first item to draw, given the one the cursor is on.
|
|
22
|
+
*
|
|
23
|
+
* A generic list window — nothing here is specific to history — so the command
|
|
24
|
+
* palette uses it too. The list can be longer than the body, so without this the
|
|
25
|
+
* cursor walks off the bottom and `enter` opens a row the user cannot see —
|
|
26
|
+
* which is exactly when the list is worth opening.
|
|
27
|
+
*/
|
|
28
|
+
export declare function listWindowStart(selected: number, total: number, capacity: number): number;
|
|
29
|
+
/** Rows the help screen occupies, with or without the gaps between groups. */
|
|
30
|
+
export declare function helpRows(compact?: boolean): number;
|
|
31
|
+
/**
|
|
32
|
+
* The `?` overlay. Replaces the body rather than floating, which keeps it legible.
|
|
33
|
+
*
|
|
34
|
+
* `flexShrink={0}` so a body smaller than the content clips it rather than
|
|
35
|
+
* folding the rows into each other — the body does the clipping, and overlap is
|
|
36
|
+
* strictly worse than a missing row.
|
|
37
|
+
*
|
|
38
|
+
* When even the compact form does not fit, what gets clipped is the bottom, so
|
|
39
|
+
* the spacing between groups is the first thing to go: on a 24-row terminal the
|
|
40
|
+
* difference is whether "esc or ? to close" survives.
|
|
41
|
+
*/
|
|
42
|
+
export declare function HelpOverlay({ width, maxRows }: {
|
|
43
|
+
width: number;
|
|
44
|
+
maxRows: number;
|
|
45
|
+
}): import("react").JSX.Element;
|
|
46
|
+
/**
|
|
47
|
+
* The finished turns, newest first.
|
|
48
|
+
*
|
|
49
|
+
* Two rows per turn on purpose: the requirement is the thing a reader is
|
|
50
|
+
* scanning for, and folding it onto the same line as the outcome would truncate
|
|
51
|
+
* exactly the part that identifies the turn.
|
|
52
|
+
*
|
|
53
|
+
* Only turns a newer turn has superseded are listed — the one being run is
|
|
54
|
+
* already on screen, so listing it would offer to show the user what they are
|
|
55
|
+
* looking at.
|
|
56
|
+
*/
|
|
57
|
+
export declare function HistoryOverlay({ turns, selected, width, maxRows, }: {
|
|
58
|
+
turns: TurnView[];
|
|
59
|
+
selected: number;
|
|
60
|
+
width: number;
|
|
61
|
+
/** Rows available in the body. The list is windowed against this. */
|
|
62
|
+
maxRows: number;
|
|
63
|
+
}): import("react").JSX.Element;
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { LeaderMessage } from "@multiclawcli/core";
|
|
2
|
+
import { type TurnView } from "../state";
|
|
3
|
+
/** The roster: one row per agent, with the focused one called out by a caret. */
|
|
4
|
+
export declare function TeamPanel({ turn, width, now, spinner, }: {
|
|
5
|
+
turn: TurnView;
|
|
6
|
+
width: number;
|
|
7
|
+
now: number;
|
|
8
|
+
spinner: string;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
/**
|
|
11
|
+
* A one-column vertical rule between the two panes.
|
|
12
|
+
*
|
|
13
|
+
* Drawn as a left border rather than a column of `│` characters: the border is
|
|
14
|
+
* laid out by Yoga and always matches the row's height, whereas a hand-counted
|
|
15
|
+
* column of glyphs overflows into the footer the moment the two disagree.
|
|
16
|
+
*/
|
|
17
|
+
export declare function Divider(): import("react").JSX.Element;
|
|
18
|
+
/**
|
|
19
|
+
* The focused agent's output.
|
|
20
|
+
*
|
|
21
|
+
* The header carries the three things a reader needs before the text: whose
|
|
22
|
+
* output this is, what it is running on, and whether the pane is still tailing
|
|
23
|
+
* or parked. Output itself is rendered at normal brightness — the chrome is what
|
|
24
|
+
* should recede, not the agent's work.
|
|
25
|
+
*/
|
|
26
|
+
export declare function StreamPanel({ turn, height, width, now, spinner, }: {
|
|
27
|
+
turn: TurnView;
|
|
28
|
+
height: number;
|
|
29
|
+
width: number;
|
|
30
|
+
now: number;
|
|
31
|
+
spinner: string;
|
|
32
|
+
}): import("react").JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* A turn the leader answered instead of planning.
|
|
35
|
+
*
|
|
36
|
+
* It takes the body for the same reason the welcome screen does: there is no
|
|
37
|
+
* pipeline to watch, and the words are the entire result. Splitting the space
|
|
38
|
+
* with two panes that have nothing to say would only shrink the one thing worth
|
|
39
|
+
* reading. The rail above still names the leader, so the source is not lost.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ReplyPanel({ reply, width }: {
|
|
42
|
+
reply: LeaderMessage;
|
|
43
|
+
width: number;
|
|
44
|
+
}): import("react").JSX.Element;
|
|
45
|
+
/**
|
|
46
|
+
* A turn that stopped with an error, shown in full.
|
|
47
|
+
*
|
|
48
|
+
* The status line has one row and truncates, which is the wrong shape for the
|
|
49
|
+
* errors that matter here: a jiti load failure or an ACP protocol error is
|
|
50
|
+
* several lines, and squeezing it to one leaves the user nothing to act on. So
|
|
51
|
+
* the error takes the body, the same way a reply does, and lets Ink wrap it.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ErrorPanel({ message, width }: {
|
|
54
|
+
message: string;
|
|
55
|
+
width: number;
|
|
56
|
+
}): import("react").JSX.Element;
|
|
57
|
+
/**
|
|
58
|
+
* The first screen.
|
|
59
|
+
*
|
|
60
|
+
* A console that opens to a blank pane wastes the one moment where the user has
|
|
61
|
+
* nothing else to read. The roster is already in the config — including the
|
|
62
|
+
* `description` of what each agent is for — so the welcome screen can say who is
|
|
63
|
+
* on the team and what each of them does.
|
|
64
|
+
*
|
|
65
|
+
* It marks the focused agent with the same `›` caret the team panel uses. Without
|
|
66
|
+
* it, `tab` looked broken here: the hint bar advertised `tab switch` while the
|
|
67
|
+
* one roster on screen ignored the focus entirely.
|
|
68
|
+
*/
|
|
69
|
+
export declare function WelcomePanel({ turn, width }: {
|
|
70
|
+
turn: TurnView;
|
|
71
|
+
width: number;
|
|
72
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PaletteRow } from "../commands";
|
|
2
|
+
/**
|
|
3
|
+
* The prompt line.
|
|
4
|
+
*
|
|
5
|
+
* The terminal's own cursor is hidden while the console owns the screen, so the
|
|
6
|
+
* caret has to be drawn. It sits *between* the text before and after it rather
|
|
7
|
+
* than always at the end — that is what makes arrow-key editing legible.
|
|
8
|
+
*
|
|
9
|
+
* The caret cell is always exactly one column wide, in both blink phases. That
|
|
10
|
+
* matters on an empty line: the ghost text starts where the caret is, so a
|
|
11
|
+
* caret that *added* a cell while visible would shove the placeholder one
|
|
12
|
+
* column sideways twice a second. Instead the caret is painted *onto* the ghost
|
|
13
|
+
* text's first character, which is what a real block cursor does.
|
|
14
|
+
*/
|
|
15
|
+
export declare function Prompt({ value, cursor, caretVisible, placeholder, }: {
|
|
16
|
+
value: string;
|
|
17
|
+
cursor: number;
|
|
18
|
+
caretVisible: boolean;
|
|
19
|
+
placeholder: string;
|
|
20
|
+
}): import("react").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* The palette, rendered directly above the prompt.
|
|
23
|
+
*
|
|
24
|
+
* Keeping it attached to the line being typed — rather than in a floating box —
|
|
25
|
+
* means the filtered list and the query are always read together, which is how
|
|
26
|
+
* fzf-style pickers work.
|
|
27
|
+
*
|
|
28
|
+
* It takes rows rather than commands so the command list and an argument
|
|
29
|
+
* completer (see `argQuery`) draw through one path: both reduce to a key, a
|
|
30
|
+
* label and a description, and the label already carries whatever prefix it
|
|
31
|
+
* needs.
|
|
32
|
+
*/
|
|
33
|
+
export declare function CommandMenu({ rows, selected }: {
|
|
34
|
+
rows: PaletteRow[];
|
|
35
|
+
selected: number;
|
|
36
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Truncate to `max` columns, ending in an ellipsis when it does not fit.
|
|
3
|
+
*
|
|
4
|
+
* Counts code units rather than grapheme clusters, which is what Ink's own
|
|
5
|
+
* `wrap="truncate-end"` does; matching it keeps hand-truncated and
|
|
6
|
+
* Ink-truncated strings the same width.
|
|
7
|
+
*/
|
|
8
|
+
export declare function truncate(text: string, max: number): string;
|
|
9
|
+
/**
|
|
10
|
+
* How many terminal columns a string occupies.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately not `text.length`: a CJK character is one code unit and *two*
|
|
13
|
+
* columns, so counting code units would size a Chinese line at half the pane
|
|
14
|
+
* and hand it to Ink to cut — the same truncation bug in a new place. Emoji and
|
|
15
|
+
* combining marks are wrong for the same reason.
|
|
16
|
+
*/
|
|
17
|
+
export declare function displayWidth(text: string): number;
|
|
18
|
+
/**
|
|
19
|
+
* Break one line of text into the rows a terminal will actually spend on it.
|
|
20
|
+
*
|
|
21
|
+
* Word-wrapped, because the stream pane carries an agent's prose as often as it
|
|
22
|
+
* carries log lines, and breaking mid-word would make the leader's reasoning
|
|
23
|
+
* harder to read than the truncation this replaced. A word with no space inside
|
|
24
|
+
* it — a path, a hash, a minified JSON blob — has nowhere to break, so it is
|
|
25
|
+
* split by width instead.
|
|
26
|
+
*
|
|
27
|
+
* Scripts that use wide characters get the rule they actually have: they put no
|
|
28
|
+
* spaces between words, so a whole Chinese sentence arrives as one "word" and
|
|
29
|
+
* would be pushed onto the next row whole, leaving the row before it mostly
|
|
30
|
+
* empty. They may be broken between any two characters instead, so they fill
|
|
31
|
+
* what is left. Narrow runs — English words, identifiers, numbers — are still
|
|
32
|
+
* atomic, which is what keeps this a no-op for output that has no CJK in it.
|
|
33
|
+
*
|
|
34
|
+
* Every returned row is at most `width` columns, which is what lets the caller
|
|
35
|
+
* count rows and trust the count.
|
|
36
|
+
*/
|
|
37
|
+
export declare function wrapLine(line: string, width: number): string[];
|
|
38
|
+
/**
|
|
39
|
+
* Human duration, used for every time column in the console.
|
|
40
|
+
*
|
|
41
|
+
* One formatter everywhere is what makes the columns comparable — the eye can
|
|
42
|
+
* scan a stack of durations only if they share a shape.
|
|
43
|
+
*/
|
|
44
|
+
export declare function formatDuration(ms: number): string;
|
|
45
|
+
/** Stage markers read as an ordered sequence at a glance; past twenty, fall back. */
|
|
46
|
+
export declare function circled(n: number): string;
|
|
47
|
+
/** Pad to a fixed column so sibling rows line up. */
|
|
48
|
+
export declare function padRight(text: string, width: number): string;
|
|
49
|
+
/** Elapsed time of a run that is still in flight, or its total once it has ended. */
|
|
50
|
+
export declare function elapsedSince(startedAt: number | undefined, now: number): number | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* A compact token count, for a column that must stay narrow.
|
|
53
|
+
*
|
|
54
|
+
* `45200 → "45.2k"`, `950 → "950"`, `1_200_000 → "1.2M"`. Below a thousand the
|
|
55
|
+
* exact number is short enough to show; above it the decimal is what the eye
|
|
56
|
+
* reads, so the digits past it are dropped.
|
|
57
|
+
*/
|
|
58
|
+
export declare function formatTokens(n: number): string;
|
|
59
|
+
/**
|
|
60
|
+
* A USD cost, at the precision a session total is read at.
|
|
61
|
+
*
|
|
62
|
+
* `0.42 → "$0.42"`, `0 → "$0.00"`, and anything under a cent as `"<$0.01"` rather
|
|
63
|
+
* than rounding it away to "$0.00" — a real but tiny cost must not read as free.
|
|
64
|
+
*/
|
|
65
|
+
export declare function formatCost(usd: number): string;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type TurnView } from "./state";
|
|
2
|
+
/** Directory, under a workspace's `.multiclaw/`, that holds console state. */
|
|
3
|
+
export declare const CONSOLE_DIR = "console";
|
|
4
|
+
/** The finished-turn record the console keeps across sessions. */
|
|
5
|
+
export declare const HISTORY_FILE = "history.json";
|
|
6
|
+
/** Where exported turn markdown files are written by default. */
|
|
7
|
+
export declare const EXPORT_DIR = "exports";
|
|
8
|
+
/** Where the console keeps its turn history for a workspace. */
|
|
9
|
+
export declare function historyPath(workDir: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Read the workspace's turn history.
|
|
12
|
+
*
|
|
13
|
+
* Synchronous on purpose: it runs once, inside the reducer's lazy initialiser,
|
|
14
|
+
* before the first frame. An async load would race the user's first keystroke
|
|
15
|
+
* and force a "history is still arriving" state the console does not need.
|
|
16
|
+
*
|
|
17
|
+
* Never throws. A missing directory, malformed JSON, or a value of the wrong
|
|
18
|
+
* shape all mean "no history": the console can always start from empty, and
|
|
19
|
+
* crashing on startup because a side file got corrupted would be far worse.
|
|
20
|
+
*/
|
|
21
|
+
export declare function loadHistory(workDir: string): TurnView[];
|
|
22
|
+
/**
|
|
23
|
+
* Write the workspace's turn history.
|
|
24
|
+
*
|
|
25
|
+
* Written to a temp file and renamed into place: `rename` is atomic within a
|
|
26
|
+
* filesystem, so a crash mid-write leaves the *previous* file intact. Writing
|
|
27
|
+
* `history.json` directly would instead leave half a JSON document, and since
|
|
28
|
+
* `loadHistory` can only drop a file it cannot parse, that would cost the user
|
|
29
|
+
* *all* of their history rather than the last write.
|
|
30
|
+
*/
|
|
31
|
+
export declare function saveHistory(workDir: string, turns: readonly TurnView[]): Promise<void>;
|
|
32
|
+
export declare function exportPath(workDir: string, turn: number): string;
|
|
33
|
+
export declare function exportTurn(turn: TurnView, filePath: string): Promise<void>;
|
|
34
|
+
export declare function formatTurnMarkdown(turn: TurnView): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EventBus, MultiClawConfig } from "@multiclawcli/core";
|
|
2
|
+
export type { AppProps } from "./App";
|
|
3
|
+
export * from "./layout";
|
|
4
|
+
export * from "./state";
|
|
5
|
+
export interface TuiOptions {
|
|
6
|
+
config: MultiClawConfig;
|
|
7
|
+
/** Attach per-turn reporters (e.g. a file log writer) here. */
|
|
8
|
+
onTurnStart?: (eventBus: EventBus, logDir: string) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Render the interactive team console.
|
|
12
|
+
*
|
|
13
|
+
* Runs on the alternate screen buffer so the app owns the viewport and the
|
|
14
|
+
* user's scrollback survives. The buffer is always restored — including when
|
|
15
|
+
* rendering throws — otherwise the terminal is left unusable.
|
|
16
|
+
*/
|
|
17
|
+
export declare function startTui(options: TuiOptions): Promise<void>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal geometry.
|
|
3
|
+
*
|
|
4
|
+
* Kept pure and separate from the components because responsive rules are the
|
|
5
|
+
* part of a TUI that is easiest to get subtly wrong and hardest to eyeball. Every
|
|
6
|
+
* row the console spends is budgeted here, so the arithmetic is testable rather
|
|
7
|
+
* than buried in JSX.
|
|
8
|
+
*/
|
|
9
|
+
export declare const MIN_COLUMNS = 40;
|
|
10
|
+
export declare const MIN_ROWS = 10;
|
|
11
|
+
/** Below this the roster is dropped so the stream keeps a usable width. */
|
|
12
|
+
export declare const COMPACT_COLUMNS = 72;
|
|
13
|
+
/** The rail costs a row; it only earns one when there is vertical room. */
|
|
14
|
+
export declare const RAIL_MIN_ROWS = 16;
|
|
15
|
+
/**
|
|
16
|
+
* The most rows the pipeline rail is ever allowed to occupy.
|
|
17
|
+
*
|
|
18
|
+
* The rail wraps, so its height depends on how many agents the turn has and how
|
|
19
|
+
* wide the terminal is. Without a cap it could eat the whole screen; with one,
|
|
20
|
+
* the budget stays honest and anything past it is clipped rather than silently
|
|
21
|
+
* swallowed by the body.
|
|
22
|
+
*/
|
|
23
|
+
export declare const MAX_RAIL_ROWS = 2;
|
|
24
|
+
/** Enough to see the palette is a list; more would push the stream off screen. */
|
|
25
|
+
export declare const MAX_MENU_ROWS = 7;
|
|
26
|
+
/** Queued follow-ups sit above the prompt; more than a few would crowd the stream. */
|
|
27
|
+
export declare const MAX_QUEUE_ROWS = 3;
|
|
28
|
+
export interface LayoutInput {
|
|
29
|
+
columns: number;
|
|
30
|
+
rows: number;
|
|
31
|
+
/** Zero means no turn has run yet, which swaps the body for the welcome screen. */
|
|
32
|
+
turn: number;
|
|
33
|
+
/** A modal owns the body — help or the history list. */
|
|
34
|
+
overlay: boolean;
|
|
35
|
+
/** Rows the command palette is asking for. */
|
|
36
|
+
menuRows: number;
|
|
37
|
+
/** Rows the queued follow-ups are asking for. */
|
|
38
|
+
queueRows?: number;
|
|
39
|
+
/** Whether a command result is waiting to be shown. */
|
|
40
|
+
notice: boolean;
|
|
41
|
+
/** A past turn is open for reading, which costs a banner row. */
|
|
42
|
+
viewing?: boolean;
|
|
43
|
+
/** Whether there is a leader or any stage to draw. */
|
|
44
|
+
hasRail: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Rendered widths of the rail's cells, widest first is not required — order
|
|
47
|
+
* is what the rail draws. Needed because the rail wraps: its height is a
|
|
48
|
+
* function of the content, so a flat "one row" charge under-budgets it.
|
|
49
|
+
*/
|
|
50
|
+
railCells?: number[];
|
|
51
|
+
}
|
|
52
|
+
export interface Layout {
|
|
53
|
+
columns: number;
|
|
54
|
+
rows: number;
|
|
55
|
+
compact: boolean;
|
|
56
|
+
showTeam: boolean;
|
|
57
|
+
showRail: boolean;
|
|
58
|
+
/** How many rows the rail will actually occupy once it wraps. */
|
|
59
|
+
railRows: number;
|
|
60
|
+
welcome: boolean;
|
|
61
|
+
bodyHeight: number;
|
|
62
|
+
logViewport: number;
|
|
63
|
+
teamWidth: number;
|
|
64
|
+
/**
|
|
65
|
+
* Text columns available to the stream pane's *content* — the roster, the
|
|
66
|
+
* rule between the panes and the stream's own left padding already taken out.
|
|
67
|
+
*
|
|
68
|
+
* The stream pre-wraps its output, so it has to know this before Ink lays
|
|
69
|
+
* anything out; there is no way to measure it back out of Yoga. The arithmetic
|
|
70
|
+
* is therefore deliberate about erring small: a row wrapped a column early
|
|
71
|
+
* costs a few characters of width, while a row wrapped a column late would be
|
|
72
|
+
* cut at the pane edge — the bug this exists to prevent.
|
|
73
|
+
*/
|
|
74
|
+
streamWidth: number;
|
|
75
|
+
menuRows: number;
|
|
76
|
+
queueRows: number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* How many rows the rail occupies given the widths of the cells it draws.
|
|
80
|
+
*
|
|
81
|
+
* Approximate on purpose: Yoga's own wrapping depends on margins and on how the
|
|
82
|
+
* terminal renders wide glyphs, and chasing exactness here is not worth it. The
|
|
83
|
+
* number is capped and the rail is clipped, so an under-count costs the last
|
|
84
|
+
* chip rather than collapsing the layout.
|
|
85
|
+
*/
|
|
86
|
+
export declare function measureRailRows(cells: number[] | undefined, columns: number): number;
|
|
87
|
+
export declare function computeLayout(input: LayoutInput): Layout;
|