@treeseed/cli 0.4.7 → 0.4.9
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 +10 -2
- package/dist/cli/handlers/close.js +3 -1
- package/dist/cli/handlers/config-ui.d.ts +72 -0
- package/dist/cli/handlers/config-ui.js +785 -0
- package/dist/cli/handlers/config.js +105 -35
- package/dist/cli/handlers/export.d.ts +2 -0
- package/dist/cli/handlers/export.js +28 -0
- package/dist/cli/handlers/release.js +3 -1
- package/dist/cli/handlers/save.js +5 -3
- package/dist/cli/handlers/stage.js +3 -1
- package/dist/cli/help-ui.d.ts +3 -0
- package/dist/cli/help-ui.js +490 -0
- package/dist/cli/help.d.ts +26 -0
- package/dist/cli/help.js +332 -91
- package/dist/cli/operations-registry.js +682 -29
- package/dist/cli/operations-types.d.ts +37 -2
- package/dist/cli/registry.d.ts +1 -0
- package/dist/cli/registry.js +2 -0
- package/dist/cli/runtime.js +22 -9
- package/dist/cli/ui/framework.d.ts +159 -0
- package/dist/cli/ui/framework.js +296 -0
- package/dist/cli/ui/mouse.d.ts +11 -0
- package/dist/cli/ui/mouse.js +72 -0
- package/package.json +7 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TreeseedOperationContext as SdkOperationContext, TreeseedOperationGroup, TreeseedOperationId, TreeseedOperationMetadata, TreeseedOperationResult as SdkOperationResult } from '@treeseed/sdk/operations';
|
|
2
2
|
export type TreeseedCommandGroup = TreeseedOperationGroup;
|
|
3
|
-
export type
|
|
3
|
+
export type TreeseedExecutionDelegate = 'agents';
|
|
4
|
+
export type TreeseedExecutionMode = 'handler' | 'adapter' | 'delegate';
|
|
4
5
|
export type TreeseedArgumentKind = 'positional' | 'message_tail';
|
|
5
6
|
export type TreeseedOptionKind = 'boolean' | 'string' | 'enum';
|
|
6
7
|
export type TreeseedCommandArgumentSpec = {
|
|
@@ -17,7 +18,36 @@ export type TreeseedCommandOptionSpec = {
|
|
|
17
18
|
repeatable?: boolean;
|
|
18
19
|
values?: string[];
|
|
19
20
|
};
|
|
20
|
-
export type
|
|
21
|
+
export type TreeseedStructuredCommandExample = {
|
|
22
|
+
command: string;
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
result?: string;
|
|
26
|
+
why?: string;
|
|
27
|
+
};
|
|
28
|
+
export type TreeseedCommandExample = string | TreeseedStructuredCommandExample;
|
|
29
|
+
export type TreeseedCommandHelpDetail = {
|
|
30
|
+
name: string;
|
|
31
|
+
detail: string;
|
|
32
|
+
};
|
|
33
|
+
export type TreeseedCommandRelatedDetail = {
|
|
34
|
+
name: string;
|
|
35
|
+
why: string;
|
|
36
|
+
};
|
|
37
|
+
export type TreeseedCommandHelpSpec = {
|
|
38
|
+
workflowPosition?: string;
|
|
39
|
+
longSummary?: string[];
|
|
40
|
+
whenToUse?: string[];
|
|
41
|
+
beforeYouRun?: string[];
|
|
42
|
+
outcomes?: string[];
|
|
43
|
+
examples?: TreeseedStructuredCommandExample[];
|
|
44
|
+
optionDetails?: TreeseedCommandHelpDetail[];
|
|
45
|
+
argumentDetails?: TreeseedCommandHelpDetail[];
|
|
46
|
+
automationNotes?: string[];
|
|
47
|
+
warnings?: string[];
|
|
48
|
+
relatedDetails?: TreeseedCommandRelatedDetail[];
|
|
49
|
+
seeAlso?: string[];
|
|
50
|
+
};
|
|
21
51
|
export type TreeseedParsedInvocation = {
|
|
22
52
|
commandName: string;
|
|
23
53
|
args: Record<string, string | string[] | boolean | undefined>;
|
|
@@ -40,6 +70,7 @@ export type TreeseedCommandContext = {
|
|
|
40
70
|
write: TreeseedWriter;
|
|
41
71
|
spawn: TreeseedSpawner;
|
|
42
72
|
outputFormat?: 'human' | 'json';
|
|
73
|
+
interactiveUi?: boolean;
|
|
43
74
|
prompt?: TreeseedPromptHandler;
|
|
44
75
|
confirm?: TreeseedConfirmHandler;
|
|
45
76
|
};
|
|
@@ -50,9 +81,13 @@ export type TreeseedOperationSpec = TreeseedOperationMetadata & {
|
|
|
50
81
|
arguments?: TreeseedCommandArgumentSpec[];
|
|
51
82
|
options?: TreeseedCommandOptionSpec[];
|
|
52
83
|
examples?: TreeseedCommandExample[];
|
|
84
|
+
help?: TreeseedCommandHelpSpec;
|
|
53
85
|
notes?: string[];
|
|
86
|
+
helpVisible?: boolean;
|
|
87
|
+
helpFeatured?: boolean;
|
|
54
88
|
executionMode: TreeseedExecutionMode;
|
|
55
89
|
handlerName?: string;
|
|
90
|
+
delegateTo?: TreeseedExecutionDelegate;
|
|
56
91
|
buildAdapterInput?: TreeseedAdapterInputBuilder;
|
|
57
92
|
};
|
|
58
93
|
export type TreeseedCommandSpec = TreeseedOperationSpec;
|
package/dist/cli/registry.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare const COMMAND_HANDLERS: {
|
|
|
16
16
|
readonly tasks: import("./operations-types.js").TreeseedCommandHandler;
|
|
17
17
|
readonly switch: import("./operations-types.js").TreeseedCommandHandler;
|
|
18
18
|
readonly stage: import("./operations-types.js").TreeseedCommandHandler;
|
|
19
|
+
readonly export: import("./operations-types.js").TreeseedCommandHandler;
|
|
19
20
|
readonly 'auth:login': import("./operations-types.js").TreeseedCommandHandler;
|
|
20
21
|
readonly 'auth:logout': import("./operations-types.js").TreeseedCommandHandler;
|
|
21
22
|
readonly 'auth:whoami': import("./operations-types.js").TreeseedCommandHandler;
|
package/dist/cli/registry.js
CHANGED
|
@@ -21,6 +21,7 @@ import { handleAuthWhoAmI } from "./handlers/auth-whoami.js";
|
|
|
21
21
|
import { handleTasks } from "./handlers/tasks.js";
|
|
22
22
|
import { handleSwitch } from "./handlers/switch.js";
|
|
23
23
|
import { handleStage } from "./handlers/stage.js";
|
|
24
|
+
import { handleExport } from "./handlers/export.js";
|
|
24
25
|
const COMMAND_HANDLERS = {
|
|
25
26
|
init: handleInit,
|
|
26
27
|
config: handleConfig,
|
|
@@ -38,6 +39,7 @@ const COMMAND_HANDLERS = {
|
|
|
38
39
|
tasks: handleTasks,
|
|
39
40
|
switch: handleSwitch,
|
|
40
41
|
stage: handleStage,
|
|
42
|
+
export: handleExport,
|
|
41
43
|
"auth:login": handleAuthLogin,
|
|
42
44
|
"auth:logout": handleAuthLogout,
|
|
43
45
|
"auth:whoami": handleAuthWhoAmI
|
package/dist/cli/runtime.js
CHANGED
|
@@ -7,6 +7,7 @@ import { findNearestTreeseedRoot, findNearestTreeseedWorkspaceRoot } from "@tree
|
|
|
7
7
|
import { TreeseedOperationsSdk as SdkOperationsRuntime } from "@treeseed/sdk/operations";
|
|
8
8
|
import { COMMAND_HANDLERS } from "./registry.js";
|
|
9
9
|
import { renderTreeseedHelp, renderUsage, suggestTreeseedCommands } from "./operations-help.js";
|
|
10
|
+
import { renderTreeseedHelpInk, shouldUseInkHelp } from "./help-ui.js";
|
|
10
11
|
import { parseTreeseedInvocation, validateTreeseedInvocation } from "./operations-parser.js";
|
|
11
12
|
import { findTreeseedOperation, TRESEED_OPERATION_SPECS } from "./operations-registry.js";
|
|
12
13
|
const require2 = createRequire(import.meta.url);
|
|
@@ -73,6 +74,7 @@ function createTreeseedCommandContext(overrides = {}) {
|
|
|
73
74
|
write: overrides.write ?? defaultWrite,
|
|
74
75
|
spawn: overrides.spawn ?? defaultSpawn,
|
|
75
76
|
outputFormat: overrides.outputFormat ?? "human",
|
|
77
|
+
interactiveUi: overrides.interactiveUi ?? overrides.write == null,
|
|
76
78
|
prompt: overrides.prompt,
|
|
77
79
|
confirm: overrides.confirm
|
|
78
80
|
};
|
|
@@ -213,15 +215,14 @@ class TreeseedOperationsSdk {
|
|
|
213
215
|
const context = createTreeseedCommandContext(overrides);
|
|
214
216
|
const argv = request.argv ?? [];
|
|
215
217
|
const commandName = request.commandName;
|
|
216
|
-
if (commandName === "agents") {
|
|
217
|
-
if (argv.some(isHelpFlag)) {
|
|
218
|
-
context.write(renderTreeseedHelp("agents"), "stdout");
|
|
219
|
-
return 0;
|
|
220
|
-
}
|
|
221
|
-
return this.executeAgents(argv, context);
|
|
222
|
-
}
|
|
223
218
|
const spec = findTreeseedOperation(commandName);
|
|
224
219
|
if (!spec) {
|
|
220
|
+
if (shouldUseInkHelp(context)) {
|
|
221
|
+
const helpExitCode = await renderTreeseedHelpInk(commandName, context);
|
|
222
|
+
if (typeof helpExitCode === "number") {
|
|
223
|
+
return helpExitCode;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
225
226
|
const suggestions = suggestTreeseedCommands(commandName);
|
|
226
227
|
const lines = [`Unknown treeseed command: ${commandName}`];
|
|
227
228
|
if (suggestions.length > 0) {
|
|
@@ -231,16 +232,28 @@ class TreeseedOperationsSdk {
|
|
|
231
232
|
return writeTreeseedResult({ exitCode: 1, stderr: [lines.join("\n")] }, context);
|
|
232
233
|
}
|
|
233
234
|
if (argv.some(isHelpFlag)) {
|
|
235
|
+
if (shouldUseInkHelp(context)) {
|
|
236
|
+
const helpExitCode = await renderTreeseedHelpInk(spec.name, context);
|
|
237
|
+
if (typeof helpExitCode === "number") {
|
|
238
|
+
return helpExitCode;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
234
241
|
context.write(renderTreeseedHelp(spec.name), "stdout");
|
|
235
242
|
return 0;
|
|
236
243
|
}
|
|
237
|
-
return spec.executionMode === "adapter" ? this.executeAdapter(spec, argv, context) : this.executeHandler(spec, argv, context);
|
|
244
|
+
return spec.executionMode === "adapter" ? this.executeAdapter(spec, argv, context) : spec.executionMode === "delegate" ? this.executeAgents(argv, context) : this.executeHandler(spec, argv, context);
|
|
238
245
|
}
|
|
239
246
|
async run(argv, overrides = {}) {
|
|
240
247
|
const context = createTreeseedCommandContext(overrides);
|
|
241
248
|
const [firstArg, ...restArgs] = argv;
|
|
242
249
|
if (!firstArg || isHelpFlag(firstArg) || firstArg === "help") {
|
|
243
250
|
const commandName = firstArg === "help" ? restArgs[0] ?? null : null;
|
|
251
|
+
if (shouldUseInkHelp(context)) {
|
|
252
|
+
const helpExitCode = await renderTreeseedHelpInk(commandName, context);
|
|
253
|
+
if (typeof helpExitCode === "number") {
|
|
254
|
+
return helpExitCode;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
244
257
|
const helpText = renderTreeseedHelp(commandName);
|
|
245
258
|
context.write(helpText, "stdout");
|
|
246
259
|
return commandName && helpText.startsWith("Unknown treeseed command:") ? 1 : 0;
|
|
@@ -257,7 +270,7 @@ function formatProjectError(spec) {
|
|
|
257
270
|
].join("\n");
|
|
258
271
|
}
|
|
259
272
|
function commandNeedsProjectRoot(spec) {
|
|
260
|
-
return spec.name !== "init";
|
|
273
|
+
return spec.name !== "init" && spec.name !== "export";
|
|
261
274
|
}
|
|
262
275
|
function resolveTreeseedCommandCwd(spec, cwd) {
|
|
263
276
|
if (!commandNeedsProjectRoot(spec)) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type HumanUiSessionResult<TPayload extends Record<string, unknown> = Record<string, unknown>> = {
|
|
3
|
+
submitted: boolean;
|
|
4
|
+
payload: TPayload;
|
|
5
|
+
};
|
|
6
|
+
export type UiViewportLayout = {
|
|
7
|
+
rows: number;
|
|
8
|
+
columns: number;
|
|
9
|
+
topBarHeight: number;
|
|
10
|
+
bodyHeight: number;
|
|
11
|
+
footerHeight: number;
|
|
12
|
+
totalHeight: number;
|
|
13
|
+
};
|
|
14
|
+
export type InteractiveControlState = {
|
|
15
|
+
focused?: boolean;
|
|
16
|
+
active?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export type ScrollRegionState = {
|
|
20
|
+
offset: number;
|
|
21
|
+
viewportSize: number;
|
|
22
|
+
totalSize: number;
|
|
23
|
+
};
|
|
24
|
+
export type UiScrollRegion = {
|
|
25
|
+
id: string;
|
|
26
|
+
rect: UiRect;
|
|
27
|
+
state: ScrollRegionState;
|
|
28
|
+
onScroll: (offset: number) => void;
|
|
29
|
+
onFocus?: () => void;
|
|
30
|
+
};
|
|
31
|
+
export type UiClickRegion = {
|
|
32
|
+
id: string;
|
|
33
|
+
rect: UiRect;
|
|
34
|
+
onClick: () => void;
|
|
35
|
+
};
|
|
36
|
+
export type UiNavigationStack<T> = T[];
|
|
37
|
+
export type UiRect = {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
};
|
|
43
|
+
export type TabItem = {
|
|
44
|
+
id: string;
|
|
45
|
+
label: string;
|
|
46
|
+
};
|
|
47
|
+
export declare function computeViewportLayout(rows: number, columns: number, options?: {
|
|
48
|
+
topBarHeight?: number;
|
|
49
|
+
footerHeight?: number;
|
|
50
|
+
}): UiViewportLayout;
|
|
51
|
+
export declare function clampOffset(offset: number, totalItems: number, viewportSize: number): number;
|
|
52
|
+
export declare function scrollOffsetByDelta(state: ScrollRegionState, delta: number): number;
|
|
53
|
+
export declare function scrollOffsetByPage(state: ScrollRegionState, pages: number): number;
|
|
54
|
+
export declare function ensureVisible(index: number, offset: number, viewportSize: number): number;
|
|
55
|
+
export declare function truncateLine(value: string, width: number): string;
|
|
56
|
+
export declare function wrapText(value: string, width: number): string[];
|
|
57
|
+
export declare function containsPoint(rect: UiRect, x: number, y: number): boolean;
|
|
58
|
+
export declare function findClickableRegion(regions: UiClickRegion[], x: number, y: number): UiClickRegion | null;
|
|
59
|
+
export declare function findScrollRegion(regions: UiScrollRegion[], x: number, y: number): UiScrollRegion | null;
|
|
60
|
+
export declare function routeWheelDeltaToScrollRegion(regions: UiScrollRegion[], x: number, y: number, delta: number): boolean;
|
|
61
|
+
export declare function pushNavigationEntry<T>(stack: UiNavigationStack<T>, entry: T): T[];
|
|
62
|
+
export declare function popNavigationEntry<T>(stack: UiNavigationStack<T>): {
|
|
63
|
+
nextStack: UiNavigationStack<T>;
|
|
64
|
+
popped: T | null;
|
|
65
|
+
};
|
|
66
|
+
type AppFrameProps = {
|
|
67
|
+
layout: UiViewportLayout;
|
|
68
|
+
topBar: React.ReactNode;
|
|
69
|
+
body: React.ReactNode;
|
|
70
|
+
footer: React.ReactNode;
|
|
71
|
+
};
|
|
72
|
+
export declare function AppFrame(props: AppFrameProps): any;
|
|
73
|
+
type TopTabsProps = {
|
|
74
|
+
title?: string;
|
|
75
|
+
items: TabItem[];
|
|
76
|
+
activeId: string;
|
|
77
|
+
focused?: boolean;
|
|
78
|
+
width: number;
|
|
79
|
+
prefix?: string;
|
|
80
|
+
};
|
|
81
|
+
export declare function TopTabs(props: TopTabsProps): any;
|
|
82
|
+
type SidebarListProps = {
|
|
83
|
+
width: number;
|
|
84
|
+
height: number;
|
|
85
|
+
items: Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
label: string;
|
|
88
|
+
active?: boolean;
|
|
89
|
+
tone?: 'required' | 'normal';
|
|
90
|
+
}>;
|
|
91
|
+
focused?: boolean;
|
|
92
|
+
scrollState?: ScrollRegionState;
|
|
93
|
+
title?: string;
|
|
94
|
+
};
|
|
95
|
+
export declare function SidebarList(props: SidebarListProps): any;
|
|
96
|
+
type ScrollPanelProps = {
|
|
97
|
+
width: number;
|
|
98
|
+
height: number;
|
|
99
|
+
title?: string;
|
|
100
|
+
lines: string[];
|
|
101
|
+
focused?: boolean;
|
|
102
|
+
tone?: 'normal' | 'accent';
|
|
103
|
+
scrollState?: ScrollRegionState;
|
|
104
|
+
};
|
|
105
|
+
export declare function ScrollPanel(props: ScrollPanelProps): any;
|
|
106
|
+
type FieldCardProps = {
|
|
107
|
+
width: number;
|
|
108
|
+
height: number;
|
|
109
|
+
title: string;
|
|
110
|
+
lines: string[];
|
|
111
|
+
focused?: boolean;
|
|
112
|
+
};
|
|
113
|
+
export declare function FieldCard(props: FieldCardProps): any;
|
|
114
|
+
type TextInputFieldProps = {
|
|
115
|
+
label: string;
|
|
116
|
+
value: string;
|
|
117
|
+
width: number;
|
|
118
|
+
height?: number;
|
|
119
|
+
focused?: boolean;
|
|
120
|
+
secret?: boolean;
|
|
121
|
+
placeholder?: string;
|
|
122
|
+
cursorPosition?: number;
|
|
123
|
+
};
|
|
124
|
+
export declare function TextInputField(props: TextInputFieldProps): any;
|
|
125
|
+
type TextAreaFieldProps = {
|
|
126
|
+
label: string;
|
|
127
|
+
value: string;
|
|
128
|
+
width: number;
|
|
129
|
+
height: number;
|
|
130
|
+
focused?: boolean;
|
|
131
|
+
};
|
|
132
|
+
export declare function TextAreaField(props: TextAreaFieldProps): any;
|
|
133
|
+
type ButtonProps = {
|
|
134
|
+
label: string;
|
|
135
|
+
focused?: boolean;
|
|
136
|
+
active?: boolean;
|
|
137
|
+
width?: number;
|
|
138
|
+
};
|
|
139
|
+
export declare function PrimaryButton(props: ButtonProps): any;
|
|
140
|
+
export declare function SecondaryButton(props: ButtonProps): any;
|
|
141
|
+
type StatusBarProps = {
|
|
142
|
+
width: number;
|
|
143
|
+
primary: string;
|
|
144
|
+
secondary?: string;
|
|
145
|
+
accent?: boolean;
|
|
146
|
+
};
|
|
147
|
+
export declare function StatusBar(props: StatusBarProps): any;
|
|
148
|
+
export declare function EmptyState(props: {
|
|
149
|
+
width: number;
|
|
150
|
+
height: number;
|
|
151
|
+
title: string;
|
|
152
|
+
message: string;
|
|
153
|
+
}): any;
|
|
154
|
+
export declare function ConfirmDialog(props: {
|
|
155
|
+
width: number;
|
|
156
|
+
title: string;
|
|
157
|
+
message: string;
|
|
158
|
+
}): any;
|
|
159
|
+
export {};
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import React from "react";
|
|
3
|
+
function computeViewportLayout(rows, columns, options = {}) {
|
|
4
|
+
const safeRows = Math.max(12, rows || 24);
|
|
5
|
+
const safeColumns = Math.max(72, columns || 100);
|
|
6
|
+
const topBarHeight = options.topBarHeight ?? 2;
|
|
7
|
+
const footerHeight = options.footerHeight ?? 2;
|
|
8
|
+
const bodyHeight = Math.max(6, safeRows - topBarHeight - footerHeight);
|
|
9
|
+
return {
|
|
10
|
+
rows: safeRows,
|
|
11
|
+
columns: safeColumns,
|
|
12
|
+
topBarHeight,
|
|
13
|
+
bodyHeight,
|
|
14
|
+
footerHeight,
|
|
15
|
+
totalHeight: topBarHeight + bodyHeight + footerHeight
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function clampOffset(offset, totalItems, viewportSize) {
|
|
19
|
+
return Math.max(0, Math.min(offset, Math.max(0, totalItems - viewportSize)));
|
|
20
|
+
}
|
|
21
|
+
function scrollOffsetByDelta(state, delta) {
|
|
22
|
+
return clampOffset(state.offset + delta, state.totalSize, state.viewportSize);
|
|
23
|
+
}
|
|
24
|
+
function scrollOffsetByPage(state, pages) {
|
|
25
|
+
return scrollOffsetByDelta(state, Math.max(1, state.viewportSize) * pages);
|
|
26
|
+
}
|
|
27
|
+
function ensureVisible(index, offset, viewportSize) {
|
|
28
|
+
if (viewportSize <= 0) {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
if (index < offset) {
|
|
32
|
+
return index;
|
|
33
|
+
}
|
|
34
|
+
if (index >= offset + viewportSize) {
|
|
35
|
+
return Math.max(0, index - viewportSize + 1);
|
|
36
|
+
}
|
|
37
|
+
return offset;
|
|
38
|
+
}
|
|
39
|
+
function truncateLine(value, width) {
|
|
40
|
+
if (width <= 0) {
|
|
41
|
+
return "";
|
|
42
|
+
}
|
|
43
|
+
if (value.length <= width) {
|
|
44
|
+
return value.padEnd(width, " ");
|
|
45
|
+
}
|
|
46
|
+
if (width <= 1) {
|
|
47
|
+
return value.slice(0, width);
|
|
48
|
+
}
|
|
49
|
+
return `${value.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
50
|
+
}
|
|
51
|
+
function wrapText(value, width) {
|
|
52
|
+
if (width <= 0) {
|
|
53
|
+
return [""];
|
|
54
|
+
}
|
|
55
|
+
const normalized = value.replace(/\r/g, "");
|
|
56
|
+
const output = [];
|
|
57
|
+
for (const sourceLine of normalized.split("\n")) {
|
|
58
|
+
if (!sourceLine) {
|
|
59
|
+
output.push("");
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let remaining = sourceLine;
|
|
63
|
+
while (remaining.length > width) {
|
|
64
|
+
let breakIndex = remaining.lastIndexOf(" ", width);
|
|
65
|
+
if (breakIndex <= 0) {
|
|
66
|
+
breakIndex = width;
|
|
67
|
+
}
|
|
68
|
+
output.push(remaining.slice(0, breakIndex).trimEnd());
|
|
69
|
+
remaining = remaining.slice(breakIndex).trimStart();
|
|
70
|
+
}
|
|
71
|
+
output.push(remaining);
|
|
72
|
+
}
|
|
73
|
+
return output.length > 0 ? output : [""];
|
|
74
|
+
}
|
|
75
|
+
function containsPoint(rect, x, y) {
|
|
76
|
+
return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
|
|
77
|
+
}
|
|
78
|
+
function findClickableRegion(regions, x, y) {
|
|
79
|
+
return regions.find((region) => containsPoint(region.rect, x, y)) ?? null;
|
|
80
|
+
}
|
|
81
|
+
function findScrollRegion(regions, x, y) {
|
|
82
|
+
return regions.find((region) => containsPoint(region.rect, x, y)) ?? null;
|
|
83
|
+
}
|
|
84
|
+
function routeWheelDeltaToScrollRegion(regions, x, y, delta) {
|
|
85
|
+
const region = findScrollRegion(regions, x, y);
|
|
86
|
+
if (!region) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
region.onFocus?.();
|
|
90
|
+
region.onScroll(scrollOffsetByDelta(region.state, delta));
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function pushNavigationEntry(stack, entry) {
|
|
94
|
+
return [...stack, entry];
|
|
95
|
+
}
|
|
96
|
+
function popNavigationEntry(stack) {
|
|
97
|
+
if (stack.length === 0) {
|
|
98
|
+
return { nextStack: stack, popped: null };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
nextStack: stack.slice(0, -1),
|
|
102
|
+
popped: stack.at(-1) ?? null
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function AppFrame(props) {
|
|
106
|
+
return React.createElement(
|
|
107
|
+
Box,
|
|
108
|
+
{ flexDirection: "column", width: props.layout.columns, height: props.layout.totalHeight, overflow: "hidden" },
|
|
109
|
+
React.createElement(Box, { flexDirection: "column", height: props.layout.topBarHeight, overflow: "hidden" }, props.topBar),
|
|
110
|
+
React.createElement(Box, { height: props.layout.bodyHeight, overflow: "hidden" }, props.body),
|
|
111
|
+
React.createElement(Box, { flexDirection: "column", height: props.layout.footerHeight, overflow: "hidden" }, props.footer)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
function TopTabs(props) {
|
|
115
|
+
const line = `${props.prefix ?? ""}${props.items.map((item) => item.id === props.activeId ? `[${item.label}]` : item.label).join(" ")}`;
|
|
116
|
+
return React.createElement(Text, {
|
|
117
|
+
color: props.focused ? "black" : "cyan",
|
|
118
|
+
backgroundColor: props.focused ? "cyan" : void 0
|
|
119
|
+
}, truncateLine(props.title ? `${props.title} ${line}` : line, props.width));
|
|
120
|
+
}
|
|
121
|
+
function SidebarList(props) {
|
|
122
|
+
const topIndicator = props.scrollState && props.scrollState.offset > 0 ? "\u2191 more" : "";
|
|
123
|
+
const bottomIndicator = props.scrollState && props.scrollState.offset + props.scrollState.viewportSize < props.scrollState.totalSize ? "\u2193 more" : "";
|
|
124
|
+
const bodyHeight = Math.max(1, props.height - 2 - (props.title ? 1 : 0) - (topIndicator ? 1 : 0) - (props.scrollState ? 1 : 0));
|
|
125
|
+
return React.createElement(
|
|
126
|
+
Box,
|
|
127
|
+
{ flexDirection: "column", width: props.width, height: props.height, borderStyle: "round", borderColor: props.focused ? "cyan" : "gray", overflow: "hidden" },
|
|
128
|
+
...props.title ? [React.createElement(Text, { key: "title", color: "yellow", bold: true }, truncateLine(props.title, props.width - 2))] : [],
|
|
129
|
+
...topIndicator ? [React.createElement(Text, { key: "top-indicator", color: "gray" }, truncateLine(topIndicator, props.width - 2))] : [],
|
|
130
|
+
...Array.from({ length: bodyHeight }, (_, index) => {
|
|
131
|
+
const item = props.items[index];
|
|
132
|
+
return React.createElement(
|
|
133
|
+
Text,
|
|
134
|
+
{
|
|
135
|
+
key: `sidebar-${item?.id ?? index}`,
|
|
136
|
+
color: item?.active ? "black" : item?.tone === "required" ? "yellow" : "white",
|
|
137
|
+
backgroundColor: item?.active ? "green" : void 0
|
|
138
|
+
},
|
|
139
|
+
truncateLine(item?.label ?? "", props.width - 2)
|
|
140
|
+
);
|
|
141
|
+
}),
|
|
142
|
+
...props.scrollState ? [React.createElement(
|
|
143
|
+
Text,
|
|
144
|
+
{ key: "scroll-status", color: "gray" },
|
|
145
|
+
truncateLine(
|
|
146
|
+
`${bottomIndicator || ""} ${props.scrollState.totalSize === 0 ? "0 items" : `${Math.min(props.scrollState.totalSize, props.scrollState.offset + 1)}-${Math.min(props.scrollState.totalSize, props.scrollState.offset + props.scrollState.viewportSize)} of ${props.scrollState.totalSize}`}`.trim(),
|
|
147
|
+
props.width - 2
|
|
148
|
+
)
|
|
149
|
+
)] : []
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
function ScrollPanel(props) {
|
|
153
|
+
const headerRows = props.title ? 1 : 0;
|
|
154
|
+
const footerRows = props.scrollState ? 1 : 0;
|
|
155
|
+
const contentRows = Math.max(1, props.height - 2 - headerRows - footerRows);
|
|
156
|
+
const topIndicator = props.scrollState && props.scrollState.offset > 0 ? "\u2191" : " ";
|
|
157
|
+
const bottomIndicator = props.scrollState && props.scrollState.offset + props.scrollState.viewportSize < props.scrollState.totalSize ? "\u2193" : " ";
|
|
158
|
+
return React.createElement(
|
|
159
|
+
Box,
|
|
160
|
+
{ flexDirection: "column", width: props.width, height: props.height, borderStyle: "round", borderColor: props.focused ? "cyan" : props.tone === "accent" ? "green" : "gray", overflow: "hidden" },
|
|
161
|
+
...props.title ? [React.createElement(Text, { key: "title", color: "yellow", bold: true }, truncateLine(props.title, props.width - 2))] : [],
|
|
162
|
+
...Array.from({ length: contentRows }, (_, index) => React.createElement(
|
|
163
|
+
Text,
|
|
164
|
+
{ key: `line-${index}` },
|
|
165
|
+
truncateLine(props.lines[index] ?? "", props.width - 2)
|
|
166
|
+
)),
|
|
167
|
+
...props.scrollState ? [React.createElement(
|
|
168
|
+
Text,
|
|
169
|
+
{ key: "scroll-status", color: "gray" },
|
|
170
|
+
truncateLine(
|
|
171
|
+
`${topIndicator}${bottomIndicator} lines ${props.scrollState.totalSize === 0 ? "0-0" : `${Math.min(props.scrollState.totalSize, props.scrollState.offset + 1)}-${Math.min(props.scrollState.totalSize, props.scrollState.offset + props.scrollState.viewportSize)}`} of ${props.scrollState.totalSize}`,
|
|
172
|
+
props.width - 2
|
|
173
|
+
)
|
|
174
|
+
)] : []
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
function FieldCard(props) {
|
|
178
|
+
return React.createElement(
|
|
179
|
+
Box,
|
|
180
|
+
{ flexDirection: "column", width: props.width, height: props.height, borderStyle: "round", borderColor: props.focused ? "cyan" : "blue", overflow: "hidden" },
|
|
181
|
+
React.createElement(Text, { color: "blue", bold: true }, truncateLine(props.title, props.width - 2)),
|
|
182
|
+
...Array.from({ length: props.height - 3 }, (_, index) => React.createElement(
|
|
183
|
+
Text,
|
|
184
|
+
{ key: `field-${index}`, color: index === 0 ? "white" : "gray" },
|
|
185
|
+
truncateLine(props.lines[index] ?? "", props.width - 2)
|
|
186
|
+
))
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
function TextInputField(props) {
|
|
190
|
+
const height = props.height ?? 4;
|
|
191
|
+
const safeCursor = Math.max(0, Math.min(props.cursorPosition ?? props.value.length, props.value.length));
|
|
192
|
+
const visibleValue = props.secret && props.value ? "\u2022".repeat(props.value.length) : props.value;
|
|
193
|
+
const placeholder = props.placeholder ?? "(empty)";
|
|
194
|
+
let inputLine = visibleValue;
|
|
195
|
+
if (props.focused) {
|
|
196
|
+
const beforeCursor = visibleValue.slice(0, safeCursor);
|
|
197
|
+
const afterCursor = visibleValue.slice(safeCursor);
|
|
198
|
+
inputLine = `${beforeCursor}\u2588${afterCursor}`;
|
|
199
|
+
} else if (!inputLine) {
|
|
200
|
+
inputLine = placeholder;
|
|
201
|
+
}
|
|
202
|
+
return React.createElement(FieldCard, {
|
|
203
|
+
width: props.width,
|
|
204
|
+
height,
|
|
205
|
+
title: props.label,
|
|
206
|
+
focused: props.focused,
|
|
207
|
+
lines: [
|
|
208
|
+
inputLine || "\u2588",
|
|
209
|
+
props.value.length > 0 ? "Type a replacement value or leave this as-is." : placeholder
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function TextAreaField(props) {
|
|
214
|
+
return React.createElement(FieldCard, {
|
|
215
|
+
width: props.width,
|
|
216
|
+
height: props.height,
|
|
217
|
+
title: props.label,
|
|
218
|
+
focused: props.focused,
|
|
219
|
+
lines: wrapText(props.value || "(empty)", Math.max(1, props.width - 2))
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function ActionButton(props) {
|
|
223
|
+
const color = props.tone === "primary" ? "black" : "white";
|
|
224
|
+
const backgroundColor = props.tone === "primary" ? "green" : "gray";
|
|
225
|
+
return React.createElement(
|
|
226
|
+
Text,
|
|
227
|
+
{
|
|
228
|
+
color: props.focused ? "black" : color,
|
|
229
|
+
backgroundColor: props.focused ? "cyan" : backgroundColor
|
|
230
|
+
},
|
|
231
|
+
truncateLine(`[ ${props.label} ]`, props.width ?? `[ ${props.label} ]`.length)
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
function PrimaryButton(props) {
|
|
235
|
+
return React.createElement(ActionButton, { ...props, tone: "primary" });
|
|
236
|
+
}
|
|
237
|
+
function SecondaryButton(props) {
|
|
238
|
+
return React.createElement(ActionButton, { ...props, tone: "secondary" });
|
|
239
|
+
}
|
|
240
|
+
function StatusBar(props) {
|
|
241
|
+
return React.createElement(
|
|
242
|
+
Box,
|
|
243
|
+
{ flexDirection: "column", width: props.width, overflow: "hidden" },
|
|
244
|
+
React.createElement(Text, { color: props.accent ? "cyan" : "gray" }, truncateLine(props.primary, props.width)),
|
|
245
|
+
React.createElement(Text, { color: "gray" }, truncateLine(props.secondary ?? "", props.width))
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function EmptyState(props) {
|
|
249
|
+
return React.createElement(
|
|
250
|
+
FieldCard,
|
|
251
|
+
{
|
|
252
|
+
width: props.width,
|
|
253
|
+
height: props.height,
|
|
254
|
+
title: props.title,
|
|
255
|
+
lines: wrapText(props.message, Math.max(1, props.width - 2))
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
function ConfirmDialog(props) {
|
|
260
|
+
return React.createElement(
|
|
261
|
+
FieldCard,
|
|
262
|
+
{
|
|
263
|
+
width: props.width,
|
|
264
|
+
height: 6,
|
|
265
|
+
title: props.title,
|
|
266
|
+
lines: wrapText(props.message, Math.max(1, props.width - 2))
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
export {
|
|
271
|
+
AppFrame,
|
|
272
|
+
ConfirmDialog,
|
|
273
|
+
EmptyState,
|
|
274
|
+
FieldCard,
|
|
275
|
+
PrimaryButton,
|
|
276
|
+
ScrollPanel,
|
|
277
|
+
SecondaryButton,
|
|
278
|
+
SidebarList,
|
|
279
|
+
StatusBar,
|
|
280
|
+
TextAreaField,
|
|
281
|
+
TextInputField,
|
|
282
|
+
TopTabs,
|
|
283
|
+
clampOffset,
|
|
284
|
+
computeViewportLayout,
|
|
285
|
+
containsPoint,
|
|
286
|
+
ensureVisible,
|
|
287
|
+
findClickableRegion,
|
|
288
|
+
findScrollRegion,
|
|
289
|
+
popNavigationEntry,
|
|
290
|
+
pushNavigationEntry,
|
|
291
|
+
routeWheelDeltaToScrollRegion,
|
|
292
|
+
scrollOffsetByDelta,
|
|
293
|
+
scrollOffsetByPage,
|
|
294
|
+
truncateLine,
|
|
295
|
+
wrapText
|
|
296
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type TerminalMouseEvent = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
button: 'left' | 'middle' | 'right' | 'scroll-up' | 'scroll-down' | 'unknown';
|
|
5
|
+
action: 'press' | 'release' | 'drag';
|
|
6
|
+
shift: boolean;
|
|
7
|
+
meta: boolean;
|
|
8
|
+
ctrl: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare function parseTerminalMouseInput(input: string): TerminalMouseEvent[];
|
|
11
|
+
export declare function useTerminalMouse(onEvent: (event: TerminalMouseEvent) => void): void;
|