mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-22 22:01:18 +00:00
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.
194 lines
7.1 KiB
TypeScript
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
|
|
/>
|
|
</>
|
|
)
|
|
}
|