Files
hapi/web/src/components/SessionHeader.tsx
T
weishu 4d18789d50 refactor: extract permission mode utilities and consolidate styling logic
Extract permission mode display logic into shared utilities for better reusability and maintainability. Add PermissionModeTone type and related helpers to centralize mode-based styling rules across components. Update components to use new PermissionModeOption type for consistent permission mode presentation.
2026-01-03 22:49:40 +08:00

194 lines
7.1 KiB
TypeScript

import { useMemo, useState } from 'react'
import type { Session } from '@/types/api'
import type { ApiClient } from '@/api/client'
import { isTelegramApp } from '@/hooks/useTelegram'
import { useSessionActions } from '@/hooks/mutations/useSessionActions'
import { SessionActionMenu } from '@/components/SessionActionMenu'
import { RenameSessionDialog } from '@/components/RenameSessionDialog'
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'
function getSessionTitle(session: Session): string {
if (session.metadata?.name) {
return session.metadata.name
}
if (session.metadata?.summary?.text) {
return session.metadata.summary.text
}
if (session.metadata?.path) {
const parts = session.metadata.path.split('/').filter(Boolean)
return parts.length > 0 ? parts[parts.length - 1] : session.id.slice(0, 8)
}
return session.id.slice(0, 8)
}
function FilesIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<path d="M14 2H7a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
<path d="M14 2v6h6" />
</svg>
)
}
function MoreVerticalIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="currentColor"
className={props.className}
>
<circle cx="12" cy="5" r="2" />
<circle cx="12" cy="12" r="2" />
<circle cx="12" cy="19" r="2" />
</svg>
)
}
export function SessionHeader(props: {
session: Session
onBack: () => void
onViewFiles?: () => void
api: ApiClient | null
onSessionDeleted?: () => void
}) {
const { session, api, onSessionDeleted } = props
const title = useMemo(() => getSessionTitle(session), [session])
const worktreeBranch = session.metadata?.worktree?.branch
const [menuOpen, setMenuOpen] = useState(false)
const [renameOpen, setRenameOpen] = useState(false)
const [archiveOpen, setArchiveOpen] = useState(false)
const [deleteOpen, setDeleteOpen] = useState(false)
const { archiveSession, renameSession, deleteSession, isPending } = useSessionActions(
api,
session.id,
session.metadata?.flavor ?? null
)
const handleDelete = async () => {
await deleteSession()
onSessionDeleted?.()
}
// In Telegram, don't render header (Telegram provides its own)
if (isTelegramApp()) {
return null
}
return (
<>
<div className="bg-[var(--app-bg)] pt-[env(safe-area-inset-top)]">
<div className="mx-auto w-full max-w-content flex items-center gap-2 p-3">
{/* Back button */}
<button
type="button"
onClick={props.onBack}
className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="15 18 9 12 15 6" />
</svg>
</button>
{/* Session info - two lines: title and path */}
<div className="min-w-0 flex-1">
<div className="truncate font-semibold">
{title}
</div>
<div className="text-xs text-[var(--app-hint)] truncate">
{session.metadata?.path ?? session.id}
{worktreeBranch ? ` • worktree: ${worktreeBranch}` : ''}
</div>
</div>
{props.onViewFiles ? (
<button
type="button"
onClick={props.onViewFiles}
className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]"
title="Files"
>
<FilesIcon />
</button>
) : null}
<button
type="button"
onClick={() => setMenuOpen(true)}
className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]"
title="More actions"
>
<MoreVerticalIcon />
</button>
</div>
</div>
<SessionActionMenu
isOpen={menuOpen}
onClose={() => setMenuOpen(false)}
sessionActive={session.active}
onRename={() => setRenameOpen(true)}
onArchive={() => setArchiveOpen(true)}
onDelete={() => setDeleteOpen(true)}
/>
<RenameSessionDialog
isOpen={renameOpen}
onClose={() => setRenameOpen(false)}
currentName={title}
onRename={renameSession}
isPending={isPending}
/>
<ConfirmDialog
isOpen={archiveOpen}
onClose={() => setArchiveOpen(false)}
title="Archive Session"
description={`Are you sure you want to archive "${title}"? This will disconnect the active session.`}
confirmLabel="Archive"
confirmingLabel="Archiving..."
onConfirm={archiveSession}
isPending={isPending}
destructive
/>
<ConfirmDialog
isOpen={deleteOpen}
onClose={() => setDeleteOpen(false)}
title="Delete Session"
description={`Are you sure you want to delete "${title}"? This action cannot be undone.`}
confirmLabel="Delete"
confirmingLabel="Deleting..."
onConfirm={handleDelete}
isPending={isPending}
destructive
/>
</>
)
}