fix(ai-vault): label preview prompt honestly (#12178)
This commit is contained in:
parent
f3e087ec06
commit
c9a37f58d8
|
|
@ -1,12 +1,5 @@
|
|||
import type React from 'react'
|
||||
import {
|
||||
FileJson,
|
||||
FolderGit2,
|
||||
MessageSquare,
|
||||
MessageSquarePlus,
|
||||
Play,
|
||||
TextCursorInput
|
||||
} from 'lucide-react'
|
||||
import { FileJson, FolderGit2, MessageSquare, MessageSquarePlus, Play } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
|
@ -17,7 +10,7 @@ import {
|
|||
} from '../../../../shared/ai-vault-types'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { FirstPromptCard } from './ai-vault-first-prompt-card'
|
||||
import { sessionDetailConversationTurns, sessionFirstPrompt } from './ai-vault-session-display'
|
||||
import { sessionDetailConversationTurns, sessionPromptPreview } from './ai-vault-session-display'
|
||||
import { SessionSubagentsSection } from './AiVaultSessionSubagents'
|
||||
import { SessionUnsavedConversationNotice } from './AiVaultSessionUnsavedNotice'
|
||||
import {
|
||||
|
|
@ -59,7 +52,7 @@ export function SessionInlineDetails({
|
|||
const showResumeInNewTab =
|
||||
hasResumableContent &&
|
||||
(!resumeActions.worktree.worktreeId || Boolean(resumeActions.newTab.worktreeId))
|
||||
const firstPromptPreview = sessionFirstPrompt(session)
|
||||
const promptPreview = sessionPromptPreview(session)
|
||||
const detailTurns = sessionDetailConversationTurns(session, 3)
|
||||
const worktreeDisplay = worktreeInfo
|
||||
|
||||
|
|
@ -158,19 +151,7 @@ export function SessionInlineDetails({
|
|||
<div className="space-y-3 p-3">
|
||||
{hasResumableContent ? (
|
||||
<>
|
||||
<SessionReceiptSection
|
||||
icon={<TextCursorInput className="size-3" />}
|
||||
label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.firstPrompt',
|
||||
'First prompt'
|
||||
)}
|
||||
>
|
||||
<FirstPromptCard
|
||||
key={session.id}
|
||||
session={session}
|
||||
previewText={firstPromptPreview ?? ''}
|
||||
/>
|
||||
</SessionReceiptSection>
|
||||
<FirstPromptCard key={session.id} session={session} preview={promptPreview} />
|
||||
<SessionReceiptSection
|
||||
icon={<MessageSquare className="size-3" />}
|
||||
label={translate(
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { StrictMode } from 'react'
|
|||
import { cleanup, render, screen, waitFor } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FirstPromptCard } from './ai-vault-first-prompt-card'
|
||||
import { sessionPromptPreview } from './ai-vault-session-display'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
|
||||
const session = {
|
||||
|
|
@ -27,6 +28,61 @@ afterEach(() => {
|
|||
})
|
||||
|
||||
describe('FirstPromptCard', () => {
|
||||
it('labels stored firstUserPrompt text as the first prompt', () => {
|
||||
const remoteSession = {
|
||||
...session,
|
||||
executionHostId: 'ssh:dev-box',
|
||||
firstUserPrompt: 'The authoritative opening ask'
|
||||
} as AiVaultSession
|
||||
stubApi(vi.fn().mockResolvedValue({ prompt: null }))
|
||||
|
||||
render(
|
||||
<FirstPromptCard session={remoteSession} preview={sessionPromptPreview(remoteSession)} />
|
||||
)
|
||||
|
||||
expect(screen.getByText('First prompt')).toBeTruthy()
|
||||
expect(screen.getByText('The authoritative opening ask')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('labels a remote preview fallback as a recent prompt when no full read is available', () => {
|
||||
const getFirstUserPrompt = vi.fn().mockResolvedValue({ prompt: null })
|
||||
const remoteSession = {
|
||||
...session,
|
||||
executionHostId: 'ssh:dev-box',
|
||||
previewMessagesTruncated: true,
|
||||
previewMessages: [
|
||||
{ role: 'user', text: 'A recent ask from the sliding window', timestamp: null }
|
||||
]
|
||||
} as AiVaultSession
|
||||
stubApi(getFirstUserPrompt)
|
||||
|
||||
render(
|
||||
<FirstPromptCard session={remoteSession} preview={sessionPromptPreview(remoteSession)} />
|
||||
)
|
||||
|
||||
expect(screen.getByText('Recent prompt')).toBeTruthy()
|
||||
expect(screen.getByText('A recent ask from the sliding window')).toBeTruthy()
|
||||
expect(getFirstUserPrompt).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('relabels a local preview fallback after the full first prompt loads', async () => {
|
||||
const localSession = {
|
||||
...session,
|
||||
previewMessagesTruncated: true,
|
||||
previewMessages: [
|
||||
{ role: 'user', text: 'A recent ask from the sliding window', timestamp: null }
|
||||
]
|
||||
} as AiVaultSession
|
||||
stubApi(vi.fn().mockResolvedValue({ prompt: 'The authoritative opening ask' }))
|
||||
|
||||
render(<FirstPromptCard session={localSession} preview={sessionPromptPreview(localSession)} />)
|
||||
|
||||
expect(screen.getByText('Recent prompt')).toBeTruthy()
|
||||
expect(screen.getByText('A recent ask from the sliding window')).toBeTruthy()
|
||||
await waitFor(() => expect(screen.getByText('First prompt')).toBeTruthy())
|
||||
expect(screen.getByText('The authoritative opening ask')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('resolves loading under StrictMode double-invoke instead of stranding the card', async () => {
|
||||
// StrictMode mounts, cleans up, then re-mounts. The cleanup marks the first
|
||||
// request stale, so only a fresh second request can clear `loading`.
|
||||
|
|
@ -34,7 +90,7 @@ describe('FirstPromptCard', () => {
|
|||
|
||||
render(
|
||||
<StrictMode>
|
||||
<FirstPromptCard session={session} previewText="" />
|
||||
<FirstPromptCard session={session} preview={null} />
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
|
|
@ -47,7 +103,7 @@ describe('FirstPromptCard', () => {
|
|||
// A main process that never answers must not pin the card in `loading`.
|
||||
stubApi(vi.fn().mockReturnValue(new Promise(() => {})))
|
||||
|
||||
render(<FirstPromptCard session={session} previewText="" />)
|
||||
render(<FirstPromptCard session={session} preview={null} />)
|
||||
|
||||
expect(screen.getByText('Loading first prompt…')).toBeTruthy()
|
||||
await vi.advanceTimersByTimeAsync(15_000)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import type React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Check, Copy, LoaderCircle } from 'lucide-react'
|
||||
import { Check, Copy, LoaderCircle, TextCursorInput } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import type { AiVaultSession } from '../../../../shared/ai-vault-types'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import type { AiVaultSessionPromptPreview } from './ai-vault-session-display'
|
||||
|
||||
// Why: the main-process re-parse has no deadline of its own. Without this the
|
||||
// card can sit in `loading` forever on a huge or stalled transcript.
|
||||
|
|
@ -23,15 +24,11 @@ function canLoadFullFirstPrompt(
|
|||
|
||||
export function FirstPromptCard({
|
||||
session,
|
||||
previewText
|
||||
preview
|
||||
}: {
|
||||
session: AiVaultSession
|
||||
/**
|
||||
* Short preview from the list scan; replaced by the full on-demand body when
|
||||
* available. Empty unless the scan proved it really is the opening ask, so the
|
||||
* fallback below can never show a recent turn (remote rows never re-parse).
|
||||
*/
|
||||
previewText: string
|
||||
/** Short list-scan text, replaced by the full opening ask when available. */
|
||||
preview: AiVaultSessionPromptPreview | null
|
||||
}): React.JSX.Element {
|
||||
// Loading starts true when an on-demand re-parse is possible so the mount effect
|
||||
// does not need a sync setState (react-doctor: no-adjust-state-on-prop-change).
|
||||
|
|
@ -128,7 +125,9 @@ export function FirstPromptCard({
|
|||
}
|
||||
}, [loadFullPrompt])
|
||||
|
||||
const previewText = preview?.text ?? ''
|
||||
const displayText = (fullText ?? previewText).trim()
|
||||
const displaySource = fullText ? 'first-user-prompt' : preview?.source
|
||||
const showEmpty = !loading && !displayText
|
||||
|
||||
const copyFirstPrompt = (): void => {
|
||||
|
|
@ -143,12 +142,7 @@ export function FirstPromptCard({
|
|||
}
|
||||
return window.api.ui.writeClipboardText(copyText).then(() => {
|
||||
setCopied(true)
|
||||
toast.success(
|
||||
translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.firstPromptCopied',
|
||||
'First prompt copied'
|
||||
)
|
||||
)
|
||||
toast.success(promptCopiedLabel(loaded ? 'first-user-prompt' : preview?.source))
|
||||
window.setTimeout(() => {
|
||||
setCopied(false)
|
||||
}, 1400)
|
||||
|
|
@ -163,54 +157,103 @@ export function FirstPromptCard({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border/70 bg-foreground/[0.04] px-2.5 py-2">
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
|
||||
<span>
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionDetails.userRole', 'You')}
|
||||
</span>
|
||||
{loading || copying ? (
|
||||
<LoaderCircle className="size-3 shrink-0 animate-spin text-muted-foreground/70" />
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
draggable={false}
|
||||
disabled={copying || (!displayText && !loading)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
copyFirstPrompt()
|
||||
}}
|
||||
className="h-6 shrink-0 gap-1 px-1.5 text-[10px] text-muted-foreground"
|
||||
aria-label={translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyFirstPrompt',
|
||||
'Copy first prompt'
|
||||
)}
|
||||
>
|
||||
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||
{copied
|
||||
? translate('auto.components.right.sidebar.AiVaultSessionDetails.copied', 'Copied')
|
||||
: translate('auto.components.right.sidebar.AiVaultSessionDetails.copy', 'Copy')}
|
||||
</Button>
|
||||
<section className="space-y-1.5">
|
||||
<div className="flex items-center gap-1.5 text-[11px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
|
||||
<span className="text-muted-foreground/80">
|
||||
<TextCursorInput className="size-3" />
|
||||
</span>
|
||||
<span>{promptSectionLabel(displaySource)}</span>
|
||||
</div>
|
||||
{showEmpty ? (
|
||||
<p className="text-[11px] leading-4 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.noFirstPromptAvailable',
|
||||
'No first prompt available'
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="scrollbar-sleek max-h-48 select-text overflow-y-auto whitespace-pre-wrap text-[12px] leading-[1.35] text-foreground/90 [overflow-wrap:anywhere]">
|
||||
{displayText ||
|
||||
translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.loadingFirstPrompt',
|
||||
'Loading first prompt…'
|
||||
<div className="rounded-md border border-border/70 bg-foreground/[0.04] px-2.5 py-2">
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<div className="flex min-w-0 items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.05em] text-muted-foreground">
|
||||
<span>
|
||||
{translate('auto.components.right.sidebar.AiVaultSessionDetails.userRole', 'You')}
|
||||
</span>
|
||||
{loading || copying ? (
|
||||
<LoaderCircle className="size-3 shrink-0 animate-spin text-muted-foreground/70" />
|
||||
) : null}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
draggable={false}
|
||||
disabled={copying || (!displayText && !loading)}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
copyFirstPrompt()
|
||||
}}
|
||||
className="h-6 shrink-0 gap-1 px-1.5 text-[10px] text-muted-foreground"
|
||||
aria-label={copyPromptLabel(displaySource)}
|
||||
>
|
||||
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
|
||||
{copied
|
||||
? translate('auto.components.right.sidebar.AiVaultSessionDetails.copied', 'Copied')
|
||||
: translate('auto.components.right.sidebar.AiVaultSessionDetails.copy', 'Copy')}
|
||||
</Button>
|
||||
</div>
|
||||
{showEmpty ? (
|
||||
<p className="text-[11px] leading-4 text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.noFirstPromptAvailable',
|
||||
'No first prompt available'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</p>
|
||||
) : (
|
||||
<p className="scrollbar-sleek max-h-48 select-text overflow-y-auto whitespace-pre-wrap text-[12px] leading-[1.35] text-foreground/90 [overflow-wrap:anywhere]">
|
||||
{displayText ||
|
||||
translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.loadingFirstPrompt',
|
||||
'Loading first prompt…'
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function promptSectionLabel(source: AiVaultSessionPromptPreview['source'] | undefined): string {
|
||||
if (source === 'first-user-prompt') {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.firstPrompt',
|
||||
'First prompt'
|
||||
)
|
||||
}
|
||||
if (source === 'preview-window') {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.recentPrompt',
|
||||
'Recent prompt'
|
||||
)
|
||||
}
|
||||
return translate('auto.components.right.sidebar.AiVaultSessionDetails.prompt', 'Prompt')
|
||||
}
|
||||
|
||||
function copyPromptLabel(source: AiVaultSessionPromptPreview['source'] | undefined): string {
|
||||
if (source === 'first-user-prompt') {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyFirstPrompt',
|
||||
'Copy first prompt'
|
||||
)
|
||||
}
|
||||
if (source === 'preview-window') {
|
||||
return translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.copyRecentPrompt',
|
||||
'Copy recent prompt'
|
||||
)
|
||||
}
|
||||
return translate('auto.components.right.sidebar.AiVaultSessionDetails.copyPrompt', 'Copy prompt')
|
||||
}
|
||||
|
||||
function promptCopiedLabel(source: AiVaultSessionPromptPreview['source'] | undefined): string {
|
||||
return source === 'first-user-prompt'
|
||||
? translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.firstPromptCopied',
|
||||
'First prompt copied'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.right.sidebar.AiVaultSessionDetails.recentPromptCopied',
|
||||
'Recent prompt copied'
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import {
|
|||
latestSessionConversationTurn,
|
||||
recentSessionConversationTurns,
|
||||
sessionDetailConversationTurns,
|
||||
sessionFirstPrompt,
|
||||
sessionModelLabel,
|
||||
sessionPromptPreview,
|
||||
sessionPreviewSearchText
|
||||
} from './ai-vault-session-display'
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ describe('ai vault session display', () => {
|
|||
|
||||
it('prefers the stored firstUserPrompt over sliding preview turns', () => {
|
||||
expect(
|
||||
sessionFirstPrompt({
|
||||
sessionPromptPreview({
|
||||
...baseSession,
|
||||
firstUserPrompt: 'Original long first prompt that scrolled out of preview',
|
||||
previewMessages: [
|
||||
|
|
@ -111,23 +111,28 @@ describe('ai vault session display', () => {
|
|||
{ role: 'assistant', text: 'Later reply', timestamp: null }
|
||||
]
|
||||
})
|
||||
).toBe('Original long first prompt that scrolled out of preview')
|
||||
).toEqual({
|
||||
text: 'Original long first prompt that scrolled out of preview',
|
||||
source: 'first-user-prompt'
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to the earliest user preview turn when firstUserPrompt is absent', () => {
|
||||
expect(sessionFirstPrompt(baseSession)).toBe('Please fix the flaky golden tests')
|
||||
it('marks list-scan fallback text as a preview-window prompt', () => {
|
||||
expect(sessionPromptPreview(baseSession)).toEqual({
|
||||
text: 'Please fix the flaky golden tests',
|
||||
source: 'preview-window'
|
||||
})
|
||||
expect(
|
||||
sessionFirstPrompt({
|
||||
sessionPromptPreview({
|
||||
...baseSession,
|
||||
previewMessages: [{ role: 'assistant', text: 'Only agent text', timestamp: null }]
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
// Remote/SSH rows never re-parse on demand, so a truncated window would
|
||||
// otherwise show a RECENT ask permanently labelled as the first prompt.
|
||||
it('refuses the preview fallback once the sliding window has truncated', () => {
|
||||
|
||||
it('keeps a truncated sliding-window fallback available as a recent prompt', () => {
|
||||
expect(
|
||||
sessionFirstPrompt({
|
||||
sessionPromptPreview({
|
||||
...baseSession,
|
||||
previewMessagesTruncated: true,
|
||||
previewMessages: [
|
||||
|
|
@ -135,12 +140,15 @@ describe('ai vault session display', () => {
|
|||
{ role: 'assistant', text: 'Later reply', timestamp: null }
|
||||
]
|
||||
})
|
||||
).toBeNull()
|
||||
).toEqual({
|
||||
text: 'Later user turn still in the preview window',
|
||||
source: 'preview-window'
|
||||
})
|
||||
})
|
||||
|
||||
it('still prefers a stored firstUserPrompt when the window has truncated', () => {
|
||||
expect(
|
||||
sessionFirstPrompt({
|
||||
sessionPromptPreview({
|
||||
...baseSession,
|
||||
previewMessagesTruncated: true,
|
||||
firstUserPrompt: 'Original long first prompt that scrolled out of preview',
|
||||
|
|
@ -148,6 +156,9 @@ describe('ai vault session display', () => {
|
|||
{ role: 'user', text: 'Later user turn still in the preview window', timestamp: null }
|
||||
]
|
||||
})
|
||||
).toBe('Original long first prompt that scrolled out of preview')
|
||||
).toEqual({
|
||||
text: 'Original long first prompt that scrolled out of preview',
|
||||
source: 'first-user-prompt'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
// Why: the pure preview/search-text core now lives in /shared so mobile can
|
||||
// reuse it (Metro can't import renderer). Re-export for renderer import parity.
|
||||
export type { AiVaultSessionDisplayTurn } from '../../../../shared/ai-vault-session-display'
|
||||
export type {
|
||||
AiVaultSessionDisplayTurn,
|
||||
AiVaultSessionPromptPreview
|
||||
} from '../../../../shared/ai-vault-session-display'
|
||||
export {
|
||||
latestSessionConversationTurn,
|
||||
recentSessionConversationTurns,
|
||||
sessionDetailConversationTurns,
|
||||
sessionFirstPrompt,
|
||||
sessionModelLabel,
|
||||
sessionPromptPreview,
|
||||
sessionPreviewSearchText
|
||||
} from '../../../../shared/ai-vault-session-display'
|
||||
|
|
|
|||
|
|
@ -11337,9 +11337,14 @@
|
|||
"jumpToOriginalPane": "Jump to Original Pane",
|
||||
"worktree": "Worktree",
|
||||
"jumpToWorktree": "Jump to Worktree",
|
||||
"prompt": "Prompt",
|
||||
"firstPrompt": "First prompt",
|
||||
"recentPrompt": "Recent prompt",
|
||||
"firstPromptCopied": "First prompt copied",
|
||||
"recentPromptCopied": "Recent prompt copied",
|
||||
"copyFirstPrompt": "Copy first prompt",
|
||||
"copyRecentPrompt": "Copy recent prompt",
|
||||
"copyPrompt": "Copy prompt",
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
"noFirstPromptAvailable": "No first prompt available",
|
||||
|
|
|
|||
|
|
@ -11201,9 +11201,14 @@
|
|||
"emptyConversationDetail": "Esta sesión no tiene conversación guardada y no se puede reanudar.",
|
||||
"queuedMessages": "{{value0}} mensaje(s) en cola",
|
||||
"subagentTranscripts": "{{value0}} transcripción(es) de subagente",
|
||||
"prompt": "Prompt",
|
||||
"firstPrompt": "Primer prompt",
|
||||
"recentPrompt": "Prompt reciente",
|
||||
"firstPromptCopied": "Primer prompt copiado",
|
||||
"recentPromptCopied": "Prompt reciente copiado",
|
||||
"copyFirstPrompt": "Copiar primer prompt",
|
||||
"copyRecentPrompt": "Copiar prompt reciente",
|
||||
"copyPrompt": "Copiar prompt",
|
||||
"copied": "Copiado",
|
||||
"copy": "Copiar",
|
||||
"noFirstPromptAvailable": "No hay primer prompt disponible",
|
||||
|
|
|
|||
|
|
@ -11201,9 +11201,14 @@
|
|||
"emptyConversationDetail": "このセッションには保存された会話がなく、再開できません。",
|
||||
"queuedMessages": "キュー内のメッセージ {{value0}} 件",
|
||||
"subagentTranscripts": "サブエージェントの履歴 {{value0}} 件",
|
||||
"prompt": "プロンプト",
|
||||
"firstPrompt": "最初のプロンプト",
|
||||
"recentPrompt": "最近のプロンプト",
|
||||
"firstPromptCopied": "最初のプロンプトをコピーしました",
|
||||
"recentPromptCopied": "最近のプロンプトをコピーしました",
|
||||
"copyFirstPrompt": "最初のプロンプトをコピー",
|
||||
"copyRecentPrompt": "最近のプロンプトをコピー",
|
||||
"copyPrompt": "プロンプトをコピー",
|
||||
"copied": "コピーしました",
|
||||
"copy": "コピー",
|
||||
"noFirstPromptAvailable": "最初のプロンプトはありません",
|
||||
|
|
|
|||
|
|
@ -11201,9 +11201,14 @@
|
|||
"emptyConversationDetail": "이 세션에는 저장된 대화가 없어 재개할 수 없습니다.",
|
||||
"queuedMessages": "대기 중인 메시지 {{value0}}개",
|
||||
"subagentTranscripts": "서브에이전트 대화 기록 {{value0}}개",
|
||||
"prompt": "프롬프트",
|
||||
"firstPrompt": "첫 프롬프트",
|
||||
"recentPrompt": "최근 프롬프트",
|
||||
"firstPromptCopied": "첫 프롬프트를 복사했습니다",
|
||||
"recentPromptCopied": "최근 프롬프트를 복사했습니다",
|
||||
"copyFirstPrompt": "첫 프롬프트 복사",
|
||||
"copyRecentPrompt": "최근 프롬프트 복사",
|
||||
"copyPrompt": "프롬프트 복사",
|
||||
"copied": "복사됨",
|
||||
"copy": "복사",
|
||||
"noFirstPromptAvailable": "첫 프롬프트를 사용할 수 없습니다",
|
||||
|
|
|
|||
|
|
@ -11213,9 +11213,14 @@
|
|||
"emptyConversationDetail": "此会话没有已保存的对话,无法恢复。",
|
||||
"queuedMessages": "{{value0}} 条排队消息",
|
||||
"subagentTranscripts": "{{value0}} 个子智能体记录",
|
||||
"prompt": "提示",
|
||||
"firstPrompt": "首次提示",
|
||||
"recentPrompt": "最近提示",
|
||||
"firstPromptCopied": "已复制首次提示",
|
||||
"recentPromptCopied": "已复制最近提示",
|
||||
"copyFirstPrompt": "复制首次提示",
|
||||
"copyRecentPrompt": "复制最近提示",
|
||||
"copyPrompt": "复制提示",
|
||||
"copied": "已复制",
|
||||
"copy": "复制",
|
||||
"noFirstPromptAvailable": "没有可用的首次提示",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,11 @@ export type AiVaultSessionDisplayTurn = {
|
|||
timestamp: string | null
|
||||
}
|
||||
|
||||
export type AiVaultSessionPromptPreview = {
|
||||
text: string
|
||||
source: 'first-user-prompt' | 'preview-window'
|
||||
}
|
||||
|
||||
export function latestSessionConversationTurn(
|
||||
session: AiVaultSession
|
||||
): AiVaultSessionDisplayTurn | null {
|
||||
|
|
@ -44,22 +49,11 @@ export function sessionDetailConversationTurns(
|
|||
return dedupeAdjacentConversationTurns(turns).slice(-limit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder text for the first-prompt row while the full body loads on demand.
|
||||
* List scans no longer store firstUserPrompt (payload/perf); this is preview-only.
|
||||
*/
|
||||
export function sessionFirstPrompt(session: AiVaultSession): string | null {
|
||||
// Prefer a stored full body when present (on-demand re-parse / tests).
|
||||
/** Prompt text with enough provenance for the renderer to avoid overclaiming. */
|
||||
export function sessionPromptPreview(session: AiVaultSession): AiVaultSessionPromptPreview | null {
|
||||
const stored = session.firstUserPrompt?.trim()
|
||||
if (stored) {
|
||||
return stored
|
||||
}
|
||||
|
||||
// Why: `previewMessages` is a newest-N sliding window. Once it has truncated,
|
||||
// its earliest user turn is a RECENT ask, not the opening one — returning it
|
||||
// would show (and copy) the wrong message under a "first prompt" label.
|
||||
if (session.previewMessagesTruncated) {
|
||||
return null
|
||||
return { text: stored, source: 'first-user-prompt' }
|
||||
}
|
||||
|
||||
for (const message of session.previewMessages) {
|
||||
|
|
@ -68,7 +62,7 @@ export function sessionFirstPrompt(session: AiVaultSession): string | null {
|
|||
}
|
||||
const text = message.text.trim()
|
||||
if (text) {
|
||||
return text
|
||||
return { text, source: 'preview-window' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue