diff --git a/src/commands/artifacts/ArtifactsMenu.tsx b/src/commands/artifacts/ArtifactsMenu.tsx
new file mode 100644
index 000000000..60a5bd131
--- /dev/null
+++ b/src/commands/artifacts/ArtifactsMenu.tsx
@@ -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 (
+
+
+ Artifacts ({artifacts.length})
+
+
+ {artifacts.length === 0 ? (
+ No artifacts uploaded this session. Run /use-artifacts to learn how.
+ ) : (
+
+ {artifacts.map((a, idx) => (
+
+ ))}
+
+ {'↑/↓ select · Enter open · c copy URL · Esc exit'}
+
+
+ )}
+
+ );
+}
+
+function ArtifactRow({ artifact, isSelected }: { artifact: ArtifactInfo; isSelected: boolean }): React.ReactElement {
+ const marker = isSelected ? '›' : ' ';
+ return (
+
+
+ {marker}
+
+ {artifact.basename}
+
+ {artifact.hash ? ({artifact.hash}) : null}
+
+ {artifact.url ? (
+
+ {artifact.url}
+
+ ) : (
+
+ {artifact.rawContent}
+
+ )}
+ {artifact.expiresAt ? (
+
+ expires: {artifact.expiresAt}
+
+ ) : null}
+
+ );
+}
+
+// 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
+ }
+}