mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
The desktop app now keeps the composer stable while turns are active, reduces low-signal tool noise in the transcript, restores project context under the composer after session creation, and relies on the CLI's own permission requests instead of injecting broader desktop-side Bash asks. This also brings in the supporting desktop app source tree and the server routes/session metadata needed for git info, filesystem browsing, session resume, slash commands, and SDK-backed permission bridging so the UI can operate as a coherent feature instead of a partial patch. Constraint: Desktop transcript needs to stay usable during long multi-tool sessions without hiding file-change diffs Constraint: Permission prompts must mirror CLI behavior closely enough that read-only commands do not get desktop-only prompts Rejected: Keep rendering Read/Bash bodies inline | too noisy and unlike the intended transcript model Rejected: Commit only the touched desktop files | would leave the newly introduced desktop app incomplete in git history Confidence: medium Scope-risk: broad Reversibility: messy Directive: Treat non-writing tools as summary-first transcript events; do not re-expand them by default without validating the UX against long sessions Tested: cd desktop && bun run lint Tested: cd desktop && bun run test -- --run Tested: bun test src/server/__tests__/conversations.test.ts Not-tested: Manual visual regression against the exact screenshots in a live desktop session Not-tested: Full root TypeScript check (repository still has unrelated extracted-native parse failures)
92 lines
3.5 KiB
TypeScript
92 lines
3.5 KiB
TypeScript
import { useEffect } from 'react'
|
|
import { Modal } from '../shared/Modal'
|
|
|
|
type GalleryImage = {
|
|
src: string
|
|
name: string
|
|
}
|
|
|
|
type Props = {
|
|
open: boolean
|
|
images: GalleryImage[]
|
|
activeIndex: number
|
|
onClose: () => void
|
|
onSelect: (index: number) => void
|
|
}
|
|
|
|
export function ImageGalleryModal({ open, images, activeIndex, onClose, onSelect }: Props) {
|
|
const activeImage = images[activeIndex]
|
|
|
|
useEffect(() => {
|
|
if (!open || images.length <= 1) return
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key === 'ArrowLeft') {
|
|
event.preventDefault()
|
|
onSelect((activeIndex - 1 + images.length) % images.length)
|
|
} else if (event.key === 'ArrowRight') {
|
|
event.preventDefault()
|
|
onSelect((activeIndex + 1) % images.length)
|
|
}
|
|
}
|
|
document.addEventListener('keydown', handleKeyDown)
|
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
}, [activeIndex, images.length, onSelect, open])
|
|
|
|
if (!activeImage) return null
|
|
|
|
return (
|
|
<Modal open={open} onClose={onClose} width={960}>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-semibold text-[var(--color-text-primary)]">{activeImage.name}</div>
|
|
<div className="text-xs text-[var(--color-text-tertiary)]">
|
|
{activeIndex + 1} / {images.length}
|
|
</div>
|
|
</div>
|
|
{images.length > 1 && (
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={() => onSelect((activeIndex - 1 + images.length) % images.length)}
|
|
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]"
|
|
aria-label="Previous image"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">chevron_left</span>
|
|
</button>
|
|
<button
|
|
onClick={() => onSelect((activeIndex + 1) % images.length)}
|
|
className="flex h-9 w-9 items-center justify-center rounded-full border border-[var(--color-border)] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-surface-hover)]"
|
|
aria-label="Next image"
|
|
>
|
|
<span className="material-symbols-outlined text-[18px]">chevron_right</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex max-h-[70vh] items-center justify-center overflow-hidden rounded-2xl bg-[#111]">
|
|
<img src={activeImage.src} alt={activeImage.name} className="max-h-[70vh] w-full object-contain" />
|
|
</div>
|
|
|
|
{images.length > 1 && (
|
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
|
{images.map((image, index) => (
|
|
<button
|
|
key={`${image.name}-${index}`}
|
|
onClick={() => onSelect(index)}
|
|
className={`overflow-hidden rounded-xl border transition-all ${
|
|
index === activeIndex
|
|
? 'border-[var(--color-brand)] shadow-[0_0_0_1px_var(--color-brand)]'
|
|
: 'border-[var(--color-border)]'
|
|
}`}
|
|
>
|
|
<img src={image.src} alt={image.name} className="h-16 w-16 object-cover" />
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|