feat: session quick wins (#51-#55) (#72)

* feat: add session quick wins (#51-#55)

- Session timer showing elapsed time in header (#51)
- Tab keyboard shortcut to focus notes textarea (#52)
- Repeat Last Session button on tree library page (#53)
- Auto-recovery banner for incomplete sessions (#54)
- Copy individual step to clipboard on session detail (#55)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add missing delete button to table and list tree views

The onDeleteTree prop was accepted but never used in TreeTableView and
TreeListView. Now both views show a trash icon (permission-gated) matching
the existing grid view behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit was merged in pull request #72.
This commit is contained in:
chihlasm
2026-02-10 19:40:45 -05:00
committed by GitHub
parent 84fa554a7a
commit 402cdea063
7 changed files with 271 additions and 52 deletions

View File

@@ -30,6 +30,7 @@ export function SessionDetailPage() {
const [showRatingModal, setShowRatingModal] = useState(false)
const [isSavingRatings, setIsSavingRatings] = useState(false)
const [librarySteps, setLibrarySteps] = useState<Step[]>([])
const [copiedStepIndex, setCopiedStepIndex] = useState<number | null>(null)
useEffect(() => {
if (id) {
@@ -217,6 +218,21 @@ export function SessionDetailPage() {
}
}
const handleCopyStep = async (decision: Session['decisions'][number], index: number) => {
const lines: string[] = []
if (decision.question) lines.push(`Question: ${decision.question}`)
if (decision.answer) lines.push(`Answer: ${decision.answer}`)
if (decision.action_performed) lines.push(`Action: ${decision.action_performed}`)
if (decision.notes) lines.push(`Notes: ${decision.notes}`)
try {
await navigator.clipboard.writeText(lines.join('\n'))
setCopiedStepIndex(index)
setTimeout(() => setCopiedStepIndex(null), 2000)
} catch {
// Clipboard access denied
}
}
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString()
}
@@ -367,25 +383,40 @@ export function SessionDetailPage() {
<div className="relative">
<span className="absolute -left-[1.625rem] top-1 h-2 w-2 rounded-full bg-white/20" />
<div className="glass-card rounded-xl p-4">
{decision.question && (
<p className="font-medium text-white">{decision.question}</p>
)}
{decision.answer && (
<p className="mt-1 text-sm text-white">Answer: {decision.answer}</p>
)}
{decision.action_performed && (
<p className="mt-1 text-sm text-white/40">
Action: {decision.action_performed}
</p>
)}
{decision.notes && (
<p className="mt-2 rounded bg-white/5 p-2 text-sm text-white/40">
Notes: {decision.notes}
</p>
)}
<p className="mt-2 text-xs text-white/40">
{formatDate(decision.timestamp)}
</p>
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
{decision.question && (
<p className="font-medium text-white">{decision.question}</p>
)}
{decision.answer && (
<p className="mt-1 text-sm text-white">Answer: {decision.answer}</p>
)}
{decision.action_performed && (
<p className="mt-1 text-sm text-white/40">
Action: {decision.action_performed}
</p>
)}
{decision.notes && (
<p className="mt-2 rounded bg-white/5 p-2 text-sm text-white/40">
Notes: {decision.notes}
</p>
)}
<p className="mt-2 text-xs text-white/40">
{formatDate(decision.timestamp)}
</p>
</div>
<button
onClick={() => handleCopyStep(decision, index)}
title="Copy step to clipboard"
className="rounded p-1 text-white/30 hover:bg-white/10 hover:text-white"
>
{copiedStepIndex === index ? (
<Check className="h-4 w-4 text-emerald-400" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
</div>
</div>
</div>