Skip to content

Commit 388840a

Browse files
claude-code-bestglm-5.2
andcommitted
feat(artifact): add ArtifactsMenu Ink component
Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
1 parent ac21f40 commit 388840a

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import * as React from 'react';
2+
import { Box, Text, useInput } from '@anthropic/ink';
3+
import type { ArtifactInfo } from './scanner.js';
4+
import { openBrowser } from 'src/utils/browser.js';
5+
6+
type Props = {
7+
artifacts: ArtifactInfo[];
8+
onExit: () => void;
9+
};
10+
11+
export function ArtifactsMenu({ artifacts, onExit }: Props): React.ReactElement {
12+
const [selected, setSelected] = React.useState(0);
13+
14+
useInput((input, key) => {
15+
if (input === 'q' || key.escape) {
16+
onExit();
17+
return;
18+
}
19+
if (artifacts.length === 0) return;
20+
if (key.upArrow) {
21+
setSelected(s => (s - 1 + artifacts.length) % artifacts.length);
22+
return;
23+
}
24+
if (key.downArrow) {
25+
setSelected(s => (s + 1) % artifacts.length);
26+
return;
27+
}
28+
if (key.return) {
29+
const target = artifacts[selected];
30+
if (target.url) {
31+
void openBrowser(target.url);
32+
}
33+
return;
34+
}
35+
if (input === 'c') {
36+
const target = artifacts[selected];
37+
if (target.url) {
38+
copyToClipboard(target.url);
39+
}
40+
}
41+
});
42+
43+
return (
44+
<Box flexDirection="column" paddingX={1} paddingY={0}>
45+
<Box marginBottom={1}>
46+
<Text bold>Artifacts ({artifacts.length})</Text>
47+
</Box>
48+
49+
{artifacts.length === 0 ? (
50+
<Text color="subtle">No artifacts uploaded this session. Run /use-artifacts to learn how.</Text>
51+
) : (
52+
<Box flexDirection="column">
53+
{artifacts.map((a, idx) => (
54+
<ArtifactRow key={a.toolUseId} artifact={a} isSelected={idx === selected} />
55+
))}
56+
<Box marginTop={1}>
57+
<Text color="subtle">{'↑/↓ select · Enter open · c copy URL · Esc exit'}</Text>
58+
</Box>
59+
</Box>
60+
)}
61+
</Box>
62+
);
63+
}
64+
65+
function ArtifactRow({ artifact, isSelected }: { artifact: ArtifactInfo; isSelected: boolean }): React.ReactElement {
66+
const marker = isSelected ? '›' : ' ';
67+
return (
68+
<Box flexDirection="column">
69+
<Box>
70+
<Text color={isSelected ? 'suggestion' : undefined}>{marker} </Text>
71+
<Text bold={isSelected} color={artifact.isError ? 'error' : undefined}>
72+
{artifact.basename}
73+
</Text>
74+
{artifact.hash ? <Text color="subtle"> ({artifact.hash})</Text> : null}
75+
</Box>
76+
{artifact.url ? (
77+
<Box marginLeft={2}>
78+
<Text color="background">{artifact.url}</Text>
79+
</Box>
80+
) : (
81+
<Box marginLeft={2}>
82+
<Text color="error">{artifact.rawContent}</Text>
83+
</Box>
84+
)}
85+
{artifact.expiresAt ? (
86+
<Box marginLeft={2}>
87+
<Text color="subtle">expires: {artifact.expiresAt}</Text>
88+
</Box>
89+
) : null}
90+
</Box>
91+
);
92+
}
93+
94+
// macOS-only clipboard via pbcopy. The CLI is primarily macOS-targeted; on
95+
// other platforms this is a no-op (URL is still rendered above for the user
96+
// to select and copy manually).
97+
function copyToClipboard(text: string): void {
98+
try {
99+
const { spawnSync } = require('node:child_process') as typeof import('node:child_process');
100+
spawnSync('pbcopy', [], { input: text });
101+
} catch {
102+
// best-effort
103+
}
104+
}

0 commit comments

Comments
 (0)