mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
Make agent session history easier to scan (#5438)
This commit is contained in:
@@ -112,7 +112,7 @@ export function __resetPersistedWindowsPathCacheForTests(): void {
|
||||
}
|
||||
|
||||
export function mergePersistedWindowsPath(
|
||||
env: Record<string, string>,
|
||||
env: NodeJS.ProcessEnv,
|
||||
options: ReadWindowsPathOptions = {}
|
||||
): void {
|
||||
const platform = options.platform ?? process.platform
|
||||
|
||||
@@ -220,7 +220,7 @@ export default function AiVaultPanel(): React.JSX.Element {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-sidebar">
|
||||
<div className="@container/ai-vault flex h-full min-h-0 flex-col bg-sidebar">
|
||||
<AiVaultPanelHeader
|
||||
query={query}
|
||||
loading={loading}
|
||||
|
||||
@@ -36,7 +36,7 @@ import { translate } from '@/i18n/i18n'
|
||||
const VAULT_HEADER_CONTROL_CLASS = 'size-6 shrink-0'
|
||||
|
||||
const VAULT_SCOPE_TOGGLE_ITEM_CLASS =
|
||||
'h-6 min-h-6 min-w-0 border border-transparent bg-transparent px-1.5 text-[10px] font-medium leading-none text-foreground shadow-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground aria-[checked=true]:border-foreground/20 aria-[checked=true]:bg-foreground/10 aria-[checked=true]:text-foreground aria-[checked=true]:shadow-xs aria-[checked=true]:hover:bg-foreground/15 aria-[checked=true]:hover:text-foreground data-[state=on]:border-foreground/20 data-[state=on]:bg-foreground/10 data-[state=on]:text-foreground data-[state=on]:shadow-xs data-[state=on]:hover:bg-foreground/15 data-[state=on]:hover:text-foreground'
|
||||
'h-6 min-h-6 min-w-0 border border-transparent bg-transparent px-1.5 text-[10px] font-medium leading-none text-foreground shadow-none hover:bg-sidebar-accent hover:text-sidebar-accent-foreground aria-[checked=true]:border-foreground/20 aria-[checked=true]:bg-foreground/10 aria-[checked=true]:text-foreground aria-[checked=true]:shadow-xs aria-[checked=true]:hover:bg-foreground/15 aria-[checked=true]:hover:text-foreground data-[state=on]:border-foreground/20 data-[state=on]:bg-foreground/10 data-[state=on]:text-foreground data-[state=on]:shadow-xs data-[state=on]:hover:bg-foreground/15 data-[state=on]:hover:text-foreground @max-[300px]/ai-vault:px-1'
|
||||
|
||||
export function VaultGroupHeader({
|
||||
group,
|
||||
|
||||
@@ -56,28 +56,47 @@ export function AiVaultPanelHeader({
|
||||
}: AiVaultPanelHeaderProps): React.JSX.Element {
|
||||
return (
|
||||
<div className="shrink-0 border-b border-sidebar-border px-2.5 py-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 @max-[300px]/ai-vault:items-start">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-xs font-semibold text-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.sessionHistory',
|
||||
'Agent Session History'
|
||||
)}
|
||||
{/* Why: below 300px the header competes with fixed controls, so compact copy prevents overlap. */}
|
||||
<span className="@max-[300px]/ai-vault:hidden">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.sessionHistory',
|
||||
'Agent Session History'
|
||||
)}
|
||||
</span>
|
||||
<span className="hidden @max-[300px]/ai-vault:inline">
|
||||
{translate('auto.components.right.sidebar.AiVaultPanel.agents', 'Agents')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="truncate text-[11px] text-muted-foreground">
|
||||
{hasScanResult
|
||||
? translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.shownRecent',
|
||||
'{{value0}} shown · {{value1}} recent',
|
||||
{ value0: shownCount, value1: sessionCount }
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.resumePastSessions',
|
||||
'Resume past sessions'
|
||||
)}
|
||||
{hasScanResult ? (
|
||||
<>
|
||||
<span className="@max-[300px]/ai-vault:hidden">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.shownRecent',
|
||||
'{{value0}} shown · {{value1}} recent',
|
||||
{ value0: shownCount, value1: sessionCount }
|
||||
)}
|
||||
</span>
|
||||
<span className="hidden @max-[300px]/ai-vault:inline">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.sessionsShownCompact',
|
||||
'{{value0}} shown',
|
||||
{ value0: shownCount }
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.resumePastSessions',
|
||||
'Resume past sessions'
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
<div className="flex shrink-0 items-center gap-1 @max-[300px]/ai-vault:gap-0.5">
|
||||
<VaultScopeSwitch
|
||||
scope={scope}
|
||||
workspaceAvailable={Boolean(activeWorktreePath)}
|
||||
|
||||
@@ -1,308 +1,225 @@
|
||||
import type React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { Copy, FileJson, FolderOpen, MoreHorizontal, Play } from 'lucide-react'
|
||||
import { Copy, Play } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { HoverCardContent } from '@/components/ui/hover-card'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { AgentIcon } from '@/lib/agent-catalog'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { agentLabel } from './ai-vault-session-filters'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
export function SessionDetailsHoverCard({
|
||||
session,
|
||||
resumeCommand
|
||||
}: {
|
||||
session: AiVaultSession
|
||||
resumeCommand: string
|
||||
}): React.JSX.Element {
|
||||
const updatedAt = session.updatedAt ?? session.modifiedAt
|
||||
|
||||
return (
|
||||
<HoverCardContent
|
||||
side="left"
|
||||
align="start"
|
||||
sideOffset={8}
|
||||
className="scrollbar-sleek max-h-[min(28rem,calc(100vh-2rem))] w-80 overflow-y-auto p-3"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-start gap-2">
|
||||
<span className="mt-0.5 flex size-5 shrink-0 items-center justify-center text-muted-foreground">
|
||||
<AgentIcon agent={session.agent} size={16} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="line-clamp-2 text-[13px] font-medium leading-5 text-popover-foreground">
|
||||
{session.title}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] text-muted-foreground">
|
||||
{agentLabel(session.agent)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div className="mt-3 space-y-1.5 text-[11px] leading-4">
|
||||
<DetailLine
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.updated',
|
||||
'Updated'
|
||||
)}
|
||||
value={formatDateTime(updatedAt)}
|
||||
/>
|
||||
<DetailLine
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.created',
|
||||
'Created'
|
||||
)}
|
||||
value={formatDateTime(session.createdAt)}
|
||||
/>
|
||||
{session.branch ? (
|
||||
<DetailLine
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.branch',
|
||||
'Branch'
|
||||
)}
|
||||
value={session.branch}
|
||||
/>
|
||||
) : null}
|
||||
{session.model ? (
|
||||
<DetailLine
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.model',
|
||||
'Model'
|
||||
)}
|
||||
value={session.model}
|
||||
/>
|
||||
) : null}
|
||||
<DetailLine
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.usage',
|
||||
'Usage'
|
||||
)}
|
||||
value={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.usageValue',
|
||||
'{{value0}} msgs{{value1}}',
|
||||
{
|
||||
value0: session.messageCount,
|
||||
value1:
|
||||
session.totalTokens > 0
|
||||
? translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.tokenSuffix',
|
||||
' · {{value0}} tok',
|
||||
{ value0: formatTokenCount(session.totalTokens) }
|
||||
)
|
||||
: ''
|
||||
}
|
||||
)}
|
||||
/>
|
||||
<DetailLine
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.session',
|
||||
'Session'
|
||||
)}
|
||||
value={session.sessionId}
|
||||
mono
|
||||
/>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
||||
<div className="mt-3 border-t border-border/60 pt-2">
|
||||
<div className="mb-1.5 text-[11px] font-semibold text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.latestLog',
|
||||
'Latest log'
|
||||
)}
|
||||
</div>
|
||||
{session.previewMessages.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{session.previewMessages.map((message, index) => (
|
||||
<div key={`${message.role}:${index}`} className="min-w-0">
|
||||
<div className="mb-0.5 text-[10px] font-semibold uppercase text-muted-foreground">
|
||||
{previewRoleLabel(message.role)}
|
||||
</div>
|
||||
<div className="line-clamp-3 whitespace-pre-wrap text-[11px] leading-4 text-popover-foreground/90">
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[11px] leading-4 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.noReadablePreview',
|
||||
'No readable message preview in this transcript.'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 border-t border-border/60 pt-2">
|
||||
<div className="mb-1 text-[11px] font-semibold text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.resumeCommand',
|
||||
'Resume command'
|
||||
)}
|
||||
</div>
|
||||
<div className="line-clamp-3 break-all font-mono text-[10.5px] leading-4 text-popover-foreground/85">
|
||||
{resumeCommand}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
)
|
||||
}
|
||||
|
||||
function DetailLine({
|
||||
label,
|
||||
value,
|
||||
mono = false
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
mono?: boolean
|
||||
}): React.JSX.Element {
|
||||
const handleCopy = useCallback(() => {
|
||||
void window.api.ui.writeClipboardText(value).then(() => {
|
||||
toast.success(
|
||||
translate('auto.components.right.sidebar.AiVaultPanel.valueCopied', '{{value0}} copied', {
|
||||
value0: label
|
||||
})
|
||||
)
|
||||
})
|
||||
}, [label, value])
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-[4.5rem_minmax(0,1fr)] gap-2">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
'min-w-0 truncate text-left text-popover-foreground/90 transition-colors hover:text-popover-foreground',
|
||||
mono && 'font-mono'
|
||||
)}
|
||||
aria-label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyDetailValue',
|
||||
'Copy {{value0}}',
|
||||
{ value0: label }
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="top"
|
||||
sideOffset={4}
|
||||
className="pointer-events-none max-w-[min(20rem,calc(100vw-2rem))] break-all"
|
||||
>
|
||||
{value}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function MetaDot(): React.JSX.Element {
|
||||
return <span className="shrink-0 text-muted-foreground/45">·</span>
|
||||
}
|
||||
|
||||
export function SessionActionsMenu({
|
||||
export function SessionInlineDetails({
|
||||
id,
|
||||
session,
|
||||
onResume,
|
||||
onCopyResume,
|
||||
onCopyId,
|
||||
onCopyPath,
|
||||
onOpenLog,
|
||||
onRevealLog,
|
||||
onOpenCwd,
|
||||
resumeDisabled
|
||||
}: {
|
||||
id: string
|
||||
session: AiVaultSession
|
||||
onResume: () => void
|
||||
onCopyResume: () => void
|
||||
onCopyId: () => void
|
||||
onCopyPath: () => void
|
||||
onOpenLog: () => void
|
||||
onRevealLog: () => void
|
||||
onOpenCwd?: () => void
|
||||
resumeDisabled: boolean
|
||||
}): React.JSX.Element {
|
||||
const updatedAt = session.updatedAt ?? session.modifiedAt
|
||||
const usage = translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.usageValue',
|
||||
'{{value0}} msgs{{value1}}',
|
||||
{
|
||||
value0: session.messageCount,
|
||||
value1:
|
||||
session.totalTokens > 0
|
||||
? translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.tokenSuffix',
|
||||
' · {{value0}} tok',
|
||||
{ value0: formatTokenCount(session.totalTokens) }
|
||||
)
|
||||
: ''
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div
|
||||
id={id}
|
||||
className="mt-2 rounded-md border border-sidebar-border bg-sidebar-accent/25 p-2"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
onDoubleClick={(event) => event.stopPropagation()}
|
||||
onDragStart={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<span className="mt-0.5 flex size-5 shrink-0 items-center justify-center text-muted-foreground">
|
||||
<AgentIcon agent={session.agent} size={16} />
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="line-clamp-2 text-[12px] font-medium leading-4 text-foreground">
|
||||
{session.title}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
|
||||
{agentLabel(session.agent)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid gap-1 text-[11px] leading-4">
|
||||
<SessionDetailCopyRow
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.updated',
|
||||
'Updated'
|
||||
)}
|
||||
value={formatDateTime(updatedAt)}
|
||||
/>
|
||||
<SessionDetailCopyRow
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.created',
|
||||
'Created'
|
||||
)}
|
||||
value={formatDateTime(session.createdAt)}
|
||||
/>
|
||||
{session.model ? (
|
||||
<SessionDetailCopyRow
|
||||
label={translate('auto.components.right.sidebar.AiVaultSessionDetails.model', 'Model')}
|
||||
value={session.model}
|
||||
/>
|
||||
) : null}
|
||||
{session.branch ? (
|
||||
<SessionDetailCopyRow
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.branch',
|
||||
'Branch'
|
||||
)}
|
||||
value={session.branch}
|
||||
/>
|
||||
) : null}
|
||||
<SessionDetailCopyRow
|
||||
label={translate('auto.components.right.sidebar.AiVaultSessionDetails.usage', 'Usage')}
|
||||
value={usage}
|
||||
/>
|
||||
<SessionDetailCopyRow
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.session',
|
||||
'Session'
|
||||
)}
|
||||
copyLabel={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.sessionId',
|
||||
'Session ID'
|
||||
)}
|
||||
value={session.sessionId}
|
||||
mono
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid gap-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.sessionActions',
|
||||
'{{value0}} session actions',
|
||||
{ value0: agentLabel(session.agent) }
|
||||
)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
disabled={resumeDisabled}
|
||||
draggable={false}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onResume()
|
||||
}}
|
||||
className="h-7 justify-start px-2 text-[11px]"
|
||||
>
|
||||
<MoreHorizontal className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem disabled={resumeDisabled} onSelect={onResume}>
|
||||
<Play className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.resumeInNewTab',
|
||||
'Resume in New Tab'
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={onCopyResume}>
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
draggable={false}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onCopyResume()
|
||||
}}
|
||||
className="h-7 justify-start px-2 text-[11px]"
|
||||
>
|
||||
<Copy className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyResumeCommand',
|
||||
'Copy Resume Command'
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={onOpenLog}>
|
||||
<FileJson className="size-3.5" />
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionDetails.openLog', 'Open Log')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={onRevealLog}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionDetails.revealLog', 'Reveal Log')}
|
||||
</DropdownMenuItem>
|
||||
{onOpenCwd ? (
|
||||
<DropdownMenuItem onSelect={onOpenCwd}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.openWorkingDirectory',
|
||||
'Open Working Directory'
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionDetailCopyRow({
|
||||
label,
|
||||
copyLabel = label,
|
||||
value,
|
||||
mono = false
|
||||
}: {
|
||||
label: string
|
||||
copyLabel?: string
|
||||
value: string
|
||||
mono?: boolean
|
||||
}): React.JSX.Element {
|
||||
const handleCopy = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
||||
event.stopPropagation()
|
||||
void window.api.ui
|
||||
.writeClipboardText(value)
|
||||
.then(() => {
|
||||
toast.success(
|
||||
translate('auto.components.right.sidebar.AiVaultPanel.valueCopied', '{{value0}} copied', {
|
||||
value0: copyLabel
|
||||
})
|
||||
)
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.right.sidebar.AiVaultPanel.valueCopyFailed',
|
||||
'Unable to copy {{value0}}',
|
||||
{ value0: copyLabel }
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid min-w-0 grid-cols-[4.5rem_minmax(0,1fr)_1.5rem] items-center gap-2 rounded-sm px-1 py-0.5">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className={cn('min-w-0 truncate text-foreground/90', mono && 'font-mono')}>
|
||||
{value}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
draggable={false}
|
||||
onClick={handleCopy}
|
||||
aria-label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyDetailValue',
|
||||
'Copy {{value0}}',
|
||||
{ value0: copyLabel }
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onSelect={onCopyId}>
|
||||
className="size-5 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<Copy className="size-3" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copySessionId',
|
||||
'Copy Session ID'
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyDetailValue',
|
||||
'Copy {{value0}}',
|
||||
{ value0: copyLabel }
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onSelect={onCopyPath}>
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyLogPath',
|
||||
'Copy Log Path'
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -344,22 +261,6 @@ function formatDateTime(value: string | null): string {
|
||||
return new Date(timestamp).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
|
||||
}
|
||||
|
||||
function previewRoleLabel(role: AiVaultSession['previewMessages'][number]['role']): string {
|
||||
if (role === 'user') {
|
||||
return translate('auto.components.right.sidebar.AiVaultSessionDetails.user', 'User')
|
||||
}
|
||||
if (role === 'assistant') {
|
||||
return translate('auto.components.right.sidebar.AiVaultSessionDetails.assistant', 'Assistant')
|
||||
}
|
||||
if (role === 'tool') {
|
||||
return translate('auto.components.right.sidebar.AiVaultSessionDetails.tool', 'Tool')
|
||||
}
|
||||
if (role === 'system') {
|
||||
return translate('auto.components.right.sidebar.AiVaultSessionDetails.system', 'System')
|
||||
}
|
||||
return translate('auto.components.right.sidebar.AiVaultSessionDetails.log', 'Log')
|
||||
}
|
||||
|
||||
function formatTimeAgo(timestamp: number): string {
|
||||
const diffMs = Date.now() - timestamp
|
||||
if (diffMs < 60_000) {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import type React from 'react'
|
||||
import { Copy, FileJson, FolderOpen, Play } from 'lucide-react'
|
||||
import { ChevronDown, Copy, FileJson, FolderOpen, MoreHorizontal, Play } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { HoverCard, HoverCardTrigger } from '@/components/ui/hover-card'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuContent,
|
||||
@@ -19,18 +26,14 @@ import {
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { agentLabel } from './ai-vault-session-filters'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import {
|
||||
formatTokenCount,
|
||||
MetaDot,
|
||||
SessionActionsMenu,
|
||||
SessionDetailsHoverCard,
|
||||
SessionTime
|
||||
} from './AiVaultSessionDetails'
|
||||
import { SessionInlineDetails, SessionTime } from './AiVaultSessionDetails'
|
||||
|
||||
export function VaultSessionRow({
|
||||
session,
|
||||
resumeCommand,
|
||||
detailsExpanded,
|
||||
resumeDisabled,
|
||||
onToggleDetails,
|
||||
onResume,
|
||||
onCopyResume,
|
||||
onCopyId,
|
||||
@@ -41,7 +44,9 @@ export function VaultSessionRow({
|
||||
}: {
|
||||
session: AiVaultSession
|
||||
resumeCommand: string
|
||||
detailsExpanded: boolean
|
||||
resumeDisabled: boolean
|
||||
onToggleDetails: () => void
|
||||
onResume: () => void
|
||||
onCopyResume: () => void
|
||||
onCopyId: () => void
|
||||
@@ -51,51 +56,62 @@ export function VaultSessionRow({
|
||||
onOpenCwd?: () => void
|
||||
}): React.JSX.Element {
|
||||
const updatedAt = session.updatedAt ?? session.modifiedAt
|
||||
const detailsId = getSessionDetailsId(session.id)
|
||||
const detailsTooltip = detailsExpanded
|
||||
? translate('auto.components.right.sidebar.AiVaultSessionRow.hideDetails', 'Hide Details')
|
||||
: translate('auto.components.right.sidebar.AiVaultSessionRow.showDetails', 'Show Details')
|
||||
|
||||
return (
|
||||
<HoverCard openDelay={350} closeDelay={80}>
|
||||
<ContextMenu>
|
||||
<HoverCardTrigger asChild>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
draggable={!resumeDisabled}
|
||||
className={cn(
|
||||
'group relative flex min-h-[64px] w-full items-start border-b border-sidebar-border px-3 py-2 text-left transition-colors hover:bg-sidebar-accent/55',
|
||||
!resumeDisabled && 'cursor-grab active:cursor-grabbing'
|
||||
)}
|
||||
onDragStart={(event) => {
|
||||
if (resumeDisabled) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
writeAiVaultSessionDragData(event.dataTransfer, {
|
||||
agent: session.agent,
|
||||
sessionId: session.sessionId,
|
||||
title: session.title,
|
||||
command: resumeCommand
|
||||
})
|
||||
window.dispatchEvent(new Event(AI_VAULT_SESSION_DRAG_START_EVENT))
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
window.dispatchEvent(new Event(AI_VAULT_SESSION_DRAG_END_EVENT))
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (!resumeDisabled) {
|
||||
onResume()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1 pr-16">
|
||||
<div className="truncate text-[13px] font-medium leading-5 text-foreground">
|
||||
{session.title}
|
||||
</div>
|
||||
<SessionMetadata session={session} />
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger asChild>
|
||||
<div
|
||||
draggable={!resumeDisabled}
|
||||
className={cn(
|
||||
'group relative flex min-h-[64px] w-full flex-col border-b border-sidebar-border px-3 py-2 text-left transition-colors hover:bg-sidebar-accent/55',
|
||||
!resumeDisabled && 'cursor-grab active:cursor-grabbing'
|
||||
)}
|
||||
onDragStart={(event) => {
|
||||
if (resumeDisabled) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
writeAiVaultSessionDragData(event.dataTransfer, {
|
||||
agent: session.agent,
|
||||
sessionId: session.sessionId,
|
||||
title: session.title,
|
||||
command: resumeCommand
|
||||
})
|
||||
window.dispatchEvent(new Event(AI_VAULT_SESSION_DRAG_START_EVENT))
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
window.dispatchEvent(new Event(AI_VAULT_SESSION_DRAG_END_EVENT))
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
if (!resumeDisabled) {
|
||||
onResume()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="min-w-0 flex-1 pr-24">
|
||||
<div className="flex min-w-0 items-start gap-1.5">
|
||||
<div className="min-w-0 flex-1 truncate text-[13px] font-medium leading-5 text-foreground">
|
||||
{session.title}
|
||||
</div>
|
||||
<SessionTime
|
||||
value={updatedAt}
|
||||
className="absolute right-3 top-2 transition-opacity group-hover:opacity-0 group-focus-within:opacity-0"
|
||||
/>
|
||||
<div className="pointer-events-none absolute right-2 top-1.5 flex items-center gap-1 rounded-md bg-sidebar/95 opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100">
|
||||
<SessionTime value={updatedAt} className="mt-0.5 @max-[300px]/ai-vault:hidden" />
|
||||
</div>
|
||||
<SessionMetadata session={session} />
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute right-2 top-1.5 flex items-center gap-1 rounded-md bg-sidebar/95"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onDoubleClick={(event) => event.stopPropagation()}
|
||||
onDragStart={(event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
@@ -106,15 +122,82 @@ export function VaultSessionRow({
|
||||
{ value0: agentLabel(session.agent) }
|
||||
)}
|
||||
disabled={resumeDisabled}
|
||||
draggable={false}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onResume()
|
||||
}}
|
||||
className="opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100 group-focus-within:pointer-events-auto group-focus-within:opacity-100"
|
||||
>
|
||||
<Play className="size-3.5" />
|
||||
</Button>
|
||||
<SessionActionsMenu
|
||||
session={session}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.resumeInNewTab',
|
||||
'Resume in New Tab'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.toggleSessionDetails',
|
||||
'{{value0}} session details',
|
||||
{ value0: agentLabel(session.agent) }
|
||||
)}
|
||||
aria-expanded={detailsExpanded}
|
||||
aria-controls={detailsId}
|
||||
draggable={false}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
onToggleDetails()
|
||||
}}
|
||||
className="pointer-events-auto"
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn('size-3.5 transition-transform', detailsExpanded && 'rotate-180')}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{detailsTooltip}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
aria-label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.moreSessionActions',
|
||||
'More Session Actions'
|
||||
)}
|
||||
draggable={false}
|
||||
className="pointer-events-auto"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<MoreHorizontal className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.moreActions',
|
||||
'More Actions'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent align="end">
|
||||
<SessionActionMenuItems
|
||||
resumeDisabled={resumeDisabled}
|
||||
onResume={onResume}
|
||||
onCopyResume={onCopyResume}
|
||||
onCopyId={onCopyId}
|
||||
@@ -122,103 +205,128 @@ export function VaultSessionRow({
|
||||
onOpenLog={onOpenLog}
|
||||
onRevealLog={onRevealLog}
|
||||
onOpenCwd={onOpenCwd}
|
||||
resumeDisabled={resumeDisabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
</HoverCardTrigger>
|
||||
<ContextMenuContent>
|
||||
<ContextMenuItem disabled={resumeDisabled} onSelect={onResume}>
|
||||
<Play className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.resumeInNewTab',
|
||||
'Resume in New Tab'
|
||||
)}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={onCopyResume}>
|
||||
<Copy className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.copyResumeCommand',
|
||||
'Copy Resume Command'
|
||||
)}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={onOpenLog}>
|
||||
<FileJson className="size-3.5" />
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionRow.openLog', 'Open Log')}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={onRevealLog}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionRow.revealLog', 'Reveal Log')}
|
||||
</ContextMenuItem>
|
||||
{onOpenCwd ? (
|
||||
<ContextMenuItem onSelect={onOpenCwd}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.openWorkingDirectory',
|
||||
'Open Working Directory'
|
||||
)}
|
||||
</ContextMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{detailsExpanded ? (
|
||||
<SessionInlineDetails
|
||||
id={detailsId}
|
||||
session={session}
|
||||
resumeDisabled={resumeDisabled}
|
||||
onResume={onResume}
|
||||
onCopyResume={onCopyResume}
|
||||
/>
|
||||
) : null}
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onSelect={onCopyId}>
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.copySessionId',
|
||||
'Copy Session ID'
|
||||
)}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onSelect={onCopyPath}>
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.copyLogPath',
|
||||
'Copy Log Path'
|
||||
)}
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
<SessionDetailsHoverCard session={session} resumeCommand={resumeCommand} />
|
||||
</HoverCard>
|
||||
</div>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent>
|
||||
<SessionActionMenuItems
|
||||
menuKind="context"
|
||||
resumeDisabled={resumeDisabled}
|
||||
onResume={onResume}
|
||||
onCopyResume={onCopyResume}
|
||||
onCopyId={onCopyId}
|
||||
onCopyPath={onCopyPath}
|
||||
onOpenLog={onOpenLog}
|
||||
onRevealLog={onRevealLog}
|
||||
onOpenCwd={onOpenCwd}
|
||||
/>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
)
|
||||
}
|
||||
|
||||
function SessionActionMenuItems({
|
||||
menuKind = 'dropdown',
|
||||
resumeDisabled,
|
||||
onResume,
|
||||
onCopyResume,
|
||||
onCopyId,
|
||||
onCopyPath,
|
||||
onOpenLog,
|
||||
onRevealLog,
|
||||
onOpenCwd
|
||||
}: {
|
||||
menuKind?: 'dropdown' | 'context'
|
||||
resumeDisabled: boolean
|
||||
onResume: () => void
|
||||
onCopyResume: () => void
|
||||
onCopyId: () => void
|
||||
onCopyPath: () => void
|
||||
onOpenLog: () => void
|
||||
onRevealLog: () => void
|
||||
onOpenCwd?: () => void
|
||||
}): React.JSX.Element {
|
||||
const Item = menuKind === 'context' ? ContextMenuItem : DropdownMenuItem
|
||||
const Separator = menuKind === 'context' ? ContextMenuSeparator : DropdownMenuSeparator
|
||||
|
||||
return (
|
||||
<>
|
||||
<Item disabled={resumeDisabled} onSelect={onResume}>
|
||||
<Play className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.resumeInNewTab',
|
||||
'Resume in New Tab'
|
||||
)}
|
||||
</Item>
|
||||
<Item onSelect={onCopyResume}>
|
||||
<Copy className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.copyResumeCommand',
|
||||
'Copy Resume Command'
|
||||
)}
|
||||
</Item>
|
||||
<Separator />
|
||||
<Item onSelect={onOpenLog}>
|
||||
<FileJson className="size-3.5" />
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionRow.openLog', 'Open Log')}
|
||||
</Item>
|
||||
<Item onSelect={onRevealLog}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionRow.revealLog', 'Reveal Log')}
|
||||
</Item>
|
||||
{onOpenCwd ? (
|
||||
<Item onSelect={onOpenCwd}>
|
||||
<FolderOpen className="size-3.5" />
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.openWorkingDirectory',
|
||||
'Open Working Directory'
|
||||
)}
|
||||
</Item>
|
||||
) : null}
|
||||
<Separator />
|
||||
<Item onSelect={onCopyId}>
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.copySessionId',
|
||||
'Copy Session ID'
|
||||
)}
|
||||
</Item>
|
||||
<Item onSelect={onCopyPath}>
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionRow.copyLogPath', 'Copy Log Path')}
|
||||
</Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function getSessionDetailsId(sessionId: string): string {
|
||||
return `ai-vault-session-details-${sessionId.replace(/[^A-Za-z0-9_-]/g, '-')}`
|
||||
}
|
||||
|
||||
function SessionMetadata({ session }: { session: AiVaultSession }): React.JSX.Element {
|
||||
return (
|
||||
<div className="mt-0.5 flex min-w-0 items-center gap-1.5 text-[11px] leading-4 text-muted-foreground">
|
||||
<span className="flex size-4 shrink-0 items-center justify-center text-muted-foreground">
|
||||
<AgentIcon agent={session.agent} size={14} />
|
||||
</span>
|
||||
{session.model ? (
|
||||
<>
|
||||
<MetaDot />
|
||||
<span className="max-w-[92px] truncate">{session.model}</span>
|
||||
</>
|
||||
) : null}
|
||||
{session.branch ? (
|
||||
<>
|
||||
<MetaDot />
|
||||
<span className="min-w-0 truncate text-muted-foreground/85">{session.branch}</span>
|
||||
</>
|
||||
) : null}
|
||||
<MetaDot />
|
||||
<span className="shrink-0">
|
||||
<span className="min-w-0 truncate">{agentLabel(session.agent)}</span>
|
||||
<span className="shrink-0 rounded-sm border border-sidebar-border bg-sidebar-accent/45 px-1.5 py-0.5 text-[10px] leading-none text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.messageCount',
|
||||
'{{value0}} msgs',
|
||||
{ value0: session.messageCount }
|
||||
)}
|
||||
</span>
|
||||
{session.totalTokens > 0 ? (
|
||||
<>
|
||||
<MetaDot />
|
||||
<span className="shrink-0">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionRow.tokenCount',
|
||||
'{{value0}} tok',
|
||||
{ value0: formatTokenCount(session.totalTokens) }
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useVirtualizer } from '@tanstack/react-virtual'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { useCallback, useMemo, useRef, useState } from 'react'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from './ai-vault-virtual-rows'
|
||||
|
||||
const VAULT_ROW_OVERSCAN = 8
|
||||
const VAULT_EXPANDED_SESSION_ROW_ESTIMATED_HEIGHT = 360
|
||||
|
||||
type AiVaultListRow =
|
||||
| { type: 'group'; group: AiVaultSessionGroup }
|
||||
@@ -58,6 +59,7 @@ export function AiVaultSessionVirtualList({
|
||||
const listScrollRef = useRef<HTMLDivElement>(null)
|
||||
const stickyRangeStartIndexRef = useRef(0)
|
||||
const activeStickyHeaderIndexRef = useRef<number | null>(null)
|
||||
const [expandedSessionIds, setExpandedSessionIds] = useState<Set<string>>(() => new Set())
|
||||
|
||||
const vaultRows = useMemo(() => {
|
||||
const rows: AiVaultListRow[] = []
|
||||
@@ -77,8 +79,16 @@ export function AiVaultSessionVirtualList({
|
||||
const virtualizer = useVirtualizer({
|
||||
count: vaultRows.length,
|
||||
getScrollElement: () => listScrollRef.current,
|
||||
estimateSize: (index) =>
|
||||
vaultRows[index]?.type === 'group' ? VAULT_GROUP_HEADER_ROW_HEIGHT : VAULT_SESSION_ROW_HEIGHT,
|
||||
estimateSize: (index) => {
|
||||
const row = vaultRows[index]
|
||||
if (row?.type === 'group') {
|
||||
return VAULT_GROUP_HEADER_ROW_HEIGHT
|
||||
}
|
||||
if (row?.type === 'session' && expandedSessionIds.has(row.session.id)) {
|
||||
return VAULT_EXPANDED_SESSION_ROW_ESTIMATED_HEIGHT
|
||||
}
|
||||
return VAULT_SESSION_ROW_HEIGHT
|
||||
},
|
||||
overscan: VAULT_ROW_OVERSCAN,
|
||||
// Why: keep the active group header mounted so CSS sticky can pin it while
|
||||
// its sessions scroll underneath in the virtual list.
|
||||
@@ -98,6 +108,18 @@ export function AiVaultSessionVirtualList({
|
||||
}
|
||||
})
|
||||
|
||||
const toggleSessionDetails = useCallback((sessionId: string) => {
|
||||
setExpandedSessionIds((current) => {
|
||||
const next = new Set(current)
|
||||
if (next.has(sessionId)) {
|
||||
next.delete(sessionId)
|
||||
} else {
|
||||
next.add(sessionId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const virtualItems = virtualizer.getVirtualItems()
|
||||
activeStickyHeaderIndexRef.current = getActiveStickyHeaderIndexForScroll({
|
||||
rangeStartIndex: stickyRangeStartIndexRef.current,
|
||||
@@ -139,9 +161,11 @@ export function AiVaultSessionVirtualList({
|
||||
activeStickyHeaderIndex={activeStickyHeaderIndexRef.current}
|
||||
measureElement={virtualizer.measureElement}
|
||||
collapsedGroups={collapsedGroups}
|
||||
expandedSessionIds={expandedSessionIds}
|
||||
resumeDisabled={resumeDisabled}
|
||||
buildResumeCommand={buildResumeCommand}
|
||||
onToggleGroup={onToggleGroup}
|
||||
onToggleSessionDetails={toggleSessionDetails}
|
||||
onResume={onResume}
|
||||
onCopyResume={onCopyResume}
|
||||
onCopyId={onCopyId}
|
||||
@@ -164,9 +188,11 @@ function AiVaultVirtualRow({
|
||||
activeStickyHeaderIndex,
|
||||
measureElement,
|
||||
collapsedGroups,
|
||||
expandedSessionIds,
|
||||
resumeDisabled,
|
||||
buildResumeCommand,
|
||||
onToggleGroup,
|
||||
onToggleSessionDetails,
|
||||
onResume,
|
||||
onCopyResume,
|
||||
onCopyId,
|
||||
@@ -181,9 +207,11 @@ function AiVaultVirtualRow({
|
||||
activeStickyHeaderIndex: number | null
|
||||
measureElement: (node: Element | null) => void
|
||||
collapsedGroups: ReadonlySet<string>
|
||||
expandedSessionIds: ReadonlySet<string>
|
||||
resumeDisabled: boolean
|
||||
buildResumeCommand: (session: AiVaultSession) => string
|
||||
onToggleGroup: (key: string) => void
|
||||
onToggleSessionDetails: (sessionId: string) => void
|
||||
onResume: (session: AiVaultSession) => void
|
||||
onCopyResume: (session: AiVaultSession) => void
|
||||
onCopyId: (session: AiVaultSession) => void
|
||||
@@ -218,7 +246,9 @@ function AiVaultVirtualRow({
|
||||
<VaultSessionRow
|
||||
session={row.session}
|
||||
resumeCommand={buildResumeCommand(row.session)}
|
||||
detailsExpanded={expandedSessionIds.has(row.session.id)}
|
||||
resumeDisabled={resumeDisabled}
|
||||
onToggleDetails={() => onToggleSessionDetails(row.session.id)}
|
||||
onResume={() => onResume(row.session)}
|
||||
onCopyResume={() => onCopyResume(row.session)}
|
||||
onCopyId={() => onCopyId(row.session)}
|
||||
|
||||
@@ -8703,11 +8703,14 @@
|
||||
"AiVaultPanel": {
|
||||
"resumeCommandCopied": "Resume command copied",
|
||||
"valueCopied": "{{value0}} copied",
|
||||
"valueCopyFailed": "Unable to copy {{value0}}",
|
||||
"openWorkspaceBeforeResuming": "Open a workspace before resuming a session.",
|
||||
"localWorkspacesOnly": "Resume from history is only available in local workspaces.",
|
||||
"agentSessionQueued": "{{value0}} session queued",
|
||||
"sessionHistory": "Agent Session History",
|
||||
"agents": "Agents",
|
||||
"shownRecent": "{{value0}} shown · {{value1}} recent",
|
||||
"sessionsShownCompact": "{{value0}} shown",
|
||||
"resumePastSessions": "Resume past sessions",
|
||||
"refreshSessionHistory": "Refresh Session History",
|
||||
"searchSessions": "Search sessions",
|
||||
@@ -8756,6 +8759,7 @@
|
||||
"usageValue": "{{value0}} msgs{{value1}}",
|
||||
"tokenSuffix": " · {{value0}} tok",
|
||||
"session": "Session",
|
||||
"sessionId": "Session ID",
|
||||
"copyDetailValue": "Copy {{value0}}",
|
||||
"latestLog": "Latest log",
|
||||
"noReadablePreview": "No readable message preview in this transcript.",
|
||||
@@ -8785,6 +8789,7 @@
|
||||
"AiVaultSessionRow": {
|
||||
"resumeAgentSession": "Resume {{value0}} session",
|
||||
"resumeInNewTab": "Resume in New Tab",
|
||||
"toggleSessionDetails": "{{value0}} session details",
|
||||
"copyResumeCommand": "Copy Resume Command",
|
||||
"openLog": "Open Log",
|
||||
"revealLog": "Reveal Log",
|
||||
@@ -8792,7 +8797,11 @@
|
||||
"copySessionId": "Copy Session ID",
|
||||
"copyLogPath": "Copy Log Path",
|
||||
"messageCount": "{{value0}} msgs",
|
||||
"tokenCount": "{{value0}} tok"
|
||||
"tokenCount": "{{value0}} tok",
|
||||
"hideDetails": "Hide Details",
|
||||
"showDetails": "Show Details",
|
||||
"moreSessionActions": "More Session Actions",
|
||||
"moreActions": "More Actions"
|
||||
},
|
||||
"FileExplorerNameFilter": {
|
||||
"26fb73c6e3": "Find files",
|
||||
|
||||
@@ -8703,6 +8703,7 @@
|
||||
"AiVaultPanel": {
|
||||
"resumeCommandCopied": "Resume command copied",
|
||||
"valueCopied": "{{value0}} copied",
|
||||
"valueCopyFailed": "No se pudo copiar {{value0}}",
|
||||
"openWorkspaceBeforeResuming": "Open a workspace before resuming a session.",
|
||||
"localWorkspacesOnly": "Resume from history is only available in local workspaces.",
|
||||
"agentSessionQueued": "{{value0}} session queued",
|
||||
@@ -8716,8 +8717,10 @@
|
||||
"transcriptsSkipped": "{{count}} transcript skipped",
|
||||
"noAgentSessionsFound": "No agent sessions found",
|
||||
"noSessionsMatchFilters": "No sessions match the current filters",
|
||||
"sessionId": "Session ID",
|
||||
"logPath": "Log path"
|
||||
"sessionId": "ID de sesión",
|
||||
"logPath": "Ruta del registro",
|
||||
"agents": "Agentes",
|
||||
"sessionsShownCompact": "{{value0}} mostradas"
|
||||
},
|
||||
"AiVaultPanelControls": {
|
||||
"scanningSessions": "Scanning sessions",
|
||||
@@ -8780,7 +8783,8 @@
|
||||
"hoursAgo": "{{value0}}h ago",
|
||||
"daysAgo": "{{value0}}d ago",
|
||||
"monthsAgo": "{{value0}}mo ago",
|
||||
"yearsAgo": "{{value0}}y ago"
|
||||
"yearsAgo": "{{value0}}y ago",
|
||||
"sessionId": "ID de sesión"
|
||||
},
|
||||
"AiVaultSessionRow": {
|
||||
"resumeAgentSession": "Resume {{value0}} session",
|
||||
@@ -8792,7 +8796,12 @@
|
||||
"copySessionId": "Copy Session ID",
|
||||
"copyLogPath": "Copy Log Path",
|
||||
"messageCount": "{{value0}} msgs",
|
||||
"tokenCount": "{{value0}} tok"
|
||||
"tokenCount": "{{value0}} tok",
|
||||
"toggleSessionDetails": "Detalles de sesión de {{value0}}",
|
||||
"hideDetails": "Ocultar detalles",
|
||||
"showDetails": "Mostrar detalles",
|
||||
"moreSessionActions": "Más acciones de sesión",
|
||||
"moreActions": "Más acciones"
|
||||
},
|
||||
"FileExplorerNameFilter": {
|
||||
"26fb73c6e3": "Buscar archivos",
|
||||
|
||||
@@ -8703,6 +8703,7 @@
|
||||
"AiVaultPanel": {
|
||||
"resumeCommandCopied": "Resume command copied",
|
||||
"valueCopied": "{{value0}} copied",
|
||||
"valueCopyFailed": "{{value0}} をコピーできません",
|
||||
"openWorkspaceBeforeResuming": "Open a workspace before resuming a session.",
|
||||
"localWorkspacesOnly": "Resume from history is only available in local workspaces.",
|
||||
"agentSessionQueued": "{{value0}} session queued",
|
||||
@@ -8716,8 +8717,10 @@
|
||||
"transcriptsSkipped": "{{count}} transcript skipped",
|
||||
"noAgentSessionsFound": "No agent sessions found",
|
||||
"noSessionsMatchFilters": "No sessions match the current filters",
|
||||
"sessionId": "Session ID",
|
||||
"logPath": "Log path"
|
||||
"sessionId": "セッション ID",
|
||||
"logPath": "ログパス",
|
||||
"agents": "エージェント",
|
||||
"sessionsShownCompact": "{{value0}} 件表示"
|
||||
},
|
||||
"AiVaultPanelControls": {
|
||||
"scanningSessions": "Scanning sessions",
|
||||
@@ -8780,7 +8783,8 @@
|
||||
"hoursAgo": "{{value0}}h ago",
|
||||
"daysAgo": "{{value0}}d ago",
|
||||
"monthsAgo": "{{value0}}mo ago",
|
||||
"yearsAgo": "{{value0}}y ago"
|
||||
"yearsAgo": "{{value0}}y ago",
|
||||
"sessionId": "セッション ID"
|
||||
},
|
||||
"AiVaultSessionRow": {
|
||||
"resumeAgentSession": "Resume {{value0}} session",
|
||||
@@ -8792,7 +8796,12 @@
|
||||
"copySessionId": "Copy Session ID",
|
||||
"copyLogPath": "Copy Log Path",
|
||||
"messageCount": "{{value0}} msgs",
|
||||
"tokenCount": "{{value0}} tok"
|
||||
"tokenCount": "{{value0}} tok",
|
||||
"toggleSessionDetails": "{{value0}} のセッション詳細",
|
||||
"hideDetails": "詳細を非表示",
|
||||
"showDetails": "詳細を表示",
|
||||
"moreSessionActions": "セッションのその他の操作",
|
||||
"moreActions": "その他の操作"
|
||||
},
|
||||
"FileExplorerNameFilter": {
|
||||
"26fb73c6e3": "ファイルを検索",
|
||||
|
||||
@@ -8703,6 +8703,7 @@
|
||||
"AiVaultPanel": {
|
||||
"resumeCommandCopied": "Resume command copied",
|
||||
"valueCopied": "{{value0}} copied",
|
||||
"valueCopyFailed": "{{value0}}을(를) 복사할 수 없습니다",
|
||||
"openWorkspaceBeforeResuming": "Open a workspace before resuming a session.",
|
||||
"localWorkspacesOnly": "Resume from history is only available in local workspaces.",
|
||||
"agentSessionQueued": "{{value0}} session queued",
|
||||
@@ -8716,8 +8717,10 @@
|
||||
"transcriptsSkipped": "{{count}} transcript skipped",
|
||||
"noAgentSessionsFound": "No agent sessions found",
|
||||
"noSessionsMatchFilters": "No sessions match the current filters",
|
||||
"sessionId": "Session ID",
|
||||
"logPath": "Log path"
|
||||
"sessionId": "세션 ID",
|
||||
"logPath": "로그 경로",
|
||||
"agents": "에이전트",
|
||||
"sessionsShownCompact": "{{value0}} 표시됨"
|
||||
},
|
||||
"AiVaultPanelControls": {
|
||||
"scanningSessions": "Scanning sessions",
|
||||
@@ -8780,7 +8783,8 @@
|
||||
"hoursAgo": "{{value0}}h ago",
|
||||
"daysAgo": "{{value0}}d ago",
|
||||
"monthsAgo": "{{value0}}mo ago",
|
||||
"yearsAgo": "{{value0}}y ago"
|
||||
"yearsAgo": "{{value0}}y ago",
|
||||
"sessionId": "세션 ID"
|
||||
},
|
||||
"AiVaultSessionRow": {
|
||||
"resumeAgentSession": "Resume {{value0}} session",
|
||||
@@ -8792,7 +8796,12 @@
|
||||
"copySessionId": "Copy Session ID",
|
||||
"copyLogPath": "Copy Log Path",
|
||||
"messageCount": "{{value0}} msgs",
|
||||
"tokenCount": "{{value0}} tok"
|
||||
"tokenCount": "{{value0}} tok",
|
||||
"toggleSessionDetails": "{{value0}} 세션 세부정보",
|
||||
"hideDetails": "세부정보 숨기기",
|
||||
"showDetails": "세부정보 표시",
|
||||
"moreSessionActions": "세션 추가 작업",
|
||||
"moreActions": "추가 작업"
|
||||
},
|
||||
"FileExplorerNameFilter": {
|
||||
"26fb73c6e3": "파일 찾기",
|
||||
|
||||
@@ -8703,6 +8703,7 @@
|
||||
"AiVaultPanel": {
|
||||
"resumeCommandCopied": "Resume command copied",
|
||||
"valueCopied": "{{value0}} copied",
|
||||
"valueCopyFailed": "无法复制 {{value0}}",
|
||||
"openWorkspaceBeforeResuming": "Open a workspace before resuming a session.",
|
||||
"localWorkspacesOnly": "Resume from history is only available in local workspaces.",
|
||||
"agentSessionQueued": "{{value0}} session queued",
|
||||
@@ -8716,8 +8717,10 @@
|
||||
"transcriptsSkipped": "{{count}} transcript skipped",
|
||||
"noAgentSessionsFound": "No agent sessions found",
|
||||
"noSessionsMatchFilters": "No sessions match the current filters",
|
||||
"sessionId": "Session ID",
|
||||
"logPath": "Log path"
|
||||
"sessionId": "会话 ID",
|
||||
"logPath": "日志路径",
|
||||
"agents": "智能体",
|
||||
"sessionsShownCompact": "已显示 {{value0}} 个"
|
||||
},
|
||||
"AiVaultPanelControls": {
|
||||
"scanningSessions": "Scanning sessions",
|
||||
@@ -8780,7 +8783,8 @@
|
||||
"hoursAgo": "{{value0}}h ago",
|
||||
"daysAgo": "{{value0}}d ago",
|
||||
"monthsAgo": "{{value0}}mo ago",
|
||||
"yearsAgo": "{{value0}}y ago"
|
||||
"yearsAgo": "{{value0}}y ago",
|
||||
"sessionId": "会话 ID"
|
||||
},
|
||||
"AiVaultSessionRow": {
|
||||
"resumeAgentSession": "Resume {{value0}} session",
|
||||
@@ -8792,7 +8796,12 @@
|
||||
"copySessionId": "Copy Session ID",
|
||||
"copyLogPath": "Copy Log Path",
|
||||
"messageCount": "{{value0}} msgs",
|
||||
"tokenCount": "{{value0}} tok"
|
||||
"tokenCount": "{{value0}} tok",
|
||||
"toggleSessionDetails": "{{value0}} 会话详情",
|
||||
"hideDetails": "隐藏详情",
|
||||
"showDetails": "显示详情",
|
||||
"moreSessionActions": "更多会话操作",
|
||||
"moreActions": "更多操作"
|
||||
},
|
||||
"FileExplorerNameFilter": {
|
||||
"26fb73c6e3": "查找文件",
|
||||
|
||||
Reference in New Issue
Block a user