Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Check, Copy } from 'lucide-react';
|
|
import { useI18n } from '../application/i18n/I18nProvider';
|
|
import type { ShellHistoryEntry } from '../types';
|
|
import { Button } from './ui/button';
|
|
import { Input } from './ui/input';
|
|
|
|
// History Item Component
|
|
interface HistoryItemProps {
|
|
entry: ShellHistoryEntry;
|
|
onSaveAsSnippet: (entry: ShellHistoryEntry, label: string) => void;
|
|
onCopy: () => void;
|
|
isCopied: boolean;
|
|
}
|
|
|
|
export const HistoryItem: React.FC<HistoryItemProps> = ({ entry, onSaveAsSnippet, onCopy, isCopied }) => {
|
|
const { t } = useI18n();
|
|
const [isEditing, setIsEditing] = useState(false);
|
|
const [label, setLabel] = useState('');
|
|
|
|
const handleSave = () => {
|
|
if (label.trim()) {
|
|
onSaveAsSnippet(entry, label);
|
|
setIsEditing(false);
|
|
setLabel('');
|
|
}
|
|
};
|
|
|
|
const formatTime = (timestamp: number) => {
|
|
const date = new Date(timestamp);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMs / 3600000);
|
|
const diffDays = Math.floor(diffMs / 86400000);
|
|
|
|
if (diffMins < 1) return t('snippets.history.time.justNow');
|
|
if (diffMins < 60) return t('snippets.history.time.minutesAgo', { count: diffMins });
|
|
if (diffHours < 24) return t('snippets.history.time.hoursAgo', { count: diffHours });
|
|
if (diffDays < 7) return t('snippets.history.time.daysAgo', { count: diffDays });
|
|
return date.toLocaleDateString();
|
|
};
|
|
|
|
return (
|
|
<div className="group rounded-lg bg-background/60 border border-border/50 p-3">
|
|
<div className="flex items-start gap-2">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-mono text-sm truncate">{entry.command}</div>
|
|
<div className="flex items-center gap-2 mt-1 text-[11px] text-muted-foreground">
|
|
<span>{entry.hostLabel}</span>
|
|
<span>{t('snippets.history.separator')}</span>
|
|
<span>{formatTime(entry.timestamp)}</span>
|
|
</div>
|
|
</div>
|
|
{!isEditing && (
|
|
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 px-2"
|
|
onClick={onCopy}
|
|
>
|
|
{isCopied ? <Check size={14} /> : <Copy size={14} />}
|
|
</Button>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
className="h-7 px-3"
|
|
onClick={() => setIsEditing(true)}
|
|
>
|
|
{t('common.save')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{isEditing && (
|
|
<div className="mt-3 space-y-2">
|
|
<Input
|
|
placeholder={t('snippets.history.labelPlaceholder')}
|
|
value={label}
|
|
onChange={(e) => setLabel(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
|
autoFocus
|
|
/>
|
|
<div className="flex justify-end gap-2">
|
|
<Button variant="ghost" size="sm" onClick={() => { setIsEditing(false); setLabel(''); }}>
|
|
{t('common.cancel')}
|
|
</Button>
|
|
<Button size="sm" onClick={handleSave} disabled={!label.trim()}>
|
|
{t('snippets.history.saveAsSnippet')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|