feat: syntax-highlighted bash commands + fix backfill missing tool calls (#1347)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
14
bun.lock
14
bun.lock
@@ -6,7 +6,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-client": "^1.7.12",
|
"@letta-ai/letta-client": "^1.7.12",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
@@ -98,12 +100,16 @@
|
|||||||
|
|
||||||
"@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="],
|
"@types/diff": ["@types/diff@8.0.0", "", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="],
|
||||||
|
|
||||||
|
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
|
"@types/node": ["@types/node@24.9.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg=="],
|
||||||
|
|
||||||
"@types/picomatch": ["@types/picomatch@4.0.2", "", {}, "sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA=="],
|
"@types/picomatch": ["@types/picomatch@4.0.2", "", {}, "sha512-qHHxQ+P9PysNEGbALT8f8YOSHW0KJu6l2xU8DYY0fu/EmGxXdVnuTLvFUvBgPJMSqXq29SYHveejeAha+4AYgA=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
"@types/react": ["@types/react@19.2.9", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA=="],
|
||||||
|
|
||||||
|
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||||
|
|
||||||
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||||
|
|
||||||
"@vscode/ripgrep": ["@vscode/ripgrep@1.17.0", "", { "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", "yauzl": "^2.9.2" } }, "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g=="],
|
"@vscode/ripgrep": ["@vscode/ripgrep@1.17.0", "", { "dependencies": { "https-proxy-agent": "^7.0.2", "proxy-from-env": "^1.1.0", "yauzl": "^2.9.2" } }, "sha512-mBRKm+ASPkUcw4o9aAgfbusIu6H4Sdhw09bjeP1YOBFTJEZAnrnk6WZwzv8NEjgC82f7ILvhmb1WIElSugea6g=="],
|
||||||
@@ -154,8 +160,12 @@
|
|||||||
|
|
||||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||||
|
|
||||||
|
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
|
||||||
|
|
||||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||||
@@ -178,6 +188,8 @@
|
|||||||
|
|
||||||
"has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
|
"has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="],
|
||||||
|
|
||||||
|
"highlight.js": ["highlight.js@11.11.1", "", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="],
|
||||||
|
|
||||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
"husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
|
||||||
@@ -214,6 +226,8 @@
|
|||||||
|
|
||||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||||
|
|
||||||
|
"lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="],
|
||||||
|
|
||||||
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
|
"lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="],
|
||||||
|
|
||||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||||
|
|||||||
@@ -35,7 +35,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@letta-ai/letta-client": "^1.7.12",
|
"@letta-ai/letta-client": "^1.7.12",
|
||||||
"glob": "^13.0.0",
|
"glob": "^13.0.0",
|
||||||
|
"highlight.js": "^11.11.1",
|
||||||
"ink-link": "^5.0.0",
|
"ink-link": "^5.0.0",
|
||||||
|
"lowlight": "^3.3.0",
|
||||||
"open": "^10.2.0",
|
"open": "^10.2.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
|
|||||||
@@ -430,9 +430,7 @@ export async function getResumeData(
|
|||||||
return {
|
return {
|
||||||
pendingApproval,
|
pendingApproval,
|
||||||
pendingApprovals,
|
pendingApprovals,
|
||||||
messageHistory: prepareMessageHistory(messages, {
|
messageHistory: prepareMessageHistory(messages),
|
||||||
primaryOnly: true,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -445,7 +443,7 @@ export async function getResumeData(
|
|||||||
return {
|
return {
|
||||||
pendingApproval: null,
|
pendingApproval: null,
|
||||||
pendingApprovals: [],
|
pendingApprovals: [],
|
||||||
messageHistory: prepareMessageHistory(messages, { primaryOnly: true }),
|
messageHistory: prepareMessageHistory(messages),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// Use agent messages API for "default" conversation or when no conversation ID
|
// Use agent messages API for "default" conversation or when no conversation ID
|
||||||
@@ -520,9 +518,7 @@ export async function getResumeData(
|
|||||||
return {
|
return {
|
||||||
pendingApproval,
|
pendingApproval,
|
||||||
pendingApprovals,
|
pendingApprovals,
|
||||||
messageHistory: prepareMessageHistory(messages, {
|
messageHistory: prepareMessageHistory(messages),
|
||||||
primaryOnly: true,
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -535,7 +531,7 @@ export async function getResumeData(
|
|||||||
return {
|
return {
|
||||||
pendingApproval: null,
|
pendingApproval: null,
|
||||||
pendingApprovals: [],
|
pendingApprovals: [],
|
||||||
messageHistory: prepareMessageHistory(messages, { primaryOnly: true }),
|
messageHistory: prepareMessageHistory(messages),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -12990,6 +12990,7 @@ If using apply_patch, use this exact relative patch path: ${applyPatchRelativePa
|
|||||||
style={{ flexDirection: "column" }}
|
style={{ flexDirection: "column" }}
|
||||||
>
|
>
|
||||||
{(item: StaticItem, index: number) => {
|
{(item: StaticItem, index: number) => {
|
||||||
|
try {
|
||||||
return (
|
return (
|
||||||
<Box key={item.id} marginTop={index > 0 ? 1 : 0}>
|
<Box key={item.id} marginTop={index > 0 ? 1 : 0}>
|
||||||
{item.kind === "welcome" ? (
|
{item.kind === "welcome" ? (
|
||||||
@@ -13040,6 +13041,19 @@ If using apply_patch, use this exact relative patch path: ${applyPatchRelativePa
|
|||||||
) : null}
|
) : null}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`[Static render error] kind=${item.kind} id=${item.id}`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Box key={item.id}>
|
||||||
|
<Text color="red">
|
||||||
|
⚠ render error: {item.kind} ({String(err)})
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
</Static>
|
</Static>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useProgressIndicator } from "../hooks/useProgressIndicator";
|
|||||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||||
import { useTextInputCursor } from "../hooks/useTextInputCursor";
|
import { useTextInputCursor } from "../hooks/useTextInputCursor";
|
||||||
import { colors } from "./colors";
|
import { colors } from "./colors";
|
||||||
|
import { SyntaxHighlightedCommand } from "./SyntaxHighlightedCommand";
|
||||||
import { Text } from "./Text";
|
import { Text } from "./Text";
|
||||||
|
|
||||||
type BashInfo = {
|
type BashInfo = {
|
||||||
@@ -151,9 +152,11 @@ export const InlineBashApproval = memo(
|
|||||||
|
|
||||||
{/* Command preview */}
|
{/* Command preview */}
|
||||||
<Box paddingLeft={2} flexDirection="column">
|
<Box paddingLeft={2} flexDirection="column">
|
||||||
<Text>{bashInfo.command}</Text>
|
<SyntaxHighlightedCommand command={bashInfo.command} />
|
||||||
{bashInfo.description && (
|
{bashInfo.description && (
|
||||||
|
<Box marginTop={1}>
|
||||||
<Text dimColor>{bashInfo.description}</Text>
|
<Text dimColor>{bashInfo.description}</Text>
|
||||||
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
172
src/cli/components/SyntaxHighlightedCommand.tsx
Normal file
172
src/cli/components/SyntaxHighlightedCommand.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import type { ElementContent, RootContent } from "hast";
|
||||||
|
import { Box } from "ink";
|
||||||
|
import { common, createLowlight } from "lowlight";
|
||||||
|
import { memo } from "react";
|
||||||
|
import { colors } from "./colors";
|
||||||
|
import { Text } from "./Text";
|
||||||
|
|
||||||
|
const lowlight = createLowlight(common);
|
||||||
|
const BASH_LANGUAGE = "bash";
|
||||||
|
const FIRST_LINE_PREFIX = "$ ";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
command: string;
|
||||||
|
showPrompt?: boolean;
|
||||||
|
prefix?: string;
|
||||||
|
suffix?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ShellSyntaxPalette = typeof colors.shellSyntax;
|
||||||
|
|
||||||
|
/** Styled text span with a resolved color. */
|
||||||
|
type StyledSpan = { text: string; color: string };
|
||||||
|
|
||||||
|
function colorForClassName(
|
||||||
|
className: string,
|
||||||
|
palette: ShellSyntaxPalette,
|
||||||
|
): string {
|
||||||
|
if (className === "hljs-comment") return palette.comment;
|
||||||
|
if (className === "hljs-keyword") return palette.keyword;
|
||||||
|
if (className === "hljs-string" || className === "hljs-regexp") {
|
||||||
|
return palette.string;
|
||||||
|
}
|
||||||
|
if (className === "hljs-number") return palette.number;
|
||||||
|
if (className === "hljs-literal") return palette.literal;
|
||||||
|
if (
|
||||||
|
className === "hljs-built_in" ||
|
||||||
|
className === "hljs-builtin-name" ||
|
||||||
|
className === "hljs-type"
|
||||||
|
) {
|
||||||
|
return palette.builtIn;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
className === "hljs-variable" ||
|
||||||
|
className === "hljs-template-variable" ||
|
||||||
|
className === "hljs-params"
|
||||||
|
) {
|
||||||
|
return palette.variable;
|
||||||
|
}
|
||||||
|
if (className === "hljs-title" || className === "hljs-function") {
|
||||||
|
return palette.title;
|
||||||
|
}
|
||||||
|
if (className === "hljs-attr" || className === "hljs-attribute") {
|
||||||
|
return palette.attr;
|
||||||
|
}
|
||||||
|
if (className === "hljs-meta") return palette.meta;
|
||||||
|
if (
|
||||||
|
className === "hljs-operator" ||
|
||||||
|
className === "hljs-punctuation" ||
|
||||||
|
className === "hljs-symbol"
|
||||||
|
) {
|
||||||
|
return palette.operator;
|
||||||
|
}
|
||||||
|
if (className === "hljs-subst") return palette.substitution;
|
||||||
|
return palette.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walk the HAST tree depth-first, collecting flat StyledSpan entries.
|
||||||
|
* Newlines within text nodes are preserved so callers can split into lines.
|
||||||
|
*/
|
||||||
|
function collectSpans(
|
||||||
|
node: RootContent | ElementContent,
|
||||||
|
palette: ShellSyntaxPalette,
|
||||||
|
spans: StyledSpan[],
|
||||||
|
inheritedColor?: string,
|
||||||
|
): void {
|
||||||
|
if (node.type === "text") {
|
||||||
|
spans.push({ text: node.value, color: inheritedColor ?? palette.text });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.type === "element") {
|
||||||
|
const nodeClasses =
|
||||||
|
(node.properties?.className as string[] | undefined) ?? [];
|
||||||
|
const highlightClass = [...nodeClasses]
|
||||||
|
.reverse()
|
||||||
|
.find((name) => name.startsWith("hljs-"));
|
||||||
|
const nodeColor = highlightClass
|
||||||
|
? colorForClassName(highlightClass, palette)
|
||||||
|
: inheritedColor;
|
||||||
|
|
||||||
|
for (const child of node.children) {
|
||||||
|
collectSpans(child, palette, spans, nodeColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlight the full command at once (preserves heredoc/multi-line parser
|
||||||
|
* state), then split the flat span list at newline boundaries into per-line
|
||||||
|
* arrays.
|
||||||
|
*/
|
||||||
|
function highlightCommand(
|
||||||
|
command: string,
|
||||||
|
palette: ShellSyntaxPalette,
|
||||||
|
): StyledSpan[][] {
|
||||||
|
let spans: StyledSpan[];
|
||||||
|
try {
|
||||||
|
const root = lowlight.highlight(BASH_LANGUAGE, command);
|
||||||
|
spans = [];
|
||||||
|
for (const child of root.children) {
|
||||||
|
collectSpans(child, palette, spans);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fallback: plain text, split by newlines.
|
||||||
|
return command
|
||||||
|
.split("\n")
|
||||||
|
.map((line) => [{ text: line, color: palette.text }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split spans at newline characters into separate lines.
|
||||||
|
const lines: StyledSpan[][] = [[]];
|
||||||
|
for (const span of spans) {
|
||||||
|
const parts = span.text.split("\n");
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
lines.push([]);
|
||||||
|
}
|
||||||
|
const part = parts[i];
|
||||||
|
if (part && part.length > 0) {
|
||||||
|
const currentLine = lines[lines.length - 1];
|
||||||
|
currentLine?.push({ text: part, color: span.color });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SyntaxHighlightedCommand = memo(
|
||||||
|
({ command, showPrompt = true, prefix, suffix }: Props) => {
|
||||||
|
const palette = colors.shellSyntax;
|
||||||
|
const lines = highlightCommand(command, palette);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box flexDirection="column">
|
||||||
|
{lines.map((spans, lineIdx) => {
|
||||||
|
const lineKey = spans.map((s) => s.text).join("");
|
||||||
|
return (
|
||||||
|
<Box key={`${lineIdx}:${lineKey}`}>
|
||||||
|
{showPrompt ? (
|
||||||
|
<Text color={palette.prompt}>
|
||||||
|
{lineIdx === 0 ? FIRST_LINE_PREFIX : " "}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
<Text color={palette.text}>
|
||||||
|
{lineIdx === 0 && prefix ? prefix : null}
|
||||||
|
{spans.map((span) => (
|
||||||
|
<Text key={`${span.color}:${span.text}`} color={span.color}>
|
||||||
|
{span.text}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
{lineIdx === lines.length - 1 && suffix ? suffix : null}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
SyntaxHighlightedCommand.displayName = "SyntaxHighlightedCommand";
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// existsSync, readFileSync removed - no longer needed since plan content
|
// existsSync, readFileSync removed - no longer needed since plan content
|
||||||
// is shown via StaticPlanApproval during approval, not in tool result
|
// is shown via StaticPlanApproval during approval, not in tool result
|
||||||
import { Box } from "ink";
|
import { Box } from "ink";
|
||||||
import { memo } from "react";
|
import { Fragment, memo, type ReactNode } from "react";
|
||||||
import { INTERRUPTED_BY_USER } from "../../constants";
|
import { INTERRUPTED_BY_USER } from "../../constants";
|
||||||
import { clipToolReturn } from "../../tools/manager.js";
|
import { clipToolReturn } from "../../tools/manager.js";
|
||||||
import type { AdvancedDiffSuccess } from "../helpers/diff";
|
import type { AdvancedDiffSuccess } from "../helpers/diff";
|
||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
isPlanTool,
|
isPlanTool,
|
||||||
isSearchTool,
|
isSearchTool,
|
||||||
isShellOutputTool,
|
isShellOutputTool,
|
||||||
|
isShellTool,
|
||||||
isTaskTool,
|
isTaskTool,
|
||||||
isTodoTool,
|
isTodoTool,
|
||||||
} from "../helpers/toolNameMapping.js";
|
} from "../helpers/toolNameMapping.js";
|
||||||
@@ -34,6 +35,55 @@ function isQuestionTool(name: string): boolean {
|
|||||||
return name === "AskUserQuestion";
|
return name === "AskUserQuestion";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Colorize tool args string with file paths, numbers, and labels.
|
||||||
|
* Regex-based tokenizer that applies shell syntax palette colors.
|
||||||
|
*/
|
||||||
|
function colorizeArgs(argsStr: string): ReactNode {
|
||||||
|
if (!argsStr) return null;
|
||||||
|
|
||||||
|
const palette = colors.shellSyntax;
|
||||||
|
const parts: ReactNode[] = [];
|
||||||
|
let lastIndex = 0;
|
||||||
|
let key = 0;
|
||||||
|
|
||||||
|
// Group 1: paths containing / (e.g. src/cli/foo.tsx, **/*.ts)
|
||||||
|
// Group 2: filenames with extension (e.g. foo.tsx, package.json)
|
||||||
|
// Group 3: labels before : (e.g. offset, limit)
|
||||||
|
// Group 4: standalone numbers (e.g. 50, 10)
|
||||||
|
const re =
|
||||||
|
/([\w.*?\-@~/]+\/[\w.*?\-@~/]*)|((?<=[(\s,])[\w.-]+\.\w{1,5}(?=[)\s,]|$))|(\w+)(?=\s*:)|(\b\d+\b)/g;
|
||||||
|
|
||||||
|
for (let m = re.exec(argsStr); m !== null; m = re.exec(argsStr)) {
|
||||||
|
if (m.index > lastIndex) {
|
||||||
|
parts.push(
|
||||||
|
<Fragment key={key++}>{argsStr.slice(lastIndex, m.index)}</Fragment>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const color = m[1]
|
||||||
|
? palette.string // path with /
|
||||||
|
: m[2]
|
||||||
|
? palette.string // filename.ext
|
||||||
|
: m[3]
|
||||||
|
? palette.comment // label (dimmed)
|
||||||
|
: palette.number; // number
|
||||||
|
|
||||||
|
parts.push(
|
||||||
|
<Text key={key++} color={color}>
|
||||||
|
{m[0]}
|
||||||
|
</Text>,
|
||||||
|
);
|
||||||
|
lastIndex = m.index + m[0].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastIndex < argsStr.length) {
|
||||||
|
parts.push(<Fragment key={key++}>{argsStr.slice(lastIndex)}</Fragment>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{parts}</>;
|
||||||
|
}
|
||||||
|
|
||||||
import type { StreamingState } from "../helpers/accumulator";
|
import type { StreamingState } from "../helpers/accumulator";
|
||||||
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
import { useTerminalWidth } from "../hooks/useTerminalWidth";
|
||||||
import { AdvancedDiffRenderer } from "./AdvancedDiffRenderer";
|
import { AdvancedDiffRenderer } from "./AdvancedDiffRenderer";
|
||||||
@@ -49,6 +99,7 @@ import { MarkdownDisplay } from "./MarkdownDisplay.js";
|
|||||||
import { MemoryDiffRenderer } from "./MemoryDiffRenderer.js";
|
import { MemoryDiffRenderer } from "./MemoryDiffRenderer.js";
|
||||||
import { PlanRenderer } from "./PlanRenderer.js";
|
import { PlanRenderer } from "./PlanRenderer.js";
|
||||||
import { StreamingOutputDisplay } from "./StreamingOutputDisplay";
|
import { StreamingOutputDisplay } from "./StreamingOutputDisplay";
|
||||||
|
import { SyntaxHighlightedCommand } from "./SyntaxHighlightedCommand";
|
||||||
import { TodoRenderer } from "./TodoRenderer.js";
|
import { TodoRenderer } from "./TodoRenderer.js";
|
||||||
|
|
||||||
type ToolCallLine = {
|
type ToolCallLine = {
|
||||||
@@ -86,7 +137,7 @@ export const ToolCallMessage = memo(
|
|||||||
isStreaming?: boolean;
|
isStreaming?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const columns = useTerminalWidth();
|
const columns = useTerminalWidth();
|
||||||
|
try {
|
||||||
// Parse and format the tool call
|
// Parse and format the tool call
|
||||||
const rawName = line.name ?? "?";
|
const rawName = line.name ?? "?";
|
||||||
const argsText =
|
const argsText =
|
||||||
@@ -153,7 +204,7 @@ export const ToolCallMessage = memo(
|
|||||||
// - Question tool: hide args (shown in result instead)
|
// - Question tool: hide args (shown in result instead)
|
||||||
// - Still streaming + phase "ready": args may be incomplete, show ellipsis
|
// - Still streaming + phase "ready": args may be incomplete, show ellipsis
|
||||||
// - Phase "running"/"finished" or stream done: args complete, show formatted
|
// - Phase "running"/"finished" or stream done: args complete, show formatted
|
||||||
let args = "";
|
let args: ReactNode = null;
|
||||||
if (!isQuestionTool(rawName)) {
|
if (!isQuestionTool(rawName)) {
|
||||||
const parseArgs = (): {
|
const parseArgs = (): {
|
||||||
formatted: ReturnType<typeof formatArgsDisplay> | null;
|
formatted: ReturnType<typeof formatArgsDisplay> | null;
|
||||||
@@ -181,7 +232,8 @@ export const ToolCallMessage = memo(
|
|||||||
if (!argsComplete) {
|
if (!argsComplete) {
|
||||||
args = "(…)";
|
args = "(…)";
|
||||||
} else {
|
} else {
|
||||||
const formattedArgs = formatted ?? formatArgsDisplay(argsText, rawName);
|
const formattedArgs =
|
||||||
|
formatted ?? formatArgsDisplay(argsText, rawName);
|
||||||
// Normalize newlines to spaces to prevent forced line breaks
|
// Normalize newlines to spaces to prevent forced line breaks
|
||||||
const normalizedDisplay = formattedArgs.display.replace(/\n/g, " ");
|
const normalizedDisplay = formattedArgs.display.replace(/\n/g, " ");
|
||||||
// For max 2 lines: boxWidth * 2, minus parens (2) and margin (2)
|
// For max 2 lines: boxWidth * 2, minus parens (2) and margin (2)
|
||||||
@@ -194,13 +246,27 @@ export const ToolCallMessage = memo(
|
|||||||
: normalizedDisplay;
|
: normalizedDisplay;
|
||||||
if (rawName.toLowerCase() === "taskoutput") {
|
if (rawName.toLowerCase() === "taskoutput") {
|
||||||
const separator = truncatedDisplay.startsWith("(") ? "" : " ";
|
const separator = truncatedDisplay.startsWith("(") ? "" : " ";
|
||||||
args = separator + truncatedDisplay;
|
args = colorizeArgs(separator + truncatedDisplay);
|
||||||
} else {
|
} else {
|
||||||
args = `(${truncatedDisplay})`;
|
args = colorizeArgs(`(${truncatedDisplay})`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shellCommand: string | null = null;
|
||||||
|
if (isShellTool(rawName) && argsText.trim()) {
|
||||||
|
try {
|
||||||
|
const parsedArgs = JSON.parse(argsText);
|
||||||
|
if (typeof parsedArgs.command === "string") {
|
||||||
|
shellCommand = parsedArgs.command;
|
||||||
|
} else if (Array.isArray(parsedArgs.command)) {
|
||||||
|
shellCommand = parsedArgs.command.join(" ");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep shellCommand null and fall back to plain args rendering.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If name exceeds available width, fall back to simple wrapped rendering
|
// If name exceeds available width, fall back to simple wrapped rendering
|
||||||
const fallback = displayName.length >= rightWidth;
|
const fallback = displayName.length >= rightWidth;
|
||||||
|
|
||||||
@@ -271,7 +337,9 @@ export const ToolCallMessage = memo(
|
|||||||
<Text>{prefix}</Text>
|
<Text>{prefix}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box flexGrow={1} width={contentWidth}>
|
<Box flexGrow={1} width={contentWidth}>
|
||||||
<Text color={colors.status.interrupt}>{INTERRUPTED_BY_USER}</Text>
|
<Text color={colors.status.interrupt}>
|
||||||
|
{INTERRUPTED_BY_USER}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
@@ -299,7 +367,8 @@ export const ToolCallMessage = memo(
|
|||||||
if (parsedArgs.todos && Array.isArray(parsedArgs.todos)) {
|
if (parsedArgs.todos && Array.isArray(parsedArgs.todos)) {
|
||||||
// Convert todos to safe format for TodoRenderer
|
// Convert todos to safe format for TodoRenderer
|
||||||
// Note: Anthropic/Codex use "content", Gemini uses "description"
|
// Note: Anthropic/Codex use "content", Gemini uses "description"
|
||||||
const safeTodos = parsedArgs.todos.map((t: unknown, i: number) => {
|
const safeTodos = parsedArgs.todos.map(
|
||||||
|
(t: unknown, i: number) => {
|
||||||
const rec = isRecord(t) ? t : {};
|
const rec = isRecord(t) ? t : {};
|
||||||
const status: "pending" | "in_progress" | "completed" =
|
const status: "pending" | "in_progress" | "completed" =
|
||||||
rec.status === "completed"
|
rec.status === "completed"
|
||||||
@@ -324,7 +393,8 @@ export const ToolCallMessage = memo(
|
|||||||
? "low"
|
? "low"
|
||||||
: undefined;
|
: undefined;
|
||||||
return { content, status, id, priority };
|
return { content, status, id, priority };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Return TodoRenderer directly - it has its own prefix
|
// Return TodoRenderer directly - it has its own prefix
|
||||||
return <TodoRenderer todos={safeTodos} />;
|
return <TodoRenderer todos={safeTodos} />;
|
||||||
@@ -353,7 +423,9 @@ export const ToolCallMessage = memo(
|
|||||||
? "in_progress"
|
? "in_progress"
|
||||||
: "pending";
|
: "pending";
|
||||||
const step =
|
const step =
|
||||||
typeof rec.step === "string" ? rec.step : JSON.stringify(item);
|
typeof rec.step === "string"
|
||||||
|
? rec.step
|
||||||
|
: JSON.stringify(item);
|
||||||
return { step, status };
|
return { step, status };
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -439,7 +511,11 @@ export const ToolCallMessage = memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a file edit tool - show diff instead of success message
|
// Check if this is a file edit tool - show diff instead of success message
|
||||||
if (isFileEditTool(rawName) && line.resultOk !== false && line.argsText) {
|
if (
|
||||||
|
isFileEditTool(rawName) &&
|
||||||
|
line.resultOk !== false &&
|
||||||
|
line.argsText
|
||||||
|
) {
|
||||||
const diff = line.toolCallId
|
const diff = line.toolCallId
|
||||||
? precomputedDiffs?.get(line.toolCallId)
|
? precomputedDiffs?.get(line.toolCallId)
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -678,7 +754,11 @@ export const ToolCallMessage = memo(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a search/grep tool - show line/file count summary
|
// Check if this is a search/grep tool - show line/file count summary
|
||||||
if (isSearchTool(rawName) && line.resultOk !== false && line.resultText) {
|
if (
|
||||||
|
isSearchTool(rawName) &&
|
||||||
|
line.resultOk !== false &&
|
||||||
|
line.resultText
|
||||||
|
) {
|
||||||
const text = line.resultText;
|
const text = line.resultText;
|
||||||
// Match "Found N file(s)" at start of output (files_with_matches mode)
|
// Match "Found N file(s)" at start of output (files_with_matches mode)
|
||||||
const filesMatch = text.match(/^Found (\d+) files?/);
|
const filesMatch = text.match(/^Found (\d+) files?/);
|
||||||
@@ -707,7 +787,8 @@ export const ToolCallMessage = memo(
|
|||||||
</Box>
|
</Box>
|
||||||
<Box flexGrow={1} width={contentWidth}>
|
<Box flexGrow={1} width={contentWidth}>
|
||||||
<Text>
|
<Text>
|
||||||
Found <Text bold>0</Text> {noFilesMatch ? "files" : "matches"}
|
Found <Text bold>0</Text>{" "}
|
||||||
|
{noFilesMatch ? "files" : "matches"}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -841,7 +922,19 @@ export const ToolCallMessage = memo(
|
|||||||
>
|
>
|
||||||
{displayName}
|
{displayName}
|
||||||
</Text>
|
</Text>
|
||||||
{args ? (
|
{shellCommand ? (
|
||||||
|
<Box
|
||||||
|
flexGrow={1}
|
||||||
|
width={Math.max(0, rightWidth - displayName.length)}
|
||||||
|
>
|
||||||
|
<SyntaxHighlightedCommand
|
||||||
|
command={shellCommand}
|
||||||
|
showPrompt={false}
|
||||||
|
prefix="("
|
||||||
|
suffix=")"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
) : args ? (
|
||||||
<Box
|
<Box
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
width={Math.max(0, rightWidth - displayName.length)}
|
width={Math.max(0, rightWidth - displayName.length)}
|
||||||
@@ -886,6 +979,17 @@ export const ToolCallMessage = memo(
|
|||||||
})()}
|
})()}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
`[ToolCallMessage render error] tool=${line.name} id=${line.id}`,
|
||||||
|
err,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Text color="red">
|
||||||
|
⚠ render error: {line.name ?? "?"} ({String(err)})
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,42 @@ const _colors = {
|
|||||||
dot: brandColors.statusError, // Red dot in output
|
dot: brandColors.statusError, // Red dot in output
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Shell command syntax highlighting
|
||||||
|
shellSyntaxDark: {
|
||||||
|
prompt: "#cba6f7",
|
||||||
|
text: "#cdd6f4",
|
||||||
|
comment: "#6c7086",
|
||||||
|
keyword: "#89b4fa",
|
||||||
|
string: "#a6e3a1",
|
||||||
|
number: "#fab387",
|
||||||
|
literal: "#fab387",
|
||||||
|
builtIn: "#f9e2af",
|
||||||
|
variable: "#f5c2e7",
|
||||||
|
title: "#94e2d5",
|
||||||
|
attr: "#74c7ec",
|
||||||
|
operator: "#bac2de",
|
||||||
|
punctuation: "#bac2de",
|
||||||
|
meta: "#f38ba8",
|
||||||
|
substitution: "#f5c2e7",
|
||||||
|
},
|
||||||
|
shellSyntaxLight: {
|
||||||
|
prompt: "#8839ef",
|
||||||
|
text: "#4c4f69",
|
||||||
|
comment: "#9ca0b0",
|
||||||
|
keyword: "#1e66f5",
|
||||||
|
string: "#40a02b",
|
||||||
|
number: "#fe640b",
|
||||||
|
literal: "#fe640b",
|
||||||
|
builtIn: "#df8e1d",
|
||||||
|
variable: "#ea76cb",
|
||||||
|
title: "#179299",
|
||||||
|
attr: "#209fb5",
|
||||||
|
operator: "#5c5f77",
|
||||||
|
punctuation: "#5c5f77",
|
||||||
|
meta: "#d20f39",
|
||||||
|
substitution: "#e64553",
|
||||||
|
},
|
||||||
|
|
||||||
// Todo list
|
// Todo list
|
||||||
todo: {
|
todo: {
|
||||||
completed: brandColors.primaryAccent, // Same blue as in-progress, with strikethrough
|
completed: brandColors.primaryAccent, // Same blue as in-progress, with strikethrough
|
||||||
@@ -220,6 +256,12 @@ const _colors = {
|
|||||||
export const colors = {
|
export const colors = {
|
||||||
..._colors,
|
..._colors,
|
||||||
|
|
||||||
|
get shellSyntax() {
|
||||||
|
return getTerminalTheme() === "light"
|
||||||
|
? _colors.shellSyntaxLight
|
||||||
|
: _colors.shellSyntaxDark;
|
||||||
|
},
|
||||||
|
|
||||||
// User messages (past prompts) - theme-aware background
|
// User messages (past prompts) - theme-aware background
|
||||||
// Uses getter to read theme at render time (after async init)
|
// Uses getter to read theme at render time (after async init)
|
||||||
get userMessage() {
|
get userMessage() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Box } from "ink";
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { useTerminalWidth } from "../../hooks/useTerminalWidth";
|
import { useTerminalWidth } from "../../hooks/useTerminalWidth";
|
||||||
import { colors } from "../colors";
|
import { colors } from "../colors";
|
||||||
|
import { SyntaxHighlightedCommand } from "../SyntaxHighlightedCommand";
|
||||||
import { Text } from "../Text";
|
import { Text } from "../Text";
|
||||||
|
|
||||||
const SOLID_LINE = "─";
|
const SOLID_LINE = "─";
|
||||||
@@ -36,7 +37,7 @@ export const BashPreview = memo(({ command, description }: Props) => {
|
|||||||
|
|
||||||
{/* Command preview */}
|
{/* Command preview */}
|
||||||
<Box paddingLeft={2} flexDirection="column">
|
<Box paddingLeft={2} flexDirection="column">
|
||||||
<Text>{command}</Text>
|
<SyntaxHighlightedCommand command={command} />
|
||||||
{description && <Text dimColor>{description}</Text>}
|
{description && <Text dimColor>{description}</Text>}
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
|
|||||||
8
vendor/ink-text-input/build/index.js
vendored
8
vendor/ink-text-input/build/index.js
vendored
@@ -70,21 +70,21 @@ function TextInput({ value: originalValue, placeholder = '', focus = true, mask,
|
|||||||
let renderedValue = value;
|
let renderedValue = value;
|
||||||
let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
|
let renderedPlaceholder = placeholder ? chalk.grey(placeholder) : undefined;
|
||||||
if (showCursor && focus) {
|
if (showCursor && focus) {
|
||||||
renderedPlaceholder = placeholder.length > 0 ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1)) : chalk.inverse(' ');
|
renderedPlaceholder = placeholder.length > 0 ? chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1)) : chalk.inverse('\u00A0');
|
||||||
renderedValue = value.length > 0 ? '' : chalk.inverse(' ');
|
renderedValue = value.length > 0 ? '' : chalk.inverse('\u00A0');
|
||||||
let i = 0;
|
let i = 0;
|
||||||
for (const char of value) {
|
for (const char of value) {
|
||||||
const isCursorPosition = i >= cursorOffset - cursorActualWidth && i <= cursorOffset;
|
const isCursorPosition = i >= cursorOffset - cursorActualWidth && i <= cursorOffset;
|
||||||
if (isCursorPosition && char === '\n') {
|
if (isCursorPosition && char === '\n') {
|
||||||
// Newline at cursor: show inverted space (visible cursor) then the newline
|
// Newline at cursor: show inverted space (visible cursor) then the newline
|
||||||
renderedValue += chalk.inverse(' ') + char;
|
renderedValue += chalk.inverse('\u00A0') + char;
|
||||||
} else {
|
} else {
|
||||||
renderedValue += isCursorPosition ? chalk.inverse(char) : char;
|
renderedValue += isCursorPosition ? chalk.inverse(char) : char;
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
if (value.length > 0 && cursorOffset === value.length) {
|
if (value.length > 0 && cursorOffset === value.length) {
|
||||||
renderedValue += chalk.inverse(' ');
|
renderedValue += chalk.inverse('\u00A0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
useInput((input, key) => {
|
useInput((input, key) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user