@haywoodmarx/agent-pager 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/src/broker/address.d.ts +49 -0
- package/dist/src/broker/address.js +110 -0
- package/dist/src/broker/broker.d.ts +665 -0
- package/dist/src/broker/broker.js +2270 -0
- package/dist/src/broker/database.d.ts +81 -0
- package/dist/src/broker/database.js +69 -0
- package/dist/src/broker/operator-projection.d.ts +291 -0
- package/dist/src/broker/operator-projection.js +722 -0
- package/dist/src/broker/protocol.d.ts +237 -0
- package/dist/src/broker/protocol.js +311 -0
- package/dist/src/broker/schema.d.ts +94 -0
- package/dist/src/broker/schema.js +454 -0
- package/dist/src/broker/snapshot.d.ts +171 -0
- package/dist/src/broker/snapshot.js +78 -0
- package/dist/src/configuration-defaults.d.ts +64 -0
- package/dist/src/configuration-defaults.js +48 -0
- package/dist/src/configuration.d.ts +76 -0
- package/dist/src/configuration.js +156 -0
- package/dist/src/implementations/claude/environment.d.ts +8 -0
- package/dist/src/implementations/claude/environment.js +9 -0
- package/dist/src/implementations/claude/gateway-cli.d.ts +2 -0
- package/dist/src/implementations/claude/gateway-cli.js +136 -0
- package/dist/src/implementations/claude/gateway-identity.d.ts +134 -0
- package/dist/src/implementations/claude/gateway-identity.js +103 -0
- package/dist/src/implementations/claude/gateway.d.ts +148 -0
- package/dist/src/implementations/claude/gateway.js +236 -0
- package/dist/src/implementations/claude/instructions.d.ts +33 -0
- package/dist/src/implementations/claude/instructions.js +64 -0
- package/dist/src/implementations/claude/manifest.d.ts +33 -0
- package/dist/src/implementations/claude/manifest.js +108 -0
- package/dist/src/implementations/claude/plugin-directory.d.ts +80 -0
- package/dist/src/implementations/claude/plugin-directory.js +137 -0
- package/dist/src/implementations/claude/session-continuation.d.ts +25 -0
- package/dist/src/implementations/claude/session-continuation.js +125 -0
- package/dist/src/implementations/claude/session-identity.d.ts +25 -0
- package/dist/src/implementations/claude/session-identity.js +31 -0
- package/dist/src/implementations/claude/session-start-cli.d.ts +2 -0
- package/dist/src/implementations/claude/session-start-cli.js +16 -0
- package/dist/src/implementations/claude/socket-delivery.d.ts +52 -0
- package/dist/src/implementations/claude/socket-delivery.js +159 -0
- package/dist/src/implementations/model.d.ts +162 -0
- package/dist/src/implementations/model.js +1 -0
- package/dist/src/implementations/opencode/adapter.d.ts +71 -0
- package/dist/src/implementations/opencode/adapter.js +94 -0
- package/dist/src/implementations/opencode/environment.d.ts +8 -0
- package/dist/src/implementations/opencode/environment.js +9 -0
- package/dist/src/implementations/opencode/manifest.d.ts +30 -0
- package/dist/src/implementations/opencode/manifest.js +93 -0
- package/dist/src/implementations/opencode/plugin-api.d.ts +120 -0
- package/dist/src/implementations/opencode/plugin-api.js +1 -0
- package/dist/src/implementations/opencode/plugin.d.ts +38 -0
- package/dist/src/implementations/opencode/plugin.js +303 -0
- package/dist/src/implementations/opencode/self-close-tool.d.ts +9 -0
- package/dist/src/implementations/opencode/self-close-tool.js +31 -0
- package/dist/src/implementations/orca/terminal.d.ts +48 -0
- package/dist/src/implementations/orca/terminal.js +154 -0
- package/dist/src/implementations/pi/broker-client.d.ts +56 -0
- package/dist/src/implementations/pi/broker-client.js +156 -0
- package/dist/src/implementations/pi/environment.d.ts +8 -0
- package/dist/src/implementations/pi/environment.js +9 -0
- package/dist/src/implementations/pi/extension.d.ts +57 -0
- package/dist/src/implementations/pi/extension.js +253 -0
- package/dist/src/implementations/pi/host-slots.d.ts +18 -0
- package/dist/src/implementations/pi/host-slots.js +27 -0
- package/dist/src/implementations/pi/manifest.d.ts +11 -0
- package/dist/src/implementations/pi/manifest.js +61 -0
- package/dist/src/implementations/pi/self-close-tool.d.ts +7 -0
- package/dist/src/implementations/pi/self-close-tool.js +27 -0
- package/dist/src/implementations/registry.d.ts +47 -0
- package/dist/src/implementations/registry.js +157 -0
- package/dist/src/implementations/session-key.d.ts +13 -0
- package/dist/src/implementations/session-key.js +19 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.js +26 -0
- package/dist/src/launch/broker-lock.d.ts +257 -0
- package/dist/src/launch/broker-lock.js +644 -0
- package/dist/src/launch/cli.d.ts +37 -0
- package/dist/src/launch/cli.js +317 -0
- package/dist/src/launch/environment.d.ts +45 -0
- package/dist/src/launch/environment.js +48 -0
- package/dist/src/launch/launch.d.ts +235 -0
- package/dist/src/launch/launch.js +223 -0
- package/dist/src/launch/on-demand.d.ts +202 -0
- package/dist/src/launch/on-demand.js +435 -0
- package/dist/src/launch/runtime-composition.d.ts +88 -0
- package/dist/src/launch/runtime-composition.js +253 -0
- package/dist/src/launch/runtime.d.ts +79 -0
- package/dist/src/launch/runtime.js +196 -0
- package/dist/src/launch/service-system.d.ts +11 -0
- package/dist/src/launch/service-system.js +39 -0
- package/dist/src/launch/service.d.ts +173 -0
- package/dist/src/launch/service.js +492 -0
- package/dist/src/launch/skill.d.ts +57 -0
- package/dist/src/launch/skill.js +81 -0
- package/dist/src/launch/terminal.d.ts +26 -0
- package/dist/src/launch/terminal.js +43 -0
- package/dist/src/launch/workspace-branch.d.ts +10 -0
- package/dist/src/launch/workspace-branch.js +29 -0
- package/dist/src/messaging/delivery-client.d.ts +144 -0
- package/dist/src/messaging/delivery-client.js +261 -0
- package/dist/src/messaging/delivery.d.ts +75 -0
- package/dist/src/messaging/delivery.js +22 -0
- package/dist/src/messaging/inbox-socket.d.ts +98 -0
- package/dist/src/messaging/inbox-socket.js +278 -0
- package/dist/src/messaging/mcp.d.ts +53 -0
- package/dist/src/messaging/mcp.js +501 -0
- package/dist/src/messaging/notifications.d.ts +231 -0
- package/dist/src/messaging/notifications.js +489 -0
- package/dist/src/messaging/push-delivery.d.ts +77 -0
- package/dist/src/messaging/push-delivery.js +187 -0
- package/dist/src/messaging/tools.d.ts +11 -0
- package/dist/src/messaging/tools.js +102 -0
- package/dist/src/messaging/wake.d.ts +23 -0
- package/dist/src/messaging/wake.js +39 -0
- package/dist/src/operator/agent-projection.d.ts +102 -0
- package/dist/src/operator/agent-projection.js +249 -0
- package/dist/src/operator/commands.d.ts +50 -0
- package/dist/src/operator/commands.js +199 -0
- package/dist/src/operator/diagnostics.d.ts +21 -0
- package/dist/src/operator/diagnostics.js +22 -0
- package/dist/src/operator/protocol.d.ts +511 -0
- package/dist/src/operator/protocol.js +49 -0
- package/dist/src/operator/repository-system.d.ts +14 -0
- package/dist/src/operator/repository-system.js +150 -0
- package/dist/src/operator/repository.d.ts +79 -0
- package/dist/src/operator/repository.js +72 -0
- package/dist/src/operator/traffic-projection.d.ts +19 -0
- package/dist/src/operator/traffic-projection.js +229 -0
- package/dist/src/operator/transport.d.ts +87 -0
- package/dist/src/operator/transport.js +549 -0
- package/dist/src/operator/update-store.d.ts +53 -0
- package/dist/src/operator/update-store.js +162 -0
- package/dist/src/operator/updates.d.ts +94 -0
- package/dist/src/operator/updates.js +280 -0
- package/dist/src/operator/validation.d.ts +114 -0
- package/dist/src/operator/validation.js +916 -0
- package/dist/src/shared/guards.d.ts +59 -0
- package/dist/src/shared/guards.js +119 -0
- package/dist/src/shared/http.d.ts +30 -0
- package/dist/src/shared/http.js +50 -0
- package/dist/src/shared/row.d.ts +2 -0
- package/dist/src/shared/row.js +1 -0
- package/dist/src/shared/shell.d.ts +6 -0
- package/dist/src/shared/shell.js +8 -0
- package/dist/src/tui/actions.d.ts +166 -0
- package/dist/src/tui/actions.js +253 -0
- package/dist/src/tui/agent-tree.d.ts +125 -0
- package/dist/src/tui/agent-tree.js +663 -0
- package/dist/src/tui/app.d.ts +38 -0
- package/dist/src/tui/app.js +378 -0
- package/dist/src/tui/chrome.d.ts +86 -0
- package/dist/src/tui/chrome.js +85 -0
- package/dist/src/tui/client.d.ts +41 -0
- package/dist/src/tui/client.js +279 -0
- package/dist/src/tui/composition.d.ts +41 -0
- package/dist/src/tui/composition.js +146 -0
- package/dist/src/tui/message-views.d.ts +94 -0
- package/dist/src/tui/message-views.js +515 -0
- package/dist/src/tui/model.d.ts +414 -0
- package/dist/src/tui/model.js +758 -0
- package/dist/src/tui/presentation.d.ts +350 -0
- package/dist/src/tui/presentation.js +541 -0
- package/dist/src/tui/production.d.ts +79 -0
- package/dist/src/tui/production.js +253 -0
- package/dist/src/tui/run-terminal.d.ts +32 -0
- package/dist/src/tui/run-terminal.js +54 -0
- package/dist/src/tui/runtime.d.ts +113 -0
- package/dist/src/tui/runtime.js +189 -0
- package/dist/src/tui/theme.d.ts +49 -0
- package/dist/src/tui/theme.js +55 -0
- package/dist/src/tui/transport.d.ts +31 -0
- package/dist/src/tui/transport.js +255 -0
- package/dist/src/version.d.ts +11 -0
- package/dist/src/version.js +52 -0
- package/package.json +142 -0
- package/skills/pager/SKILL.md +112 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type TuiActionCatalogue } from "./actions.js";
|
|
3
|
+
import type { TuiOperatorModel, TuiOperatorModelState } from "./model.js";
|
|
4
|
+
import { type MessageViewsProps } from "./message-views.js";
|
|
5
|
+
import { type TuiTheme } from "./theme.js";
|
|
6
|
+
/** Explicit production presentation collaborators that do not belong to the operator model. */
|
|
7
|
+
export type OperatorTuiProps = {
|
|
8
|
+
readonly model: TuiOperatorModel;
|
|
9
|
+
readonly theme: Readonly<TuiTheme>;
|
|
10
|
+
readonly body: MessageViewsProps["body"];
|
|
11
|
+
readonly copyFallback: MessageViewsProps["copyFallback"];
|
|
12
|
+
readonly commands: MessageViewsProps["commands"];
|
|
13
|
+
readonly onShutdown: () => void;
|
|
14
|
+
readonly colorEnabled?: boolean;
|
|
15
|
+
/** Everything the actions menu offers. */
|
|
16
|
+
readonly actions: TuiActionCatalogue;
|
|
17
|
+
};
|
|
18
|
+
/** Keys the tree pane advertises whatever its selection. */
|
|
19
|
+
export declare const TREE_FOOTER_KEYS = "m menu l log ? help";
|
|
20
|
+
/** Keys the portrait tree screen advertises whatever its selection. */
|
|
21
|
+
export declare const PORTRAIT_TREE_FOOTER_KEYS = "m menu l log ? help \u2191\u2193 move \u2192 open";
|
|
22
|
+
/**
|
|
23
|
+
* Reduce the shell-owned pane navigation without changing authoritative or component-local state.
|
|
24
|
+
* @param state - Current model state; an open overlay makes every key a no-op here.
|
|
25
|
+
* @param key - Key name as OpenTUI normalises it, such as `tab`, `right` or `return`.
|
|
26
|
+
* @param portrait - Whether the single-pane stack is in use, which makes the message log a focus stop.
|
|
27
|
+
* @returns The next view: the same object for a key navigation does not handle, and possibly a new object with
|
|
28
|
+
* unchanged focus for a movement key that has nowhere to go.
|
|
29
|
+
*/
|
|
30
|
+
export declare function reduceOperatorTuiNavigation(state: TuiOperatorModelState, key: string, portrait: boolean): TuiOperatorModelState["view"];
|
|
31
|
+
/**
|
|
32
|
+
* Compose the complete responsive operator surface over the renderer-independent model.
|
|
33
|
+
*
|
|
34
|
+
* Mounting starts the model and unmounting disposes it.
|
|
35
|
+
* @param properties - Model and presentation collaborators, and the action that ends the process.
|
|
36
|
+
* @returns Root element filling the terminal, bordered, with the wide or portrait layout chosen from its width.
|
|
37
|
+
*/
|
|
38
|
+
export declare function OperatorTui(properties: OperatorTuiProps): ReactNode;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useKeyboard, useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { useEffect, useMemo } from "react";
|
|
4
|
+
import { OPERATOR_PROTOCOL_VERSION } from "../operator/protocol.js";
|
|
5
|
+
import { AgentTree, AgentTreeOverlay, executeAgentTreeResult, reduceAgentTreeKey, selectTuiCollapsibleSelection, } from "./agent-tree.js";
|
|
6
|
+
import { ActionRunOverlay, ActionsOverlay, reduceOperatorActionsKey, skillInstallTerminalSize, } from "./actions.js";
|
|
7
|
+
import { Line, Overlay, PaneBar, Rule, VERTICAL_SCROLL_STYLE, useTuiModel } from "./chrome.js";
|
|
8
|
+
import { AgentTrafficLog, MessageDetail, MessageOverlays, selectNewestTuiTraffic, stepTuiTrafficSelection, } from "./message-views.js";
|
|
9
|
+
import { AGENT_TREE_COLUMNS, sidePaneWidths, MESSAGE_LOG_COLUMNS, PORTRAIT_BREAKPOINT_COLUMNS, agentColor, activeAgentCount, agentLogScope, brokerIndicator, COLLAPSED_LOG_ROWS, describeCommandOutcome, diagnosticColor, fitBarLeading, formatClock, formatDiagnostic, selectedMessage, selectMessageBodyRows, selectTuiTraffic, } from "./presentation.js";
|
|
10
|
+
import { dimTuiTheme } from "./theme.js";
|
|
11
|
+
/* eslint-enable jsdoc/require-jsdoc -- The collaborator declarations end here. */
|
|
12
|
+
/** Rows the wide layout spends on fixed chrome rather than pane content. */
|
|
13
|
+
const WIDE_PANE_CHROME_ROWS = 7;
|
|
14
|
+
/** Rows the portrait layout spends on fixed chrome rather than pane content. */
|
|
15
|
+
const PORTRAIT_PANE_CHROME_ROWS = 5;
|
|
16
|
+
/** Keys the tree pane advertises whatever its selection. */
|
|
17
|
+
export const TREE_FOOTER_KEYS = "m menu l log ? help";
|
|
18
|
+
/** Keys the portrait tree screen advertises whatever its selection. */
|
|
19
|
+
export const PORTRAIT_TREE_FOOTER_KEYS = "m menu l log ? help ↑↓ move → open";
|
|
20
|
+
/** Keys the tree pane adds once a selection can answer them. */
|
|
21
|
+
const AGENT_FOOTER_KEYS = "i details p ping";
|
|
22
|
+
/**
|
|
23
|
+
* Compose the tree footer for the current selection.
|
|
24
|
+
* @param state - Current projection and view state.
|
|
25
|
+
* @param base - Keys the pane advertises whatever is selected.
|
|
26
|
+
* @returns Footer text naming only the keys the selection can answer.
|
|
27
|
+
*/
|
|
28
|
+
function selectTreeFooterKeys(state, base) {
|
|
29
|
+
return state.view.selection?.kind === "agent" ? `${base} ${AGENT_FOOTER_KEYS}` : base;
|
|
30
|
+
}
|
|
31
|
+
/** Keys the portrait traffic screen advertises in its navigation footer. */
|
|
32
|
+
const PORTRAIT_TRAFFIC_FOOTER_KEYS = "← back → open f follow";
|
|
33
|
+
/** Keys the portrait message screen advertises in its navigation footer. */
|
|
34
|
+
const PORTRAIT_MESSAGE_FOOTER_KEYS = "← back o full message";
|
|
35
|
+
/**
|
|
36
|
+
* Report the character rows a pane gives its content at one terminal height.
|
|
37
|
+
* @param height - Current terminal rows.
|
|
38
|
+
* @param portrait - Whether the single-pane stack is in use.
|
|
39
|
+
* @param logRows - Rows the diagnostics log occupies beneath the panes.
|
|
40
|
+
* @returns Rows left for pane content.
|
|
41
|
+
*/
|
|
42
|
+
function selectPaneContentRows(height, portrait, logRows) {
|
|
43
|
+
return Math.max(0, height - (portrait ? PORTRAIT_PANE_CHROME_ROWS : WIDE_PANE_CHROME_ROWS) - logRows);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Report the character columns each pane occupies at one terminal width and the widths the operator dragged.
|
|
47
|
+
* @param width - Current terminal columns.
|
|
48
|
+
* @param portrait - Whether the single-pane stack is in use, where every pane takes the whole inner width.
|
|
49
|
+
* @param view - View state holding the dragged widths of the two side panes.
|
|
50
|
+
* @returns Columns each pane occupies, including its own one-column insets; the agent log takes what the side panes leave.
|
|
51
|
+
*/
|
|
52
|
+
function selectPaneWidths(width, portrait, view) {
|
|
53
|
+
const outer = Math.max(0, width - 2);
|
|
54
|
+
if (portrait)
|
|
55
|
+
return { tree: outer, agentLog: outer, messageLog: outer };
|
|
56
|
+
const shared = Math.max(0, outer - 2);
|
|
57
|
+
const { tree, messageLog } = sidePaneWidths({
|
|
58
|
+
tree: view.treeColumns,
|
|
59
|
+
treeFallback: AGENT_TREE_COLUMNS,
|
|
60
|
+
messageLog: view.messageLogColumns,
|
|
61
|
+
messageLogFallback: MESSAGE_LOG_COLUMNS,
|
|
62
|
+
available: shared,
|
|
63
|
+
});
|
|
64
|
+
return { tree, agentLog: Math.max(0, shared - tree - messageLog), messageLog };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Reduce the shell-owned pane navigation without changing authoritative or component-local state.
|
|
68
|
+
* @param state - Current model state; an open overlay makes every key a no-op here.
|
|
69
|
+
* @param key - Key name as OpenTUI normalises it, such as `tab`, `right` or `return`.
|
|
70
|
+
* @param portrait - Whether the single-pane stack is in use, which makes the message log a focus stop.
|
|
71
|
+
* @returns The next view: the same object for a key navigation does not handle, and possibly a new object with
|
|
72
|
+
* unchanged focus for a movement key that has nowhere to go.
|
|
73
|
+
*/
|
|
74
|
+
export function reduceOperatorTuiNavigation(state, key, portrait) {
|
|
75
|
+
const view = state.view;
|
|
76
|
+
if (view.overlay.kind !== "none")
|
|
77
|
+
return view;
|
|
78
|
+
// An empty agent log has no row to select, so focus would land somewhere with nothing to act on.
|
|
79
|
+
const carriesTraffic = selectTuiTraffic(state).length > 0;
|
|
80
|
+
// The message log is a stack level in portrait; in the wide layout it is a detail pane whose one
|
|
81
|
+
// key reaches it from the agent log, so keyboard navigation never stops there. A click on the
|
|
82
|
+
// detail focuses it in either layout.
|
|
83
|
+
const message = selectedMessage(state);
|
|
84
|
+
const panes = [
|
|
85
|
+
"tree",
|
|
86
|
+
...(carriesTraffic ? ["agent_log"] : []),
|
|
87
|
+
...(portrait && carriesTraffic && message ? ["message_log"] : []),
|
|
88
|
+
];
|
|
89
|
+
const current = panes.indexOf(view.focus);
|
|
90
|
+
const step = (delta) => ({
|
|
91
|
+
...view,
|
|
92
|
+
focus: panes[Math.min(Math.max(Math.max(current, 0) + delta, 0), panes.length - 1)] ?? "tree",
|
|
93
|
+
});
|
|
94
|
+
if (key === "tab")
|
|
95
|
+
return { ...view, focus: panes[(Math.max(current, 0) + 1) % panes.length] ?? "tree" };
|
|
96
|
+
if (key === "right")
|
|
97
|
+
return step(1);
|
|
98
|
+
if (key === "left")
|
|
99
|
+
return step(-1);
|
|
100
|
+
if (key === "enter" || key === "return") {
|
|
101
|
+
if (view.focus === "message_log" && message)
|
|
102
|
+
return {
|
|
103
|
+
...view,
|
|
104
|
+
overlay: { kind: "full_message", messageId: message.messageId },
|
|
105
|
+
overlayReturnFocus: view.focus,
|
|
106
|
+
};
|
|
107
|
+
if (selectTuiCollapsibleSelection(state) === null)
|
|
108
|
+
return step(1);
|
|
109
|
+
}
|
|
110
|
+
if (key === "escape" && portrait && view.focus === "message_log")
|
|
111
|
+
return { ...view, focus: "agent_log" };
|
|
112
|
+
if (key === "escape" && portrait && view.focus === "agent_log")
|
|
113
|
+
return { ...view, focus: "tree" };
|
|
114
|
+
return view;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Compose the complete responsive operator surface over the renderer-independent model.
|
|
118
|
+
*
|
|
119
|
+
* Mounting starts the model and unmounting disposes it.
|
|
120
|
+
* @param properties - Model and presentation collaborators, and the action that ends the process.
|
|
121
|
+
* @returns Root element filling the terminal, bordered, with the wide or portrait layout chosen from its width.
|
|
122
|
+
*/
|
|
123
|
+
export function OperatorTui(properties) {
|
|
124
|
+
const state = useTuiModel(properties.model);
|
|
125
|
+
const { width, height } = useTerminalDimensions();
|
|
126
|
+
const portrait = width < PORTRAIT_BREAKPOINT_COLUMNS;
|
|
127
|
+
const overlaid = state.view.overlay.kind !== "none";
|
|
128
|
+
const backdrop = useMemo(() => dimTuiTheme(properties.theme), [properties.theme]);
|
|
129
|
+
const theme = overlaid ? backdrop : properties.theme;
|
|
130
|
+
const colorEnabled = properties.colorEnabled ?? true;
|
|
131
|
+
const messageProperties = {
|
|
132
|
+
model: properties.model,
|
|
133
|
+
theme,
|
|
134
|
+
body: properties.body,
|
|
135
|
+
copyFallback: properties.copyFallback,
|
|
136
|
+
commands: properties.commands,
|
|
137
|
+
};
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
void properties.model.start();
|
|
140
|
+
return () => {
|
|
141
|
+
properties.model.dispose();
|
|
142
|
+
};
|
|
143
|
+
}, [properties.model]);
|
|
144
|
+
useKeyboard((key) => {
|
|
145
|
+
const current = properties.model.state;
|
|
146
|
+
if (current.view.overlay.kind === "help" || current.view.overlay.kind === "log") {
|
|
147
|
+
// The key that opened the log closes it, so it reads as a toggle rather than a one-way door.
|
|
148
|
+
const closes = key.name === "escape" || (key.name === "l" && current.view.overlay.kind === "log");
|
|
149
|
+
if (closes)
|
|
150
|
+
properties.model.updateView((view) => ({ ...view, overlay: { kind: "none" } }));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (current.view.overlay.kind === "actions" || current.view.overlay.kind === "action_run") {
|
|
154
|
+
const result = reduceOperatorActionsKey(current, key.name, properties.actions);
|
|
155
|
+
if (result.view !== current.view)
|
|
156
|
+
properties.model.updateView(() => result.view);
|
|
157
|
+
const request = result.request;
|
|
158
|
+
switch (request.kind) {
|
|
159
|
+
case "none":
|
|
160
|
+
break;
|
|
161
|
+
case "clear":
|
|
162
|
+
properties.model.clearLocalRun();
|
|
163
|
+
break;
|
|
164
|
+
case "cancel":
|
|
165
|
+
properties.model.cancelLocalRun();
|
|
166
|
+
break;
|
|
167
|
+
case "start":
|
|
168
|
+
properties.model.startLocalRun(request.command.command, request.command.args, skillInstallTerminalSize(height));
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (key.name === "m" && current.view.overlay.kind === "none") {
|
|
174
|
+
properties.model.updateView((view) => ({
|
|
175
|
+
...view,
|
|
176
|
+
overlay: { kind: "actions" },
|
|
177
|
+
actionIndex: 0,
|
|
178
|
+
overlayReturnFocus: view.focus,
|
|
179
|
+
}));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (key.name === "q" && current.view.overlay.kind === "none")
|
|
183
|
+
properties.onShutdown();
|
|
184
|
+
if (key.name === "l" && current.view.overlay.kind === "none") {
|
|
185
|
+
properties.model.updateView((view) => ({ ...view, overlay: { kind: "log" }, overlayReturnFocus: view.focus }));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (key.name === "?" && current.view.overlay.kind === "none") {
|
|
189
|
+
properties.model.updateView((view) => ({
|
|
190
|
+
...view,
|
|
191
|
+
overlay: { kind: "help" },
|
|
192
|
+
overlayReturnFocus: view.focus,
|
|
193
|
+
}));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (key.name === "up" || key.name === "down") {
|
|
197
|
+
stepFocus(properties.model, key.name, onCommand);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const view = reduceOperatorTuiNavigation(current, key.name, portrait);
|
|
201
|
+
if (view !== current.view) {
|
|
202
|
+
properties.model.updateView(() => view);
|
|
203
|
+
if (view.focus === "agent_log" && current.view.focus !== "agent_log" && view.trafficId === null)
|
|
204
|
+
selectNewestTuiTraffic(properties.model);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
const paneWidths = selectPaneWidths(width, portrait, state.view);
|
|
208
|
+
const bodyRows = selectMessageBodyRows(selectPaneContentRows(height, portrait, COLLAPSED_LOG_ROWS));
|
|
209
|
+
const onCommand = submitCommand(properties.model, properties.commands);
|
|
210
|
+
const tree = (_jsx(AgentTree, { model: properties.model, theme: theme, onCommand: onCommand, colorEnabled: colorEnabled, width: paneWidths.tree }));
|
|
211
|
+
const traffic = _jsx(AgentTrafficLog, { ...messageProperties, colorEnabled: colorEnabled, width: paneWidths.agentLog });
|
|
212
|
+
const detail = (_jsx(MessageDetail, { ...messageProperties, colorEnabled: colorEnabled, width: paneWidths.messageLog, bodyRows: bodyRows }));
|
|
213
|
+
return (_jsxs("box", { width: "100%", height: "100%", flexDirection: "column", border: true, borderStyle: "single", borderColor: theme.structure.rulesAndLabels, children: [portrait ? null : _jsx(StatusBar, { state: state, theme: theme }), portrait ? (_jsx(PortraitStack, { state: state, theme: theme, colorEnabled: colorEnabled, tree: tree, traffic: traffic, detail: detail, width: paneWidths.messageLog })) : (_jsxs("box", { flexGrow: 1, flexDirection: "row", children: [_jsx(Pane, { theme: theme, width: paneWidths.tree, footers: [
|
|
214
|
+
_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: selectTreeFooterKeys(state, TREE_FOOTER_KEYS) }), right: null }, "keys"),
|
|
215
|
+
], children: tree }), _jsx(Divider, { theme: theme, headerJunction: "\u251C" }), _jsx(Pane, { theme: theme, flexible: true, header: _jsx(AgentLogHeader, { state: state, theme: theme, colorEnabled: colorEnabled, width: paneWidths.agentLog }), footers: [_jsx(FollowFooter, { state: state, theme: theme }, "follow")], children: traffic }), _jsx(Divider, { theme: theme, headerJunction: "\u253C" }), _jsx(Pane, { theme: theme, width: paneWidths.messageLog, header: _jsx(MessageLogHeader, { state: state, theme: theme, width: paneWidths.messageLog }), footers: [
|
|
216
|
+
_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: selectedMessage(state) ? "o full message" : "" }), right: null }, "keys"),
|
|
217
|
+
], children: detail })] })), _jsx(DiagnosticsLog, { state: state, theme: theme, width: width }), portrait ? null : (_jsxs(_Fragment, { children: [_jsx(PaneGrabStrip, { left: paneWidths.tree, onDrag: (column) => {
|
|
218
|
+
properties.model.resizePane("tree", Math.max(0, column - 1), paneWidths.messageLog);
|
|
219
|
+
}, onSettle: () => {
|
|
220
|
+
properties.model.commitPaneLayout();
|
|
221
|
+
} }), _jsx(PaneGrabStrip, { left: paneWidths.tree + 1 + paneWidths.agentLog, onDrag: (column) => {
|
|
222
|
+
properties.model.resizePane("message_log", Math.max(0, width - column - 2), paneWidths.tree);
|
|
223
|
+
}, onSettle: () => {
|
|
224
|
+
properties.model.commitPaneLayout();
|
|
225
|
+
} })] })), _jsx(AgentTreeOverlay, { model: properties.model, theme: properties.theme, onCommand: onCommand, colorEnabled: colorEnabled }), _jsx(MessageOverlays, { ...messageProperties, theme: properties.theme, colorEnabled: colorEnabled }), state.view.overlay.kind === "actions" ? (_jsx(ActionsOverlay, { actionIndex: state.view.actionIndex, catalogue: properties.actions, theme: properties.theme, onSelect: (action) => {
|
|
226
|
+
properties.model.updateView((view) => ({ ...view, overlay: { kind: "action_run", actionId: action.id } }));
|
|
227
|
+
} })) : null, state.view.overlay.kind === "action_run" ? (_jsx(ActionRunOverlay, { run: state.run, actionId: state.view.overlay.actionId, catalogue: properties.actions, terminalRows: height, copyFallback: properties.copyFallback, onInput: (bytes) => {
|
|
228
|
+
properties.model.writeToLocalRun(bytes);
|
|
229
|
+
}, onResize: (columns, rows) => {
|
|
230
|
+
properties.model.resizeLocalRun(columns, rows);
|
|
231
|
+
}, theme: properties.theme })) : null, state.view.overlay.kind === "help" ? _jsx(HelpOverlay, { theme: properties.theme }) : null, state.view.overlay.kind === "log" ? _jsx(DiagnosticsOverlay, { state: state, theme: properties.theme }) : null] }));
|
|
232
|
+
}
|
|
233
|
+
/** Marker leading a portrait screen's header, which returns to the screen above it. */
|
|
234
|
+
const PORTRAIT_BACK_GLYPH = "◂ ";
|
|
235
|
+
function PortraitStack({ state, theme, colorEnabled, tree, traffic, detail, width, }) {
|
|
236
|
+
if (state.view.focus === "agent_log") {
|
|
237
|
+
const scope = agentLogScope(state);
|
|
238
|
+
return (_jsx(Pane, { theme: theme, flexible: true, topRule: false, header: _jsx(PaneBar, { left: _jsxs("text", { fg: theme.structure.rulesAndLabels, children: ["◂ ", _jsx("span", { fg: scope.kind === "agent" && colorEnabled ? agentColor(scope.agentId, theme) : theme.structure.emphasis, children: scope.title })] }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: scope.detail }) }), footers: [
|
|
239
|
+
_jsx(FollowFooter, { state: state, theme: theme }, "follow"),
|
|
240
|
+
_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: PORTRAIT_TRAFFIC_FOOTER_KEYS }), right: null }, "keys"),
|
|
241
|
+
], children: traffic }));
|
|
242
|
+
}
|
|
243
|
+
if (state.view.focus === "message_log") {
|
|
244
|
+
const message = selectedMessage(state);
|
|
245
|
+
const clock = message ? formatClock(message.acceptedAt) : "";
|
|
246
|
+
return (_jsx(Pane, { theme: theme, flexible: true, topRule: false, header: _jsx(PaneBar, { left: _jsxs("text", { fg: theme.structure.rulesAndLabels, children: [PORTRAIT_BACK_GLYPH, _jsx("span", { fg: theme.structure.emphasis, children: message ? fitBarLeading(message.messageId, width - PORTRAIT_BACK_GLYPH.length, clock) : "—" })] }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: clock }) }), footers: [
|
|
247
|
+
_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: PORTRAIT_MESSAGE_FOOTER_KEYS }), right: null }, "keys"),
|
|
248
|
+
], children: detail }));
|
|
249
|
+
}
|
|
250
|
+
return (_jsx(Pane, { theme: theme, flexible: true, topRule: false, header: _jsx(PortraitStatusBar, { state: state, theme: theme }), footers: [
|
|
251
|
+
_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: selectTreeFooterKeys(state, PORTRAIT_TREE_FOOTER_KEYS) }), right: null }, "keys"),
|
|
252
|
+
], children: tree }));
|
|
253
|
+
}
|
|
254
|
+
function Pane({ theme, width, flexible = false, topRule = true, header, footers, children, }) {
|
|
255
|
+
return (_jsxs("box", { ...(width === undefined ? {} : { width }), ...(flexible ? { flexGrow: 1 } : {}), height: "100%", flexDirection: "column", ...(topRule
|
|
256
|
+
? {
|
|
257
|
+
border: ["top", "bottom"],
|
|
258
|
+
borderStyle: "single",
|
|
259
|
+
borderColor: theme.structure.rulesAndLabels,
|
|
260
|
+
}
|
|
261
|
+
: { border: ["bottom"], borderStyle: "single", borderColor: theme.structure.rulesAndLabels }), children: [header === undefined ? null : (_jsxs(_Fragment, { children: [header, _jsx(Rule, { theme: theme })] })), _jsx("box", { flexGrow: 1, flexShrink: 1, overflow: "hidden", flexDirection: "column", children: children }), footers.map((footer, index) => (_jsxs("box", { flexDirection: "column", flexShrink: 0, children: [_jsx(Rule, { theme: theme }), footer] }, index)))] }));
|
|
262
|
+
}
|
|
263
|
+
function AgentLogHeader({ state, theme, colorEnabled, width, }) {
|
|
264
|
+
const scope = agentLogScope(state);
|
|
265
|
+
const titleColor = scope.kind === "agent" && colorEnabled ? agentColor(scope.agentId, theme) : theme.structure.emphasis;
|
|
266
|
+
return (_jsx(PaneBar, { left: _jsx("text", { fg: titleColor, children: fitBarLeading(scope.title, width, scope.detail) }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: scope.detail }) }));
|
|
267
|
+
}
|
|
268
|
+
function MessageLogHeader({ state, theme, width, }) {
|
|
269
|
+
const message = selectedMessage(state);
|
|
270
|
+
return (_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: message ? fitBarLeading(message.messageId, width) : "—" }), right: null }));
|
|
271
|
+
}
|
|
272
|
+
function FollowFooter({ state, theme }) {
|
|
273
|
+
return (_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "f follow" }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: state.view.followAgentLog ? "▼ following" : "▽ paused" }) }));
|
|
274
|
+
}
|
|
275
|
+
function StatusBar({ state, theme }) {
|
|
276
|
+
const broker = brokerIndicator(state.connection, theme);
|
|
277
|
+
return (_jsxs("box", { height: 1, flexShrink: 0, flexDirection: "row", paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [_jsx("text", { fg: theme.structure.emphasis, children: "pager" }), _jsx("box", { flexDirection: "row", children: _jsxs("text", { fg: theme.structure.rulesAndLabels, children: [_jsx("span", { fg: broker.color, children: broker.glyph }), " broker ", `${String(state.unreadMessageIds.size)} unread ${String(activeAgentCount(state))} agents`] }) })] }));
|
|
278
|
+
}
|
|
279
|
+
function PortraitStatusBar({ state, theme }) {
|
|
280
|
+
const broker = brokerIndicator(state.connection, theme);
|
|
281
|
+
return (_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.emphasis, children: "pager" }), right: _jsxs("text", { fg: theme.structure.rulesAndLabels, children: [_jsx("span", { fg: broker.color, children: broker.glyph }), ` ${String(state.unreadMessageIds.size)} unread · ${String(activeAgentCount(state))} agents`] }) }));
|
|
282
|
+
}
|
|
283
|
+
const HELP_KEYS = [
|
|
284
|
+
["↑↓", "move within the focused pane"],
|
|
285
|
+
["←→", "move between panes"],
|
|
286
|
+
["tab", "cycle panes"],
|
|
287
|
+
["esc", "close an overlay, or go back"],
|
|
288
|
+
["i", "agent identity"],
|
|
289
|
+
["p", "ping the selected agent's inbox"],
|
|
290
|
+
["a", "row actions"],
|
|
291
|
+
["m", "operator actions"],
|
|
292
|
+
["f", "follow the newest message"],
|
|
293
|
+
["enter", "toggle a section, else go to the next pane"],
|
|
294
|
+
["", "or, on the message pane, open the message"],
|
|
295
|
+
["o", "open the full message"],
|
|
296
|
+
["l", "open the diagnostics log"],
|
|
297
|
+
["y", "copy raw, in the full message"],
|
|
298
|
+
["q", "quit"],
|
|
299
|
+
];
|
|
300
|
+
function HelpOverlay({ theme }) {
|
|
301
|
+
return (_jsxs(Overlay, { theme: theme, width: 56, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.emphasis, children: "keys" }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "esc" }) }), _jsx(Rule, { theme: theme }), _jsx("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: HELP_KEYS.map(([key, description]) => (_jsx(Line, { children: _jsxs("text", { fg: theme.structure.bodyText, children: [_jsx("span", { fg: theme.structure.rulesAndLabels, children: key.padEnd(8) }), description] }) }, description))) })] }));
|
|
302
|
+
}
|
|
303
|
+
function stepFocus(model, direction, onCommand) {
|
|
304
|
+
const view = model.state.view;
|
|
305
|
+
if (view.overlay.kind !== "none")
|
|
306
|
+
return;
|
|
307
|
+
// The diagnostics pane is not focusable: it is read in the overlay the global key opens, so there
|
|
308
|
+
// is nothing below the last row to step into.
|
|
309
|
+
if (view.focus === "tree")
|
|
310
|
+
applyTreeStep(model, direction, onCommand);
|
|
311
|
+
else if (view.focus === "agent_log")
|
|
312
|
+
stepTuiTrafficSelection(model, direction);
|
|
313
|
+
}
|
|
314
|
+
function applyTreeStep(model, direction, onCommand) {
|
|
315
|
+
const before = model.state.view.selection;
|
|
316
|
+
const result = reduceAgentTreeKey(model.state, direction);
|
|
317
|
+
if (JSON.stringify(result.view.selection) === JSON.stringify(before))
|
|
318
|
+
return false;
|
|
319
|
+
executeAgentTreeResult(model, onCommand, result);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
function DiagnosticsLog({ state, theme, width, }) {
|
|
323
|
+
const entries = state.diagnostics.slice(-COLLAPSED_LOG_ROWS);
|
|
324
|
+
return (_jsx("box", { flexDirection: "column", height: COLLAPSED_LOG_ROWS, flexShrink: 0, overflow: "hidden", paddingLeft: 1, paddingRight: 1, children: entries.length === 0 ? (_jsx(Line, { children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "no diagnostics" }) })) : (entries.map((diagnostic, index) => (_jsx(Line, { children: _jsx("text", { fg: diagnosticColor(diagnostic.severity, theme), children: formatDiagnostic(diagnostic, width - 2) }) }, `${String(diagnostic.at)}-${String(index)}`)))) }));
|
|
325
|
+
}
|
|
326
|
+
function DiagnosticsOverlay({ state, theme }) {
|
|
327
|
+
return (_jsxs(Overlay, { theme: theme, width: DIAGNOSTICS_OVERLAY_COLUMNS, height: DIAGNOSTICS_OVERLAY_ROWS, children: [_jsx(PaneBar, { left: _jsx("text", { fg: theme.structure.emphasis, children: "log" }), right: _jsx("text", { fg: theme.structure.rulesAndLabels, children: "esc" }) }), _jsx(Rule, { theme: theme }), _jsx("scrollbox", { flexGrow: 1, focused: true, stickyScroll: true, stickyStart: "bottom", style: VERTICAL_SCROLL_STYLE, children: state.diagnostics.map((diagnostic, index) => (_jsx(Line, { children: _jsx("text", { fg: diagnosticColor(diagnostic.severity, theme), children: ` ${formatDiagnostic(diagnostic, DIAGNOSTICS_OVERLAY_COLUMNS - 3)}` }) }, `${String(diagnostic.at)}-${String(index)}`))) })] }));
|
|
328
|
+
}
|
|
329
|
+
const DIAGNOSTICS_OVERLAY_COLUMNS = 110;
|
|
330
|
+
const DIAGNOSTICS_OVERLAY_ROWS = 24;
|
|
331
|
+
/**
|
|
332
|
+
* Cover one pane rule with an invisible full-height strip that drags it.
|
|
333
|
+
*
|
|
334
|
+
* The rule sits inside the pane row and so cannot be grabbed level with the status bar above it. This
|
|
335
|
+
* strip spans the status bar and the pane row instead, stopping short of the window's own border and
|
|
336
|
+
* of the diagnostics log. It is declared after every pane because the hit grid is written in render
|
|
337
|
+
* order: an earlier sibling's cells are reclaimed by whatever draws over them.
|
|
338
|
+
* @param properties - Column the strip stands at, the drag handler given each pointer column, and the handler run when a drag ends.
|
|
339
|
+
* @returns One-column absolute box with no visible content, spanning the status bar and the pane row.
|
|
340
|
+
*/
|
|
341
|
+
function PaneGrabStrip({ left, onDrag, onSettle, }) {
|
|
342
|
+
return (_jsx("box", { position: "absolute", left: left, top: 0, bottom: COLLAPSED_LOG_ROWS, width: 1, onMouseDown: (event) => {
|
|
343
|
+
event.preventDefault();
|
|
344
|
+
}, onMouseDrag: (event) => {
|
|
345
|
+
onDrag(event.x);
|
|
346
|
+
}, onMouseDragEnd: () => {
|
|
347
|
+
onSettle();
|
|
348
|
+
} }));
|
|
349
|
+
}
|
|
350
|
+
function Divider({ theme, headerJunction }) {
|
|
351
|
+
// A bordered box would draw one character down its whole edge, overwriting each rule it crosses.
|
|
352
|
+
return (_jsxs("box", { width: 1, height: "100%", flexDirection: "column", children: [_jsx(DividerCell, { theme: theme, glyph: "\u252C" }), _jsx(DividerCell, { theme: theme, glyph: "\u2502" }), _jsx(DividerCell, { theme: theme, glyph: headerJunction }), _jsx("box", { flexGrow: 1, flexShrink: 1, border: ["left"], borderStyle: "single", borderColor: theme.structure.rulesAndLabels }), _jsx(DividerCell, { theme: theme, glyph: "\u253C" }), _jsx(DividerCell, { theme: theme, glyph: "\u2502" }), _jsx(DividerCell, { theme: theme, glyph: "\u2534" })] }));
|
|
353
|
+
}
|
|
354
|
+
function DividerCell({ theme, glyph }) {
|
|
355
|
+
return (_jsx("box", { height: 1, flexShrink: 0, children: _jsx("text", { fg: theme.structure.rulesAndLabels, children: glyph }) }));
|
|
356
|
+
}
|
|
357
|
+
function submitCommand(model, commands) {
|
|
358
|
+
return (command) => {
|
|
359
|
+
const report = (result) => {
|
|
360
|
+
const outcome = describeCommandOutcome(model.state, command, result);
|
|
361
|
+
model.reportDiagnostic(outcome.severity, "command", outcome.summary);
|
|
362
|
+
};
|
|
363
|
+
void model
|
|
364
|
+
.execute({
|
|
365
|
+
protocolVersion: OPERATOR_PROTOCOL_VERSION,
|
|
366
|
+
commandId: commands.commandId(),
|
|
367
|
+
issuedAt: commands.issuedAt(),
|
|
368
|
+
command,
|
|
369
|
+
})
|
|
370
|
+
.then((result) => {
|
|
371
|
+
report(result);
|
|
372
|
+
commands.observe(result);
|
|
373
|
+
}, () => {
|
|
374
|
+
report(null);
|
|
375
|
+
})
|
|
376
|
+
.catch(() => undefined);
|
|
377
|
+
};
|
|
378
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { TuiOperatorModel, TuiOperatorModelState } from "./model.js";
|
|
3
|
+
import type { TuiTheme } from "./theme.js";
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe a component to the model's immutable state.
|
|
6
|
+
* @param model - Renderer-independent state owner.
|
|
7
|
+
* @returns The current model state, re-rendering the caller on each transition.
|
|
8
|
+
*/
|
|
9
|
+
export declare function useTuiModel(model: TuiOperatorModel): TuiOperatorModelState;
|
|
10
|
+
/**
|
|
11
|
+
* Options that let a ScrollBox measure the height it was actually given.
|
|
12
|
+
*
|
|
13
|
+
* Its viewport reserves a row for the horizontal scrollbar even while that bar is hidden, and its
|
|
14
|
+
* content is at least as tall as the ScrollBox. A vertical list that fits therefore reports exactly
|
|
15
|
+
* one row of overflow it does not have: the scrollbar draws, and a sticky-bottom list scrolls its
|
|
16
|
+
* first row out of sight. Hiding the horizontal scrollbar gives the row back. A headless-renderer
|
|
17
|
+
* test pins both the quirk and this workaround, and fails once the ScrollBox no longer has the quirk.
|
|
18
|
+
*/
|
|
19
|
+
export declare const VERTICAL_SCROLL_STYLE: Readonly<{
|
|
20
|
+
horizontalScrollbarOptions: Readonly<{
|
|
21
|
+
visible: false;
|
|
22
|
+
}>;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* Columns a ScrollBox keeps for its vertical scrollbar, which it reserves whether or not the bar is
|
|
26
|
+
* drawn. Row content that must right-align against the pane edge has to subtract it.
|
|
27
|
+
*/
|
|
28
|
+
export declare const SCROLLBAR_RESERVED_COLUMNS = 1;
|
|
29
|
+
/**
|
|
30
|
+
* Options for a ScrollBox holding text it word-wraps itself.
|
|
31
|
+
*
|
|
32
|
+
* Text wraps to the ScrollBox's full width while the vertical scrollbar draws over the last column,
|
|
33
|
+
* so a row that fills its width loses its final character under the bar. Padding the content by the
|
|
34
|
+
* scrollbar's columns wraps the text clear of it.
|
|
35
|
+
*/
|
|
36
|
+
export declare const WRAPPED_TEXT_SCROLL_STYLE: Readonly<{
|
|
37
|
+
contentOptions: Readonly<{
|
|
38
|
+
paddingRight: 1;
|
|
39
|
+
}>;
|
|
40
|
+
horizontalScrollbarOptions: Readonly<{
|
|
41
|
+
visible: false;
|
|
42
|
+
}>;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* Draw one full-width horizontal rule occupying a single character row.
|
|
46
|
+
* @param properties - Semantic palette supplying the rule colour.
|
|
47
|
+
* @returns One rule row.
|
|
48
|
+
*/
|
|
49
|
+
export declare function Rule({ theme }: {
|
|
50
|
+
readonly theme: Readonly<TuiTheme>;
|
|
51
|
+
}): ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Hold content to exactly one character row so stacked rows keep the grid.
|
|
54
|
+
* @param properties - Row content.
|
|
55
|
+
* @returns One fixed-height row.
|
|
56
|
+
*/
|
|
57
|
+
export declare function Line({ children }: {
|
|
58
|
+
readonly children?: ReactNode;
|
|
59
|
+
}): ReactNode;
|
|
60
|
+
/**
|
|
61
|
+
* Draw one header or footer row with its leading and trailing content pushed apart.
|
|
62
|
+
* @param properties - Row content for each edge; an absent edge keeps the row's height.
|
|
63
|
+
* @returns One inset bar row.
|
|
64
|
+
*/
|
|
65
|
+
export declare function PaneBar({ left, right }: {
|
|
66
|
+
readonly left: ReactNode;
|
|
67
|
+
readonly right: ReactNode;
|
|
68
|
+
}): ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* Report the columns an overlay occupies once it is fitted to the terminal.
|
|
71
|
+
* @param width - Columns the overlay asks for.
|
|
72
|
+
* @param terminalColumns - Columns the whole terminal has.
|
|
73
|
+
* @returns - Columns the overlay draws at, including its border.
|
|
74
|
+
*/
|
|
75
|
+
export declare function fitOverlayColumns(width: number, terminalColumns: number): number;
|
|
76
|
+
/**
|
|
77
|
+
* Centre one bordered overlay over the panes it covers.
|
|
78
|
+
* @param properties - Palette, fixed overlay width, optional fixed height, and overlay content.
|
|
79
|
+
* @returns The overlay positioned at the terminal root.
|
|
80
|
+
*/
|
|
81
|
+
export declare function Overlay({ theme, width, height, children, }: {
|
|
82
|
+
readonly theme: Readonly<TuiTheme>;
|
|
83
|
+
readonly width: number;
|
|
84
|
+
readonly height?: number;
|
|
85
|
+
readonly children: ReactNode;
|
|
86
|
+
}): ReactNode;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { useSyncExternalStore } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* Subscribe a component to the model's immutable state.
|
|
6
|
+
* @param model - Renderer-independent state owner.
|
|
7
|
+
* @returns The current model state, re-rendering the caller on each transition.
|
|
8
|
+
*/
|
|
9
|
+
export function useTuiModel(model) {
|
|
10
|
+
return useSyncExternalStore(model.subscribe.bind(model), () => model.state, () => model.state);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Options that let a ScrollBox measure the height it was actually given.
|
|
14
|
+
*
|
|
15
|
+
* Its viewport reserves a row for the horizontal scrollbar even while that bar is hidden, and its
|
|
16
|
+
* content is at least as tall as the ScrollBox. A vertical list that fits therefore reports exactly
|
|
17
|
+
* one row of overflow it does not have: the scrollbar draws, and a sticky-bottom list scrolls its
|
|
18
|
+
* first row out of sight. Hiding the horizontal scrollbar gives the row back. A headless-renderer
|
|
19
|
+
* test pins both the quirk and this workaround, and fails once the ScrollBox no longer has the quirk.
|
|
20
|
+
*/
|
|
21
|
+
export const VERTICAL_SCROLL_STYLE = Object.freeze({
|
|
22
|
+
horizontalScrollbarOptions: Object.freeze({ visible: false }),
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Columns a ScrollBox keeps for its vertical scrollbar, which it reserves whether or not the bar is
|
|
26
|
+
* drawn. Row content that must right-align against the pane edge has to subtract it.
|
|
27
|
+
*/
|
|
28
|
+
export const SCROLLBAR_RESERVED_COLUMNS = 1;
|
|
29
|
+
/**
|
|
30
|
+
* Options for a ScrollBox holding text it word-wraps itself.
|
|
31
|
+
*
|
|
32
|
+
* Text wraps to the ScrollBox's full width while the vertical scrollbar draws over the last column,
|
|
33
|
+
* so a row that fills its width loses its final character under the bar. Padding the content by the
|
|
34
|
+
* scrollbar's columns wraps the text clear of it.
|
|
35
|
+
*/
|
|
36
|
+
export const WRAPPED_TEXT_SCROLL_STYLE = Object.freeze({
|
|
37
|
+
...VERTICAL_SCROLL_STYLE,
|
|
38
|
+
contentOptions: Object.freeze({ paddingRight: SCROLLBAR_RESERVED_COLUMNS }),
|
|
39
|
+
});
|
|
40
|
+
/** Narrowest overlay that still reads as a panel rather than a fragment. */
|
|
41
|
+
const MINIMUM_OVERLAY_COLUMNS = 20;
|
|
42
|
+
/**
|
|
43
|
+
* Draw one full-width horizontal rule occupying a single character row.
|
|
44
|
+
* @param properties - Semantic palette supplying the rule colour.
|
|
45
|
+
* @returns One rule row.
|
|
46
|
+
*/
|
|
47
|
+
export function Rule({ theme }) {
|
|
48
|
+
return (_jsx("box", { height: 1, flexShrink: 0, border: ["top"], borderStyle: "single", borderColor: theme.structure.rulesAndLabels }));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Hold content to exactly one character row so stacked rows keep the grid.
|
|
52
|
+
* @param properties - Row content.
|
|
53
|
+
* @returns One fixed-height row.
|
|
54
|
+
*/
|
|
55
|
+
export function Line({ children }) {
|
|
56
|
+
return (_jsx("box", { height: 1, flexShrink: 0, children: children }));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Draw one header or footer row with its leading and trailing content pushed apart.
|
|
60
|
+
* @param properties - Row content for each edge; an absent edge keeps the row's height.
|
|
61
|
+
* @returns One inset bar row.
|
|
62
|
+
*/
|
|
63
|
+
export function PaneBar({ left, right }) {
|
|
64
|
+
return (_jsxs("box", { height: 1, flexShrink: 0, flexDirection: "row", paddingLeft: 1, paddingRight: 1, justifyContent: "space-between", children: [left ?? _jsx("text", { children: " " }), right ?? _jsx("text", { children: " " })] }));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Report the columns an overlay occupies once it is fitted to the terminal.
|
|
68
|
+
* @param width - Columns the overlay asks for.
|
|
69
|
+
* @param terminalColumns - Columns the whole terminal has.
|
|
70
|
+
* @returns - Columns the overlay draws at, including its border.
|
|
71
|
+
*/
|
|
72
|
+
export function fitOverlayColumns(width, terminalColumns) {
|
|
73
|
+
return Math.min(width, Math.max(MINIMUM_OVERLAY_COLUMNS, terminalColumns - 2));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Centre one bordered overlay over the panes it covers.
|
|
77
|
+
* @param properties - Palette, fixed overlay width, optional fixed height, and overlay content.
|
|
78
|
+
* @returns The overlay positioned at the terminal root.
|
|
79
|
+
*/
|
|
80
|
+
export function Overlay({ theme, width, height, children, }) {
|
|
81
|
+
const terminal = useTerminalDimensions();
|
|
82
|
+
const fitted = fitOverlayColumns(width, terminal.width);
|
|
83
|
+
const fittedHeight = height === undefined ? undefined : Math.min(height, Math.max(3, terminal.height - 2));
|
|
84
|
+
return (_jsx("box", { position: "absolute", left: 0, top: 0, width: "100%", height: "100%", justifyContent: "center", alignItems: "center", children: _jsx("box", { width: fitted, ...(fittedHeight === undefined ? {} : { height: fittedHeight }), border: true, borderStyle: "single", borderColor: theme.structure.rulesAndLabels, flexDirection: "column", backgroundColor: theme.structure.background, children: children }) }));
|
|
85
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type OperatorCommandEnvelope, type OperatorCommandResult, type OperatorCursor } from "../operator/protocol.js";
|
|
2
|
+
import type { TuiRequest, TuiStreamObservation } from "./model.js";
|
|
3
|
+
/** Owns one cancellable loopback NDJSON session and exposes only model-guarded stream callbacks. */
|
|
4
|
+
export declare class TuiOperatorTcpClient {
|
|
5
|
+
#private;
|
|
6
|
+
private readonly url;
|
|
7
|
+
/**
|
|
8
|
+
* Construct a client for the published numeric loopback TCP endpoint. No connection opens until a fetch or open.
|
|
9
|
+
* @param url - Operator endpoint the broker published, naming a numeric loopback host and port.
|
|
10
|
+
*/
|
|
11
|
+
constructor(url: string);
|
|
12
|
+
/**
|
|
13
|
+
* Open cold hydration and resolve exactly one authoritative snapshot.
|
|
14
|
+
* @returns Snapshot request; cancelling it resets the session, and a second fetch while one is pending rejects.
|
|
15
|
+
*/
|
|
16
|
+
fetch(): TuiRequest<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Attach the model-owned ordered callback, opening a resumed session when no socket is live.
|
|
19
|
+
*
|
|
20
|
+
* After hydration the session opened by `fetch` is already subscribed, and the cursor it was
|
|
21
|
+
* subscribed at is the one the caller now holds. Only a caller resuming without hydrating first
|
|
22
|
+
* reaches the connect below, and the cursor it supplies is what makes the server replay rather
|
|
23
|
+
* than transfer a whole snapshot.
|
|
24
|
+
* @param cursor - Position the caller already holds, or null when it holds no snapshot.
|
|
25
|
+
* @param receive - Receives each validated observation in stream order, including any buffered before this call.
|
|
26
|
+
* @param unavailable - Called once when the session fails; a failure recorded before this call is reported on the next microtask.
|
|
27
|
+
* @returns Cancellation callback for this stream generation.
|
|
28
|
+
*/
|
|
29
|
+
open(cursor: OperatorCursor | null, receive: (observation: TuiStreamObservation) => void, unavailable: () => void): () => void;
|
|
30
|
+
/** Reset the current session. */
|
|
31
|
+
reconnect(): void;
|
|
32
|
+
/**
|
|
33
|
+
* Submit one correlated non-authoritative command.
|
|
34
|
+
* @param command - Validated command envelope, correlated with its result by `commandId`.
|
|
35
|
+
* @returns Command request; it rejects at once without a live session, while hydration is pending, when
|
|
36
|
+
* too many commands are pending, or for a duplicate `commandId`, and rejects later if the session resets.
|
|
37
|
+
*/
|
|
38
|
+
execute(command: OperatorCommandEnvelope): TuiRequest<OperatorCommandResult>;
|
|
39
|
+
/** Release socket, input buffers, and all pending work exactly once. */
|
|
40
|
+
dispose(): void;
|
|
41
|
+
}
|