feat(artifact): add ArtifactsMenu Ink component

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-20 16:43:27 +08:00
parent ac21f40453
commit 388840a4b4

View File

@@ -0,0 +1,104 @@
import * as React from 'react';
import { Box, Text, useInput } from '@anthropic/ink';
import type { ArtifactInfo } from './scanner.js';
import { openBrowser } from 'src/utils/browser.js';
type Props = {
artifacts: ArtifactInfo[];
onExit: () => void;
};
export function ArtifactsMenu({ artifacts, onExit }: Props): React.ReactElement {
const [selected, setSelected] = React.useState(0);
useInput((input, key) => {
if (input === 'q' || key.escape) {
onExit();
return;
}
if (artifacts.length === 0) return;
if (key.upArrow) {
setSelected(s => (s - 1 + artifacts.length) % artifacts.length);
return;
}
if (key.downArrow) {
setSelected(s => (s + 1) % artifacts.length);
return;
}
if (key.return) {
const target = artifacts[selected];
if (target.url) {
void openBrowser(target.url);
}
return;
}
if (input === 'c') {
const target = artifacts[selected];
if (target.url) {
copyToClipboard(target.url);
}
}
});
return (
<Box flexDirection="column" paddingX={1} paddingY={0}>
<Box marginBottom={1}>
<Text bold>Artifacts ({artifacts.length})</Text>
</Box>
{artifacts.length === 0 ? (
<Text color="subtle">No artifacts uploaded this session. Run /use-artifacts to learn how.</Text>
) : (
<Box flexDirection="column">
{artifacts.map((a, idx) => (
<ArtifactRow key={a.toolUseId} artifact={a} isSelected={idx === selected} />
))}
<Box marginTop={1}>
<Text color="subtle">{'↑/↓ select · Enter open · c copy URL · Esc exit'}</Text>
</Box>
</Box>
)}
</Box>
);
}
function ArtifactRow({ artifact, isSelected }: { artifact: ArtifactInfo; isSelected: boolean }): React.ReactElement {
const marker = isSelected ? '' : ' ';
return (
<Box flexDirection="column">
<Box>
<Text color={isSelected ? 'suggestion' : undefined}>{marker} </Text>
<Text bold={isSelected} color={artifact.isError ? 'error' : undefined}>
{artifact.basename}
</Text>
{artifact.hash ? <Text color="subtle"> ({artifact.hash})</Text> : null}
</Box>
{artifact.url ? (
<Box marginLeft={2}>
<Text color="background">{artifact.url}</Text>
</Box>
) : (
<Box marginLeft={2}>
<Text color="error">{artifact.rawContent}</Text>
</Box>
)}
{artifact.expiresAt ? (
<Box marginLeft={2}>
<Text color="subtle">expires: {artifact.expiresAt}</Text>
</Box>
) : null}
</Box>
);
}
// macOS-only clipboard via pbcopy. The CLI is primarily macOS-targeted; on
// other platforms this is a no-op (URL is still rendered above for the user
// to select and copy manually).
function copyToClipboard(text: string): void {
try {
const { spawnSync } = require('node:child_process') as typeof import('node:child_process');
spawnSync('pbcopy', [], { input: text });
} catch {
// best-effort
}
}