From 388840a4b4c3fae7a35a856519d56510bed109af Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sat, 20 Jun 2026 16:43:27 +0800 Subject: [PATCH] feat(artifact): add ArtifactsMenu Ink component Co-Authored-By: glm-5.2 --- src/commands/artifacts/ArtifactsMenu.tsx | 104 +++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/commands/artifacts/ArtifactsMenu.tsx 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 + } +}