toolcraft 0.0.1
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 +91 -0
- package/dist/cli.compile-check.d.ts +1 -0
- package/dist/cli.compile-check.js +26 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +1312 -0
- package/dist/index.compile-check.d.ts +1 -0
- package/dist/index.compile-check.js +50 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +366 -0
- package/dist/mcp.compile-check.d.ts +1 -0
- package/dist/mcp.compile-check.js +26 -0
- package/dist/mcp.d.ts +31 -0
- package/dist/mcp.js +354 -0
- package/dist/number-schema.d.ts +3 -0
- package/dist/number-schema.js +8 -0
- package/dist/renderer.d.ts +5 -0
- package/dist/renderer.js +148 -0
- package/dist/schema-scope.d.ts +4 -0
- package/dist/schema-scope.js +34 -0
- package/dist/sdk.compile-check.d.ts +1 -0
- package/dist/sdk.compile-check.js +79 -0
- package/dist/sdk.d.ts +63 -0
- package/dist/sdk.js +218 -0
- package/node_modules/@poe-code/design-system/dist/acp/components.d.ts +11 -0
- package/node_modules/@poe-code/design-system/dist/acp/components.js +121 -0
- package/node_modules/@poe-code/design-system/dist/acp/index.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/acp/index.js +2 -0
- package/node_modules/@poe-code/design-system/dist/acp/writer.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/acp/writer.js +21 -0
- package/node_modules/@poe-code/design-system/dist/components/command-errors.d.ts +16 -0
- package/node_modules/@poe-code/design-system/dist/components/command-errors.js +22 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.d.ts +20 -0
- package/node_modules/@poe-code/design-system/dist/components/help-formatter.js +27 -0
- package/node_modules/@poe-code/design-system/dist/components/index.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/components/index.js +7 -0
- package/node_modules/@poe-code/design-system/dist/components/logger.d.ts +11 -0
- package/node_modules/@poe-code/design-system/dist/components/logger.js +60 -0
- package/node_modules/@poe-code/design-system/dist/components/symbols.d.ts +12 -0
- package/node_modules/@poe-code/design-system/dist/components/symbols.js +71 -0
- package/node_modules/@poe-code/design-system/dist/components/table.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/components/table.js +74 -0
- package/node_modules/@poe-code/design-system/dist/components/text.d.ts +14 -0
- package/node_modules/@poe-code/design-system/dist/components/text.js +104 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.d.ts +18 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/ansi.js +298 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.d.ts +25 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/buffer.js +189 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/border.d.ts +9 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/border.js +123 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/footer.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/footer.js +57 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.d.ts +12 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/output-pane.js +254 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/stats-pane.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/components/stats-pane.js +121 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/dashboard.d.ts +20 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/dashboard.js +167 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/demo.d.ts +13 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/demo.js +145 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/index.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/index.js +4 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.d.ts +3 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/keymap.js +99 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/layout.d.ts +25 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/layout.js +79 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/should-use-dashboard.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/should-use-dashboard.js +7 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/snapshot.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/snapshot.js +68 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/store.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/store.js +51 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal.d.ts +37 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/terminal.js +233 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.d.ts +36 -0
- package/node_modules/@poe-code/design-system/dist/dashboard/types.js +1 -0
- package/node_modules/@poe-code/design-system/dist/index.d.ts +33 -0
- package/node_modules/@poe-code/design-system/dist/index.js +31 -0
- package/node_modules/@poe-code/design-system/dist/internal/output-format.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/internal/output-format.js +22 -0
- package/node_modules/@poe-code/design-system/dist/internal/strip-ansi.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/internal/strip-ansi.js +3 -0
- package/node_modules/@poe-code/design-system/dist/internal/theme-detect.d.ts +11 -0
- package/node_modules/@poe-code/design-system/dist/internal/theme-detect.js +49 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.d.ts +66 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +132 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/cancel.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/cancel.js +9 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/intro.js +15 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.d.ts +18 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/log.js +101 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/note.js +39 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/outro.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/outro.js +16 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/spinner.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/prompts/primitives/spinner.js +74 -0
- package/node_modules/@poe-code/design-system/dist/prompts/theme.d.ts +11 -0
- package/node_modules/@poe-code/design-system/dist/prompts/theme.js +12 -0
- package/node_modules/@poe-code/design-system/dist/static/index.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/static/index.js +2 -0
- package/node_modules/@poe-code/design-system/dist/static/menu.d.ts +11 -0
- package/node_modules/@poe-code/design-system/dist/static/menu.js +36 -0
- package/node_modules/@poe-code/design-system/dist/static/spinner.d.ts +14 -0
- package/node_modules/@poe-code/design-system/dist/static/spinner.js +46 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/ast.d.ts +92 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/ast.js +1 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/demo-content.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/demo-content.js +139 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/index.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/index.js +8 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/block.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/block.js +1495 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.d.ts +8 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/frontmatter.js +412 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/inline.d.ts +10 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser/inline.js +1166 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser.d.ts +5 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/parser.js +42 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/renderer.js +572 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/testing/theme-render-fixture.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/terminal-markdown/testing/theme-render-fixture.js +27 -0
- package/node_modules/@poe-code/design-system/dist/tokens/colors.d.ts +35 -0
- package/node_modules/@poe-code/design-system/dist/tokens/colors.js +34 -0
- package/node_modules/@poe-code/design-system/dist/tokens/index.d.ts +4 -0
- package/node_modules/@poe-code/design-system/dist/tokens/index.js +4 -0
- package/node_modules/@poe-code/design-system/dist/tokens/spacing.d.ts +6 -0
- package/node_modules/@poe-code/design-system/dist/tokens/spacing.js +6 -0
- package/node_modules/@poe-code/design-system/dist/tokens/typography.d.ts +7 -0
- package/node_modules/@poe-code/design-system/dist/tokens/typography.js +8 -0
- package/node_modules/@poe-code/design-system/dist/tokens/widths.d.ts +5 -0
- package/node_modules/@poe-code/design-system/dist/tokens/widths.js +5 -0
- package/node_modules/@poe-code/design-system/package.json +25 -0
- package/package.json +57 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { getTheme } from "../../internal/theme-detect.js";
|
|
2
|
+
import { light } from "../../tokens/colors.js";
|
|
3
|
+
export function renderStatsPane(buffer, rect, stats) {
|
|
4
|
+
buffer.clearRect(rect);
|
|
5
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const lines = statsToLines(stats, rect.width);
|
|
9
|
+
for (let row = 0; row < rect.height; row += 1) {
|
|
10
|
+
const line = lines[row];
|
|
11
|
+
if (line === undefined) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (line.prefix.length > 0) {
|
|
15
|
+
buffer.putInRect(rect, row, line.prefix, line.prefixStyle);
|
|
16
|
+
}
|
|
17
|
+
if (line.text.length === 0) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
const textStart = Math.min(line.prefix.length, rect.width);
|
|
21
|
+
buffer.putInRect({ x: rect.x + textStart, y: rect.y + row, width: rect.width - textStart, height: 1 }, 0, line.text, line.style);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function formatElapsed(ms) {
|
|
25
|
+
const safeMs = Number.isFinite(ms) ? ms : 0;
|
|
26
|
+
const totalSeconds = Math.max(0, Math.floor(safeMs / 1_000));
|
|
27
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
28
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
29
|
+
const seconds = totalSeconds % 60;
|
|
30
|
+
return [hours, minutes, seconds].map((value) => value.toString().padStart(2, "0")).join(":");
|
|
31
|
+
}
|
|
32
|
+
export function formatNumber(n) {
|
|
33
|
+
return new Intl.NumberFormat("en-US").format(n);
|
|
34
|
+
}
|
|
35
|
+
export function statsToLines(stats, width) {
|
|
36
|
+
if (width <= 0) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const mutedStyle = getToneStyle("muted");
|
|
40
|
+
const totalTokens = stats.tokensIn + stats.tokensOut;
|
|
41
|
+
const iterationsLabel = stats.iterationsLabel ?? "Iteration";
|
|
42
|
+
const lines = [
|
|
43
|
+
createKeyValueLine("Status", formatStatus(stats.status), width, getStatusStyle(stats.status)),
|
|
44
|
+
createKeyValueLine(iterationsLabel, formatNumber(stats.iterations), width),
|
|
45
|
+
createKeyValueLine("Elapsed", formatElapsed(stats.elapsedMs), width),
|
|
46
|
+
createBlankLine(),
|
|
47
|
+
createKeyValueLine("Tokens In", formatNumber(stats.tokensIn), width),
|
|
48
|
+
createKeyValueLine("Tokens Out", formatNumber(stats.tokensOut), width),
|
|
49
|
+
createKeyValueLine("Total", formatNumber(totalTokens), width)
|
|
50
|
+
];
|
|
51
|
+
if (stats.currentAction !== undefined) {
|
|
52
|
+
lines.push(createBlankLine(), {
|
|
53
|
+
prefix: clipText("Current:", width),
|
|
54
|
+
prefixStyle: {},
|
|
55
|
+
style: {},
|
|
56
|
+
text: ""
|
|
57
|
+
}, {
|
|
58
|
+
prefix: width > 0 ? clipText(" ", width) : "",
|
|
59
|
+
prefixStyle: mutedStyle,
|
|
60
|
+
style: mutedStyle,
|
|
61
|
+
text: clipText(stats.currentAction, Math.max(width - 2, 0))
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return lines;
|
|
65
|
+
}
|
|
66
|
+
function createBlankLine() {
|
|
67
|
+
return {
|
|
68
|
+
prefix: "",
|
|
69
|
+
prefixStyle: {},
|
|
70
|
+
style: {},
|
|
71
|
+
text: ""
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function createKeyValueLine(label, value, width, valueStyle = {}) {
|
|
75
|
+
const clippedValue = clipText(value, width);
|
|
76
|
+
const availableBeforeValue = Math.max(width - clippedValue.length, 0);
|
|
77
|
+
const clippedLabel = clipText(label, Math.max(availableBeforeValue - 1, 0));
|
|
78
|
+
return {
|
|
79
|
+
prefix: clippedLabel + " ".repeat(Math.max(availableBeforeValue - clippedLabel.length, 0)),
|
|
80
|
+
prefixStyle: {},
|
|
81
|
+
style: valueStyle,
|
|
82
|
+
text: clippedValue
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function clipText(value, width) {
|
|
86
|
+
return width <= 0 ? "" : value.slice(0, width);
|
|
87
|
+
}
|
|
88
|
+
function formatStatus(status) {
|
|
89
|
+
return `${status.slice(0, 1).toUpperCase()}${status.slice(1)}`;
|
|
90
|
+
}
|
|
91
|
+
function getStatusStyle(status) {
|
|
92
|
+
if (status === "running") {
|
|
93
|
+
return getToneStyle("info");
|
|
94
|
+
}
|
|
95
|
+
if (status === "paused") {
|
|
96
|
+
return getToneStyle("warning");
|
|
97
|
+
}
|
|
98
|
+
if (status === "error") {
|
|
99
|
+
return getToneStyle("error");
|
|
100
|
+
}
|
|
101
|
+
if (status === "done") {
|
|
102
|
+
return getToneStyle("success");
|
|
103
|
+
}
|
|
104
|
+
return getToneStyle("muted");
|
|
105
|
+
}
|
|
106
|
+
function getToneStyle(tone) {
|
|
107
|
+
const isLightTheme = getTheme() === light;
|
|
108
|
+
if (tone === "muted") {
|
|
109
|
+
return isLightTheme ? { fg: "#666666" } : { dim: true };
|
|
110
|
+
}
|
|
111
|
+
if (tone === "info") {
|
|
112
|
+
return isLightTheme ? { fg: "#a200ff" } : { fg: "magenta" };
|
|
113
|
+
}
|
|
114
|
+
if (tone === "warning") {
|
|
115
|
+
return isLightTheme ? { fg: "#cc6600" } : { fg: "yellow" };
|
|
116
|
+
}
|
|
117
|
+
if (tone === "error") {
|
|
118
|
+
return isLightTheme ? { fg: "#cc0000" } : { fg: "red" };
|
|
119
|
+
}
|
|
120
|
+
return isLightTheme ? { fg: "#008800" } : { fg: "green" };
|
|
121
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { FooterHint } from "./components/footer.js";
|
|
2
|
+
import type { Command, DashboardStats, OutputItem } from "./types.js";
|
|
3
|
+
export type DashboardOptions = {
|
|
4
|
+
title?: string;
|
|
5
|
+
statsTitle?: string;
|
|
6
|
+
keymap?: Partial<Record<Command, string[]>>;
|
|
7
|
+
rightPaneWidth?: number;
|
|
8
|
+
hints?: FooterHint[];
|
|
9
|
+
stdin?: NodeJS.ReadStream;
|
|
10
|
+
stdout?: NodeJS.WriteStream;
|
|
11
|
+
};
|
|
12
|
+
export type Dashboard = {
|
|
13
|
+
start(): void;
|
|
14
|
+
stop(): void;
|
|
15
|
+
appendOutput(item: OutputItem): void;
|
|
16
|
+
updateStats(stats: Partial<DashboardStats>): void;
|
|
17
|
+
onCommand(handler: (cmd: Command) => void): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
};
|
|
20
|
+
export declare function createDashboard(opts?: DashboardOptions): Dashboard;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createLogger } from "../components/logger.js";
|
|
2
|
+
import { resolveOutputFormat } from "../internal/output-format.js";
|
|
3
|
+
import { ScreenBuffer, diff } from "./buffer.js";
|
|
4
|
+
import { renderBorder } from "./components/border.js";
|
|
5
|
+
import { defaultHints, renderFooter } from "./components/footer.js";
|
|
6
|
+
import { renderOutputPane } from "./components/output-pane.js";
|
|
7
|
+
import { renderStatsPane } from "./components/stats-pane.js";
|
|
8
|
+
import { createKeymap } from "./keymap.js";
|
|
9
|
+
import { computeDashboardLayout } from "./layout.js";
|
|
10
|
+
import { createStore } from "./store.js";
|
|
11
|
+
import { createTerminalDriver } from "./terminal.js";
|
|
12
|
+
const DEFAULT_TITLE = "Output";
|
|
13
|
+
const DEFAULT_STATS_TITLE = "Stats";
|
|
14
|
+
const DEFAULT_RIGHT_PANE_WIDTH = 25;
|
|
15
|
+
export function createDashboard(opts = {}) {
|
|
16
|
+
const stdin = opts.stdin ?? process.stdin;
|
|
17
|
+
const stdout = opts.stdout ?? process.stdout;
|
|
18
|
+
const resolveCommand = createKeymap(opts.keymap);
|
|
19
|
+
const footerHints = opts.hints ?? defaultHints();
|
|
20
|
+
const title = opts.title ?? DEFAULT_TITLE;
|
|
21
|
+
const statsTitle = opts.statsTitle ?? DEFAULT_STATS_TITLE;
|
|
22
|
+
const rightPaneWidth = opts.rightPaneWidth ?? DEFAULT_RIGHT_PANE_WIDTH;
|
|
23
|
+
const commandHandlers = new Set();
|
|
24
|
+
const fallbackLogger = createLogger((message) => {
|
|
25
|
+
stdout.write(`${message}\n`);
|
|
26
|
+
});
|
|
27
|
+
let driver;
|
|
28
|
+
let store;
|
|
29
|
+
let previousBuffer = new ScreenBuffer(0, 0);
|
|
30
|
+
let unsubscribeStore;
|
|
31
|
+
let unsubscribeKeypress;
|
|
32
|
+
let unsubscribeResize;
|
|
33
|
+
let started = false;
|
|
34
|
+
let destroyed = false;
|
|
35
|
+
function appendOutput(item) {
|
|
36
|
+
if (destroyed) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!isTerminalMode()) {
|
|
40
|
+
writeFallbackOutput(item);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
getStore().appendOutput(item);
|
|
44
|
+
}
|
|
45
|
+
function updateStats(stats) {
|
|
46
|
+
if (destroyed || !isTerminalMode()) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
getStore().updateStats(stats);
|
|
50
|
+
}
|
|
51
|
+
function start() {
|
|
52
|
+
if (destroyed || started || !isTerminalMode()) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
driver = createTerminalDriver({ stdin, stdout });
|
|
56
|
+
started = true;
|
|
57
|
+
previousBuffer = new ScreenBuffer(0, 0);
|
|
58
|
+
driver.enterRawMode();
|
|
59
|
+
driver.enterAltScreen();
|
|
60
|
+
driver.disableLineWrap();
|
|
61
|
+
driver.hideCursor();
|
|
62
|
+
render();
|
|
63
|
+
const activeStore = getStore();
|
|
64
|
+
unsubscribeStore = activeStore.onChange(() => {
|
|
65
|
+
render();
|
|
66
|
+
});
|
|
67
|
+
unsubscribeKeypress = driver.onKeypress((event) => {
|
|
68
|
+
const command = resolveCommand(event);
|
|
69
|
+
if (command === undefined) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
emitCommand(command);
|
|
73
|
+
});
|
|
74
|
+
unsubscribeResize = driver.onResize(() => {
|
|
75
|
+
render();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function stop() {
|
|
79
|
+
unsubscribeStore?.();
|
|
80
|
+
unsubscribeKeypress?.();
|
|
81
|
+
unsubscribeResize?.();
|
|
82
|
+
unsubscribeStore = undefined;
|
|
83
|
+
unsubscribeKeypress = undefined;
|
|
84
|
+
unsubscribeResize = undefined;
|
|
85
|
+
if (driver === undefined) {
|
|
86
|
+
started = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
driver.destroy();
|
|
90
|
+
driver = undefined;
|
|
91
|
+
previousBuffer = new ScreenBuffer(0, 0);
|
|
92
|
+
started = false;
|
|
93
|
+
}
|
|
94
|
+
function onCommand(handler) {
|
|
95
|
+
if (destroyed) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
commandHandlers.add(handler);
|
|
99
|
+
}
|
|
100
|
+
function destroy() {
|
|
101
|
+
if (destroyed) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
stop();
|
|
105
|
+
commandHandlers.clear();
|
|
106
|
+
store = undefined;
|
|
107
|
+
destroyed = true;
|
|
108
|
+
}
|
|
109
|
+
function getStore() {
|
|
110
|
+
store ??= createStore();
|
|
111
|
+
return store;
|
|
112
|
+
}
|
|
113
|
+
function render() {
|
|
114
|
+
if (driver === undefined) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { cols, rows } = driver.getSize();
|
|
118
|
+
const layout = computeDashboardLayout({
|
|
119
|
+
totalWidth: cols,
|
|
120
|
+
totalHeight: rows,
|
|
121
|
+
rightPaneWidth
|
|
122
|
+
});
|
|
123
|
+
const nextBuffer = new ScreenBuffer(cols, rows);
|
|
124
|
+
const state = getStore().getState();
|
|
125
|
+
renderBorder(nextBuffer, layout, {
|
|
126
|
+
leftTitle: title,
|
|
127
|
+
rightTitle: statsTitle,
|
|
128
|
+
style: { dim: true }
|
|
129
|
+
});
|
|
130
|
+
renderOutputPane(nextBuffer, layout.leftPane, state.output);
|
|
131
|
+
renderStatsPane(nextBuffer, layout.rightPane, state.stats);
|
|
132
|
+
renderFooter(nextBuffer, layout.footer, footerHints);
|
|
133
|
+
driver.flush(diff(previousBuffer, nextBuffer));
|
|
134
|
+
previousBuffer = nextBuffer;
|
|
135
|
+
}
|
|
136
|
+
function emitCommand(command) {
|
|
137
|
+
for (const handler of commandHandlers) {
|
|
138
|
+
handler(command);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function writeFallbackOutput(item) {
|
|
142
|
+
if (item.kind === "success") {
|
|
143
|
+
fallbackLogger.success(item.text);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (item.kind === "error") {
|
|
147
|
+
fallbackLogger.error(item.text);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (item.kind === "tool") {
|
|
151
|
+
fallbackLogger.message(item.text);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
fallbackLogger.info(item.text);
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
start,
|
|
158
|
+
stop,
|
|
159
|
+
appendOutput,
|
|
160
|
+
updateStats,
|
|
161
|
+
onCommand,
|
|
162
|
+
destroy
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function isTerminalMode() {
|
|
166
|
+
return resolveOutputFormat() === "terminal";
|
|
167
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Dashboard } from "./dashboard.js";
|
|
2
|
+
type DemoDashboard = Pick<Dashboard, "appendOutput" | "updateStats">;
|
|
3
|
+
type DemoRuntime = {
|
|
4
|
+
setInterval: typeof globalThis.setInterval;
|
|
5
|
+
clearInterval: typeof globalThis.clearInterval;
|
|
6
|
+
setTimeout: typeof globalThis.setTimeout;
|
|
7
|
+
clearTimeout: typeof globalThis.clearTimeout;
|
|
8
|
+
now: () => number;
|
|
9
|
+
random: () => number;
|
|
10
|
+
};
|
|
11
|
+
export declare function startDashboardDemo(dashboard: DemoDashboard, runtime?: Partial<DemoRuntime>): () => void;
|
|
12
|
+
export declare function main(): Promise<void>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createDashboard } from "./dashboard.js";
|
|
5
|
+
const OUTPUT_INTERVAL_MS = 500;
|
|
6
|
+
const STATS_INTERVAL_MS = 1_000;
|
|
7
|
+
const DEMO_DURATION_MS = 30_000;
|
|
8
|
+
const TOKENS_IN_PER_ITERATION = 137;
|
|
9
|
+
const TOKENS_OUT_PER_ITERATION = 89;
|
|
10
|
+
const OUTPUT_KINDS = ["info", "success", "error", "tool", "status"];
|
|
11
|
+
const OUTPUT_MESSAGES = {
|
|
12
|
+
info: [
|
|
13
|
+
"Analyzing repository state",
|
|
14
|
+
"Inspecting agent configuration",
|
|
15
|
+
"Collecting recent command output"
|
|
16
|
+
],
|
|
17
|
+
success: [
|
|
18
|
+
"Generated provider config",
|
|
19
|
+
"Updated dashboard layout",
|
|
20
|
+
"Saved session checkpoint"
|
|
21
|
+
],
|
|
22
|
+
error: [
|
|
23
|
+
"Retrying transient network request",
|
|
24
|
+
"Tool execution returned a non-zero exit code",
|
|
25
|
+
"Encountered a recoverable validation error"
|
|
26
|
+
],
|
|
27
|
+
tool: [
|
|
28
|
+
"Running npm test -- --runInBand",
|
|
29
|
+
"Executing npm run lint:types",
|
|
30
|
+
"Opening task plan documentation"
|
|
31
|
+
],
|
|
32
|
+
status: [
|
|
33
|
+
"Waiting for follow-up task",
|
|
34
|
+
"Streaming model response",
|
|
35
|
+
"Syncing derived metrics"
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
const RUNNING_ACTIONS = [
|
|
39
|
+
"Planning next step",
|
|
40
|
+
"Executing tool call",
|
|
41
|
+
"Reviewing tool results",
|
|
42
|
+
"Updating working memory",
|
|
43
|
+
"Preparing final response"
|
|
44
|
+
];
|
|
45
|
+
const INITIAL_ACTION = "Connecting to provider";
|
|
46
|
+
const COMPLETED_ACTION = "Completed";
|
|
47
|
+
export function startDashboardDemo(dashboard, runtime = {}) {
|
|
48
|
+
const setIntervalFn = runtime.setInterval ?? globalThis.setInterval.bind(globalThis);
|
|
49
|
+
const clearIntervalFn = runtime.clearInterval ?? globalThis.clearInterval.bind(globalThis);
|
|
50
|
+
const setTimeoutFn = runtime.setTimeout ?? globalThis.setTimeout.bind(globalThis);
|
|
51
|
+
const clearTimeoutFn = runtime.clearTimeout ?? globalThis.clearTimeout.bind(globalThis);
|
|
52
|
+
const now = runtime.now ?? Date.now;
|
|
53
|
+
const random = runtime.random ?? Math.random;
|
|
54
|
+
let outputCount = 0;
|
|
55
|
+
let iterations = 0;
|
|
56
|
+
let cleanedUp = false;
|
|
57
|
+
dashboard.updateStats({
|
|
58
|
+
status: "running",
|
|
59
|
+
currentAction: INITIAL_ACTION
|
|
60
|
+
});
|
|
61
|
+
const outputTimer = setIntervalFn(() => {
|
|
62
|
+
const kind = OUTPUT_KINDS[outputCount % OUTPUT_KINDS.length] ?? "info";
|
|
63
|
+
dashboard.appendOutput({
|
|
64
|
+
kind,
|
|
65
|
+
text: pickOutputMessage(kind, random),
|
|
66
|
+
ts: now()
|
|
67
|
+
});
|
|
68
|
+
outputCount += 1;
|
|
69
|
+
}, OUTPUT_INTERVAL_MS);
|
|
70
|
+
const statsTimer = setIntervalFn(() => {
|
|
71
|
+
iterations += 1;
|
|
72
|
+
dashboard.updateStats({
|
|
73
|
+
status: "running",
|
|
74
|
+
iterations,
|
|
75
|
+
tokensIn: iterations * TOKENS_IN_PER_ITERATION,
|
|
76
|
+
tokensOut: iterations * TOKENS_OUT_PER_ITERATION,
|
|
77
|
+
elapsedMs: iterations * STATS_INTERVAL_MS,
|
|
78
|
+
currentAction: RUNNING_ACTIONS[(iterations - 1) % RUNNING_ACTIONS.length] ?? INITIAL_ACTION
|
|
79
|
+
});
|
|
80
|
+
}, STATS_INTERVAL_MS);
|
|
81
|
+
const finishTimeout = setTimeoutFn(() => {
|
|
82
|
+
cleanup();
|
|
83
|
+
dashboard.updateStats({
|
|
84
|
+
status: "done",
|
|
85
|
+
iterations,
|
|
86
|
+
tokensIn: iterations * TOKENS_IN_PER_ITERATION,
|
|
87
|
+
tokensOut: iterations * TOKENS_OUT_PER_ITERATION,
|
|
88
|
+
elapsedMs: DEMO_DURATION_MS,
|
|
89
|
+
currentAction: COMPLETED_ACTION
|
|
90
|
+
});
|
|
91
|
+
}, DEMO_DURATION_MS);
|
|
92
|
+
function cleanup() {
|
|
93
|
+
if (cleanedUp) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
cleanedUp = true;
|
|
97
|
+
clearTimeoutFn(finishTimeout);
|
|
98
|
+
clearIntervalFn(outputTimer);
|
|
99
|
+
clearIntervalFn(statsTimer);
|
|
100
|
+
}
|
|
101
|
+
return cleanup;
|
|
102
|
+
}
|
|
103
|
+
function pickOutputMessage(kind, random) {
|
|
104
|
+
const options = OUTPUT_MESSAGES[kind];
|
|
105
|
+
const cappedRandom = Math.max(0, Math.min(0.999_999, random()));
|
|
106
|
+
const index = Math.floor(cappedRandom * options.length);
|
|
107
|
+
return options[index] ?? options[0] ?? kind;
|
|
108
|
+
}
|
|
109
|
+
export async function main() {
|
|
110
|
+
const dashboard = createDashboard({ title: "Agent Output", statsTitle: "Stats" });
|
|
111
|
+
const stopDemo = startDashboardDemo(dashboard);
|
|
112
|
+
let shutDown = false;
|
|
113
|
+
const shutdown = (exitCode) => {
|
|
114
|
+
if (shutDown) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
shutDown = true;
|
|
118
|
+
stopDemo();
|
|
119
|
+
dashboard.destroy();
|
|
120
|
+
if (exitCode !== undefined) {
|
|
121
|
+
process.exit(exitCode);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
dashboard.onCommand((command) => {
|
|
125
|
+
if (command === "quit") {
|
|
126
|
+
shutdown(0);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
process.once("SIGINT", () => {
|
|
130
|
+
shutdown(0);
|
|
131
|
+
});
|
|
132
|
+
process.once("SIGTERM", () => {
|
|
133
|
+
shutdown(0);
|
|
134
|
+
});
|
|
135
|
+
dashboard.start();
|
|
136
|
+
}
|
|
137
|
+
const entry = process.argv[1];
|
|
138
|
+
const isMain = typeof entry === "string" && path.resolve(entry) === fileURLToPath(import.meta.url);
|
|
139
|
+
if (isMain) {
|
|
140
|
+
main().catch((error) => {
|
|
141
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
142
|
+
process.stderr.write(`${message}\n`);
|
|
143
|
+
process.exitCode = 1;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createDashboard } from "./dashboard.js";
|
|
2
|
+
export { shouldUseInteractiveDashboard } from "./should-use-dashboard.js";
|
|
3
|
+
export type { Dashboard, DashboardOptions } from "./dashboard.js";
|
|
4
|
+
export { renderDashboardSnapshot } from "./snapshot.js";
|
|
5
|
+
export type { SnapshotOptions } from "./snapshot.js";
|
|
6
|
+
export type { OutputItem, OutputItemKind, DashboardStats, Command, DashboardState } from "./types.js";
|
|
7
|
+
export { defaultHints } from "./components/footer.js";
|
|
8
|
+
export type { FooterHint } from "./components/footer.js";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const commands = ["forceQuit", "quit", "edit", "pause", "retry", "view-log"];
|
|
2
|
+
const defaultBindings = {
|
|
3
|
+
forceQuit: ["Ctrl+C"],
|
|
4
|
+
quit: ["q"],
|
|
5
|
+
edit: ["e"],
|
|
6
|
+
pause: ["p"],
|
|
7
|
+
retry: ["r"],
|
|
8
|
+
"view-log": ["l"]
|
|
9
|
+
};
|
|
10
|
+
export function createKeymap(overrides) {
|
|
11
|
+
const bindings = new Map();
|
|
12
|
+
for (const command of commands) {
|
|
13
|
+
const keys = overrides?.[command] ?? defaultBindings[command];
|
|
14
|
+
bindings.set(command, keys
|
|
15
|
+
.map(parseBinding)
|
|
16
|
+
.filter((binding) => binding !== undefined));
|
|
17
|
+
}
|
|
18
|
+
return (event) => {
|
|
19
|
+
for (const command of commands) {
|
|
20
|
+
const commandBindings = bindings.get(command);
|
|
21
|
+
if (commandBindings?.some((binding) => matches(binding, event))) {
|
|
22
|
+
return command;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function parseBinding(binding) {
|
|
29
|
+
const value = binding.trim();
|
|
30
|
+
if (value.length === 0) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
const parts = value.split("+").map((part) => part.trim()).filter(Boolean);
|
|
34
|
+
if (parts.length === 0) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
let ctrl = false;
|
|
38
|
+
let meta = false;
|
|
39
|
+
let shift = false;
|
|
40
|
+
const key = parts.at(-1);
|
|
41
|
+
if (key === undefined) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
for (const modifier of parts.slice(0, -1)) {
|
|
45
|
+
const normalized = modifier.toLowerCase();
|
|
46
|
+
if (normalized === "ctrl" || normalized === "control") {
|
|
47
|
+
ctrl = true;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (normalized === "meta" || normalized === "alt") {
|
|
51
|
+
meta = true;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (normalized === "shift") {
|
|
55
|
+
shift = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (parts.length === 1 && isShiftedCharacter(key)) {
|
|
60
|
+
shift = true;
|
|
61
|
+
}
|
|
62
|
+
if (key.length === 1) {
|
|
63
|
+
return {
|
|
64
|
+
ch: normalizeBindingCharacter(key, shift),
|
|
65
|
+
ctrl,
|
|
66
|
+
meta,
|
|
67
|
+
shift
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
name: key.toLowerCase(),
|
|
72
|
+
ctrl,
|
|
73
|
+
meta,
|
|
74
|
+
shift
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function matches(binding, event) {
|
|
78
|
+
if (binding.ctrl !== event.ctrl ||
|
|
79
|
+
binding.meta !== event.meta ||
|
|
80
|
+
binding.shift !== event.shift) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (binding.ch !== undefined) {
|
|
84
|
+
return event.ch === binding.ch || event.name === binding.ch.toLowerCase();
|
|
85
|
+
}
|
|
86
|
+
if (binding.name !== undefined) {
|
|
87
|
+
return event.name === binding.name;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
function isShiftedCharacter(value) {
|
|
92
|
+
return value.length === 1 && value.toLowerCase() !== value && value.toUpperCase() === value;
|
|
93
|
+
}
|
|
94
|
+
function normalizeBindingCharacter(value, shift) {
|
|
95
|
+
if (!shift || value.toLowerCase() === value.toUpperCase()) {
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
return value.toUpperCase();
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Rect } from "./types.js";
|
|
2
|
+
export type LayoutOptions = {
|
|
3
|
+
totalWidth: number;
|
|
4
|
+
totalHeight: number;
|
|
5
|
+
rightPaneWidth?: number;
|
|
6
|
+
footerHeight?: number;
|
|
7
|
+
borderWidth?: number;
|
|
8
|
+
};
|
|
9
|
+
export type DashboardLayout = {
|
|
10
|
+
outerBorder: Rect;
|
|
11
|
+
leftPane: Rect;
|
|
12
|
+
rightPane: Rect;
|
|
13
|
+
divider: {
|
|
14
|
+
x: number;
|
|
15
|
+
top: number;
|
|
16
|
+
bottom: number;
|
|
17
|
+
};
|
|
18
|
+
footer: Rect;
|
|
19
|
+
footerDivider: {
|
|
20
|
+
y: number;
|
|
21
|
+
left: number;
|
|
22
|
+
right: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
export declare function computeDashboardLayout(opts: LayoutOptions): DashboardLayout;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const DEFAULT_RIGHT_PANE_WIDTH = 25;
|
|
2
|
+
const DEFAULT_FOOTER_HEIGHT = 1;
|
|
3
|
+
const DEFAULT_BORDER_WIDTH = 1;
|
|
4
|
+
const MIN_LEFT_PANE_WIDTH = 20;
|
|
5
|
+
export function computeDashboardLayout(opts) {
|
|
6
|
+
const totalWidth = normalizeSize(opts.totalWidth);
|
|
7
|
+
const totalHeight = normalizeSize(opts.totalHeight);
|
|
8
|
+
const borderWidth = normalizeSize(opts.borderWidth ?? DEFAULT_BORDER_WIDTH);
|
|
9
|
+
const footerHeight = normalizeSize(opts.footerHeight ?? DEFAULT_FOOTER_HEIGHT);
|
|
10
|
+
const requestedRightPaneWidth = normalizeSize(opts.rightPaneWidth ?? DEFAULT_RIGHT_PANE_WIDTH);
|
|
11
|
+
const maxX = Math.max(0, totalWidth - 1);
|
|
12
|
+
const maxY = Math.max(0, totalHeight - 1);
|
|
13
|
+
const outerBorder = { x: 0, y: 0, width: totalWidth, height: totalHeight };
|
|
14
|
+
const innerWidth = Math.max(0, totalWidth - (borderWidth * 2));
|
|
15
|
+
const innerHeight = Math.max(0, totalHeight - (borderWidth * 2));
|
|
16
|
+
const innerX = clampCoordinate(borderWidth, maxX);
|
|
17
|
+
const innerY = clampCoordinate(borderWidth, maxY);
|
|
18
|
+
const dividerWidth = innerWidth > 0 ? 1 : 0;
|
|
19
|
+
const availablePaneWidth = Math.max(0, innerWidth - dividerWidth);
|
|
20
|
+
const leftPaneWidth = computeLeftPaneWidth(availablePaneWidth, requestedRightPaneWidth);
|
|
21
|
+
const rightPaneWidth = Math.max(0, availablePaneWidth - leftPaneWidth);
|
|
22
|
+
const actualFooterHeight = Math.min(footerHeight, innerHeight);
|
|
23
|
+
const footerDividerHeight = innerHeight > actualFooterHeight ? 1 : 0;
|
|
24
|
+
const contentHeight = Math.max(0, innerHeight - actualFooterHeight - footerDividerHeight);
|
|
25
|
+
const leftPane = {
|
|
26
|
+
x: innerX,
|
|
27
|
+
y: innerY,
|
|
28
|
+
width: leftPaneWidth,
|
|
29
|
+
height: contentHeight
|
|
30
|
+
};
|
|
31
|
+
const dividerX = clampCoordinate(leftPane.x + leftPane.width, maxX);
|
|
32
|
+
const rightPane = {
|
|
33
|
+
x: clampCoordinate(dividerX + dividerWidth, maxX),
|
|
34
|
+
y: innerY,
|
|
35
|
+
width: rightPaneWidth,
|
|
36
|
+
height: contentHeight
|
|
37
|
+
};
|
|
38
|
+
const dividerTop = innerY;
|
|
39
|
+
const dividerBottom = clampCoordinate(dividerTop + Math.max(contentHeight - 1, 0), maxY);
|
|
40
|
+
const footerDividerY = clampCoordinate(innerY + contentHeight, maxY);
|
|
41
|
+
const footerY = clampCoordinate(footerDividerY + footerDividerHeight, maxY);
|
|
42
|
+
const footerDividerLeft = innerX;
|
|
43
|
+
const footerDividerRight = clampCoordinate(Math.max(innerX, totalWidth - borderWidth - 1), maxX);
|
|
44
|
+
return {
|
|
45
|
+
outerBorder,
|
|
46
|
+
leftPane,
|
|
47
|
+
rightPane,
|
|
48
|
+
divider: {
|
|
49
|
+
x: dividerX,
|
|
50
|
+
top: dividerTop,
|
|
51
|
+
bottom: dividerBottom
|
|
52
|
+
},
|
|
53
|
+
footer: {
|
|
54
|
+
x: innerX,
|
|
55
|
+
y: footerY,
|
|
56
|
+
width: innerWidth,
|
|
57
|
+
height: actualFooterHeight
|
|
58
|
+
},
|
|
59
|
+
footerDivider: {
|
|
60
|
+
y: footerDividerY,
|
|
61
|
+
left: footerDividerLeft,
|
|
62
|
+
right: footerDividerRight
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function computeLeftPaneWidth(availablePaneWidth, requestedRightPaneWidth) {
|
|
67
|
+
if (availablePaneWidth <= 0) {
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
const maxRightPaneWidth = Math.max(0, availablePaneWidth - MIN_LEFT_PANE_WIDTH);
|
|
71
|
+
const rightPaneWidth = Math.min(requestedRightPaneWidth, maxRightPaneWidth);
|
|
72
|
+
return Math.max(0, availablePaneWidth - rightPaneWidth);
|
|
73
|
+
}
|
|
74
|
+
function normalizeSize(value) {
|
|
75
|
+
return Math.max(0, Math.floor(value));
|
|
76
|
+
}
|
|
77
|
+
function clampCoordinate(value, max) {
|
|
78
|
+
return Math.max(0, Math.min(value, max));
|
|
79
|
+
}
|