Files
codex/codex-cli/src/components/chat/terminal-chat-response-item.tsx
Luci a9ecb2efce chore: upgrade prettier to v3 (#644)
## Description

This PR addresses the following improvements:

**Unify Prettier Version**: Currently, the Prettier version used in
`/package.json` and `/codex-cli/package.json` are different. In this PR,
we're updating both to use Prettier v3.

- Prettier v3 introduces improved support for JavaScript and TypeScript.
(e.g. the formatting scenario shown in the image below. This is more
aligned with the TypeScript indentation standard).

<img width="1126" alt="image"
src="https://github.com/user-attachments/assets/6e237eb8-4553-4574-b336-ed9561c55370"
/>

**Add Prettier Auto-Formatting in lint-staged**: We've added a step to
automatically run prettier --write on JavaScript and TypeScript files as
part of the lint-staged process, before the ESLint checks.

- This will help ensure that all committed code is properly formatted
according to the project's Prettier configuration.
2025-04-25 07:21:50 -07:00

265 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { OverlayModeType } from "./terminal-chat";
import type { TerminalRendererOptions } from "marked-terminal";
import type {
ResponseFunctionToolCallItem,
ResponseFunctionToolCallOutputItem,
ResponseInputMessageItem,
ResponseItem,
ResponseOutputMessage,
ResponseReasoningItem,
} from "openai/resources/responses/responses";
import { useTerminalSize } from "../../hooks/use-terminal-size";
import { parseToolCall, parseToolCallOutput } from "../../utils/parsers";
import chalk, { type ForegroundColorName } from "chalk";
import { Box, Text } from "ink";
import { parse, setOptions } from "marked";
import TerminalRenderer from "marked-terminal";
import React, { useEffect, useMemo } from "react";
export default function TerminalChatResponseItem({
item,
fullStdout = false,
setOverlayMode,
}: {
item: ResponseItem;
fullStdout?: boolean;
setOverlayMode?: React.Dispatch<React.SetStateAction<OverlayModeType>>;
}): React.ReactElement {
switch (item.type) {
case "message":
return (
<TerminalChatResponseMessage
setOverlayMode={setOverlayMode}
message={item}
/>
);
case "function_call":
return <TerminalChatResponseToolCall message={item} />;
case "function_call_output":
return (
<TerminalChatResponseToolCallOutput
message={item}
fullStdout={fullStdout}
/>
);
default:
break;
}
// @ts-expect-error `reasoning` is not in the responses API yet
if (item.type === "reasoning") {
return <TerminalChatResponseReasoning message={item} />;
}
return <TerminalChatResponseGenericMessage message={item} />;
}
// TODO: this should be part of `ResponseReasoningItem`. Also it doesn't work.
// ---------------------------------------------------------------------------
// Utility helpers
// ---------------------------------------------------------------------------
/**
* Guess how long the assistant spent "thinking" based on the combined length
* of the reasoning summary. The calculation itself is fast, but wrapping it in
* `useMemo` in the consuming component ensures it only runs when the
* `summary` array actually changes.
*/
// TODO: use actual thinking time
//
// function guessThinkingTime(summary: Array<ResponseReasoningItem.Summary>) {
// const totalTextLength = summary
// .map((t) => t.text.length)
// .reduce((a, b) => a + b, summary.length - 1);
// return Math.max(1, Math.ceil(totalTextLength / 300));
// }
export function TerminalChatResponseReasoning({
message,
}: {
message: ResponseReasoningItem & { duration_ms?: number };
}): React.ReactElement | null {
// Only render when there is a reasoning summary
if (!message.summary || message.summary.length === 0) {
return null;
}
return (
<Box gap={1} flexDirection="column">
{message.summary.map((summary, key) => {
const s = summary as { headline?: string; text: string };
return (
<Box key={key} flexDirection="column">
{s.headline && <Text bold>{s.headline}</Text>}
<Markdown>{s.text}</Markdown>
</Box>
);
})}
</Box>
);
}
const colorsByRole: Record<string, ForegroundColorName> = {
assistant: "magentaBright",
user: "blueBright",
};
function TerminalChatResponseMessage({
message,
setOverlayMode,
}: {
message: ResponseInputMessageItem | ResponseOutputMessage;
setOverlayMode?: React.Dispatch<React.SetStateAction<OverlayModeType>>;
}) {
// auto switch to model mode if the system message contains "has been deprecated"
useEffect(() => {
if (message.role === "system") {
const systemMessage = message.content.find(
(c) => c.type === "input_text",
)?.text;
if (systemMessage?.includes("model_not_found")) {
setOverlayMode?.("model");
}
}
}, [message, setOverlayMode]);
return (
<Box flexDirection="column">
<Text bold color={colorsByRole[message.role] || "gray"}>
{message.role === "assistant" ? "codex" : message.role}
</Text>
<Markdown>
{message.content
.map(
(c) =>
c.type === "output_text"
? c.text
: c.type === "refusal"
? c.refusal
: c.type === "input_text"
? c.text
: c.type === "input_image"
? "<Image>"
: c.type === "input_file"
? c.filename
: "", // unknown content type
)
.join(" ")}
</Markdown>
</Box>
);
}
function TerminalChatResponseToolCall({
message,
}: {
message: ResponseFunctionToolCallItem;
}) {
const details = parseToolCall(message);
return (
<Box flexDirection="column" gap={1}>
<Text color="magentaBright" bold>
command
</Text>
<Text>
<Text dimColor>$</Text> {details?.cmdReadableText}
</Text>
</Box>
);
}
function TerminalChatResponseToolCallOutput({
message,
fullStdout,
}: {
message: ResponseFunctionToolCallOutputItem;
fullStdout: boolean;
}) {
const { output, metadata } = parseToolCallOutput(message.output);
const { exit_code, duration_seconds } = metadata;
const metadataInfo = useMemo(
() =>
[
typeof exit_code !== "undefined" ? `code: ${exit_code}` : "",
typeof duration_seconds !== "undefined"
? `duration: ${duration_seconds}s`
: "",
]
.filter(Boolean)
.join(", "),
[exit_code, duration_seconds],
);
let displayedContent = output;
if (message.type === "function_call_output" && !fullStdout) {
const lines = displayedContent.split("\n");
if (lines.length > 4) {
const head = lines.slice(0, 4);
const remaining = lines.length - 4;
displayedContent = [...head, `... (${remaining} more lines)`].join("\n");
}
}
// -------------------------------------------------------------------------
// Colorize diff output: lines starting with '-' in red, '+' in green.
// This makes patches and other difflike stdout easier to read.
// We exclude the typical diff file headers ('---', '+++') so they retain
// the default color. This is a besteffort heuristic and should be safe for
// nondiff output only the very first character of a line is inspected.
// -------------------------------------------------------------------------
const colorizedContent = displayedContent
.split("\n")
.map((line) => {
if (line.startsWith("+") && !line.startsWith("++")) {
return chalk.green(line);
}
if (line.startsWith("-") && !line.startsWith("--")) {
return chalk.red(line);
}
return line;
})
.join("\n");
return (
<Box flexDirection="column" gap={1}>
<Text color="magenta" bold>
command.stdout{" "}
<Text dimColor>{metadataInfo ? `(${metadataInfo})` : ""}</Text>
</Text>
<Text dimColor>{colorizedContent}</Text>
</Box>
);
}
export function TerminalChatResponseGenericMessage({
message,
}: {
message: ResponseItem;
}): React.ReactElement {
return <Text>{JSON.stringify(message, null, 2)}</Text>;
}
export type MarkdownProps = TerminalRendererOptions & {
children: string;
};
export function Markdown({
children,
...options
}: MarkdownProps): React.ReactElement {
const size = useTerminalSize();
const rendered = React.useMemo(() => {
// Configure marked for this specific render
setOptions({
// @ts-expect-error missing parser, space props
renderer: new TerminalRenderer({ ...options, width: size.columns }),
});
const parsed = parse(children, { async: false }).trim();
// Remove the truncation logic
return parsed;
// eslint-disable-next-line react-hooks/exhaustive-deps -- options is an object of primitives
}, [children, size.columns, size.rows]);
return <Text>{rendered}</Text>;
}