import React from 'react';
import { Box, Text, useStdout, type DOMElement } from 'ink';
import ImeTextInput from './ImeTextInput.js';
import type { CliStatus, SlashCommand, SkillDefinition, TimelineItem, Workspace } from './types.js';
import { formatContext, shortPath } from './format.js';
const SHOW_THINKING_DETAILS = false;
export function StatusLine({ status }: { status: CliStatus }) {
const skill = status.activeSkill ? ` skill:${status.activeSkill.label}` : '';
const background = [
status.backgroundAgents > 0 ? `${status.backgroundAgents}后台智能体` : '',
status.backgroundCommands > 0 ? `${status.backgroundCommands}后台指令` : '',
].filter(Boolean).join(' · ');
const left = `${status.model} · ${status.workspace.label} · ${status.permissionMode} · ${status.executionMode}${background ? ` · ${background}` : ''} · 版本控制:${status.versioningEnabled ? '开' : '关'}${skill}`;
const right = formatContext(status.contextUsed, status.contextLimit);
return (
{left}
{right}
);
}
export function Composer(
{
value,
disabled,
marginTop = 1,
cursorYOffset = 0,
refreshTick = 0,
onChange,
onSubmit,
}: {
value: string;
disabled?: boolean;
marginTop?: number;
cursorYOffset?: number;
refreshTick?: number;
onChange: (value: string) => void;
onSubmit: (value: string) => void | Promise;
},
) {
const contentRef = React.useRef(null);
return (
›
);
}
function displayWidth(text: string): number {
let width = 0;
for (const char of Array.from(text)) {
const code = char.codePointAt(0) || 0;
if (code === 0) continue;
if (code < 32 || (code >= 0x7f && code < 0xa0)) continue;
width += isWideCodePoint(code) ? 2 : 1;
}
return width;
}
function isWideCodePoint(code: number): boolean {
return (
code >= 0x1100 && (
code <= 0x115f ||
code === 0x2329 ||
code === 0x232a ||
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
(code >= 0xac00 && code <= 0xd7a3) ||
(code >= 0xf900 && code <= 0xfaff) ||
(code >= 0xfe10 && code <= 0xfe19) ||
(code >= 0xfe30 && code <= 0xfe6f) ||
(code >= 0xff00 && code <= 0xff60) ||
(code >= 0xffe0 && code <= 0xffe6) ||
(code >= 0x1f300 && code <= 0x1f64f) ||
(code >= 0x1f900 && code <= 0x1f9ff) ||
(code >= 0x20000 && code <= 0x3fffd)
)
);
}
export function WelcomePanel({ status }: { status: CliStatus }) {
return (
Agents
Welcome back!
{status.model} · {status.runMode}
{shortPath(status.directory)}
);
}
export function Timeline({ items, pulseTick = 0, width = 78 }: { items: TimelineItem[]; pulseTick?: number; width?: number }) {
return (
{items.map((item) => (
))}
);
}
function TimelineRow({ item, pulseTick }: { item: TimelineItem; pulseTick: number }) {
const separator = useFullWidthSeparator();
if (item.kind === 'user') {
const lines = String(item.body || '').split(/\r?\n/);
return (
{lines.map((line, index) => (
{index === 0 ? › : }
{line}
))}
);
}
if (item.kind === 'assistant') {
return (
{separator}
{wrapIndentedText(String(item.body || ''), 2, separator.length).map((line, index) => (
{line}
))}
{item.status === 'success' ? <> {separator}> : null}
);
}
if (item.kind === 'system') {
return • {item.body};
}
const running = item.status === 'running';
const failed = item.status === 'failed';
const color = running ? (pulseTick % 2 === 0 ? 'gray' : undefined) : failed ? 'red' : item.kind === 'thinking' ? undefined : 'green';
const dot = running ? (pulseTick % 2 === 0 ? '◦' : '•') : '•';
const detailColor = item.kind === 'thinking' || item.kind === 'tool' || item.kind === 'sub_agent' ? 'gray' : undefined;
const detailWidth = separator.length;
const showDetails = item.kind === 'thinking' ? SHOW_THINKING_DETAILS : true;
return (
{dot} {item.title || item.kind}
{showDetails && !running && item.body ? wrapIndentedText(`└ ${item.body}`, 2, detailWidth).map((line, index) => (
{line}
)) : null}
{showDetails && (item.kind === 'thinking' || !running) && (item.lines || []).map((line, index) => (
wrapIndentedText(line, index === 0 && !item.body ? 2 : 4, detailWidth).map((wrapped, wrappedIndex) => (
{index === 0 && !item.body && wrappedIndex === 0 ? wrapped.replace(/^ /, ' └ ') : wrapped}
))
))}
);
}
function useFullWidthSeparator(): string {
const { stdout } = useStdout();
const width = Math.max(2, (stdout.columns || 80) - 2);
return '─'.repeat(width);
}
function wrapIndentedText(text: string, indent: number, width: number): string[] {
const prefix = ' '.repeat(indent);
const contentWidth = Math.max(10, width - indent);
const result: string[] = [];
for (const rawLine of text.split(/\r?\n/)) {
const line = rawLine || '';
if (displayWidth(line) <= contentWidth) {
result.push(`${prefix}${line}`);
continue;
}
let current = '';
let currentWidth = 0;
for (const char of Array.from(line)) {
const charWidth = displayWidth(char);
if (current && currentWidth + charWidth > contentWidth) {
result.push(`${prefix}${current}`);
current = char;
currentWidth = charWidth;
} else {
current += char;
currentWidth += charWidth;
}
}
result.push(`${prefix}${current}`);
}
return result.length ? result : [prefix];
}
export function SlashMenu({ query, commands, selectedIndex }: { query: string; commands: SlashCommand[]; selectedIndex: number }) {
const visibleCount = 5;
const leftIndent = ' ';
const start = Math.max(0, selectedIndex - 2);
const end = Math.min(commands.length, start + visibleCount);
const adjustedStart = Math.max(0, end - visibleCount);
const visibleCommands = commands.slice(adjustedStart, end);
return (
{visibleCommands.map((command, visibleIndex) => {
const index = adjustedStart + visibleIndex;
return (
{leftIndent}{index === selectedIndex ? '› ' : ' '}{command.name.padEnd(13)} {command.description}
);
})}
{!commands.length ? {leftIndent} 无匹配命令 : null}
);
}
export function Picker({ title, items, selectedIndex }: { title: string; items: Array<{ label: string; description?: string; disabled?: boolean }>; selectedIndex: number }) {
const visibleCount = 5;
const start = Math.max(0, selectedIndex - 2);
const end = Math.min(items.length, start + visibleCount);
const adjustedStart = Math.max(0, end - visibleCount);
const visibleItems = items.slice(adjustedStart, end);
return (
{title}
{visibleItems.map((item, visibleIndex) => {
const index = adjustedStart + visibleIndex;
return (
{index === selectedIndex ? '› ' : ' '}{item.label.padEnd(20)} {item.description || ''}
);
})}
{!items.length ? 暂无选项 : null}
);
}
export function WorkspacePrompt({ cwd, selectedIndex }: { cwd: string; selectedIndex: number }) {
const options = ['确认,添加并继续', '拒绝,退出'];
return (
当前目录尚未添加为工作区:
{shortPath(cwd)}
Agent 将在此目录中读取文件、执行命令和修改内容。
是否将此目录添加为工作区?
{options.map((label, index) => (
{index === selectedIndex ? '› ' : ' '}{index + 1}. {label}
))}
);
}
export function StatusPanel({ status }: { status: CliStatus }) {
return (
{'>_ Agents CLI'}
);
}
function StatusField({ label, value }: { label: string; value: string }) {
return {label.padEnd(15)} {value};
}
export function HelpPanel({ commands }: { commands: SlashCommand[] }) {
return (
Commands
{commands.map((command) => (
{command.name.padEnd(13)} {command.description}
))}
);
}
export function SkillHint({ skill }: { skill?: SkillDefinition }) {
if (!skill) return null;
return + 先阅读 {skill.label} skill;
}
export function WorkspaceList({ workspaces, selectedIndex }: { workspaces: Workspace[]; selectedIndex: number }) {
return (
({ label: workspace.label, description: shortPath(workspace.path) }))}
/>
);
}