From 4597244bc71ee44f94fd8f4bbdafadf336f1af73 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 08:45:45 +0000 Subject: [PATCH 1/2] Open a conversation by clicking its row, and select from the preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking anywhere on a session row now opens its conversation; the eye button stays as an explicit entry point. Selection remains on the checkbox alone (with a wider hit area), so browsing a deletion list never adds to what gets deleted. The preview gains a "Select this conversation" checkbox bound to the list's selection, plus previous / next through the filtered list (buttons and ← / →). Escape in the preview no longer also sends the app back to the overview. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T3VnHzsih7Qq4QPYwvLtBk --- src/App.css | 11 ++++++ src/views/SessionsView.tsx | 79 ++++++++++++++++++++++++++++++-------- 2 files changed, 74 insertions(+), 16 deletions(-) diff --git a/src/App.css b/src/App.css index 63f4c59..6043a6e 100644 --- a/src/App.css +++ b/src/App.css @@ -1132,6 +1132,17 @@ label.entry-label { cursor: pointer; } } .worktree-session-option:has(input:disabled) { cursor: default; opacity: 0.65; } .row-actions { display: flex; justify-content: flex-end; } +.session-row.clickable { cursor: pointer; } +.session-row.clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; } +.session-row.selected { background: color-mix(in srgb, var(--accent) 7%, transparent); } +.session-row.active { box-shadow: inset 3px 0 0 var(--accent); } +/* The checkbox cell is padded out so selecting does not demand a 15px-precise click. */ +.row-check { display: flex; align-items: center; align-self: stretch; margin: -10px -8px -10px -12px; padding: 0 8px 0 12px; cursor: default; } +.transcript-actions { align-items: center; } +.transcript-select { display: flex; align-items: center; gap: 7px; margin-right: auto; font-size: 12.5px; cursor: pointer; } +.transcript-select.selected { color: var(--accent); font-weight: 600; } +.transcript-nav { display: flex; align-items: center; gap: 2px; } +.transcript-position { min-width: 52px; text-align: center; color: var(--text-tertiary); font-size: 11.5px; font-variant-numeric: tabular-nums; } /* --- conversation preview --- */ .transcript-dialog { width: min(720px, calc(100vw - 48px)); height: min(760px, calc(100vh - 48px)); display: flex; flex-direction: column; overflow: hidden; } diff --git a/src/views/SessionsView.tsx b/src/views/SessionsView.tsx index 7e7fb7b..c25769b 100644 --- a/src/views/SessionsView.tsx +++ b/src/views/SessionsView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useState, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent } from 'react' import { type ScanSnapshot, type SessionItem, @@ -178,7 +178,8 @@ export default function SessionsView({ snapshot, cleaning, actionsDisabled, clea
    - {visible.map((session) => selection.toggle(session)} onPreview={() => setPreviewing(session)} locale={locale} />)} + {visible.map((session) => selection.toggle(session)} onPreview={() => setPreviewing(session)} locale={locale} />)}
{!visible.length &&

{listable.length ? t('没有符合筛选条件的会话', 'No sessions match these filters') @@ -191,14 +192,37 @@ export default function SessionsView({ snapshot, cleaning, actionsDisabled, clea cleaning={cleaning} actionsDisabled={actionsDisabled} progress={cleanProgress} onDelete={() => onCleanup({ kind: 'sessions-delete', ids: selectedSessions.map(sessionID) })} /> - {previewing && setPreviewing(null)} />} + {previewing && setPreviewing(null)} + checked={selection.isSelected(previewing)} onToggle={() => selection.toggle(previewing)} + position={visible.indexOf(previewing)} count={visible.length} + onNavigate={(step) => { + const next = visible[visible.indexOf(previewing) + step] + if (next) setPreviewing(next) + }} />} } -function SessionRow({ session, checked, locale, onToggle, onPreview }: { session: SessionItem; checked: boolean; locale: string; onToggle: () => void; onPreview: () => void }) { +/** Clicks and keys that land on the row's own controls must not also open the preview. */ +const fromControl = (event: ReactMouseEvent | ReactKeyboardEvent): boolean => + event.target instanceof Element && event.target.closest('input, button, label') !== null + +function SessionRow({ session, checked, active, locale, onToggle, onPreview }: { + session: SessionItem; checked: boolean; active: boolean; locale: string; onToggle: () => void; onPreview: () => void +}) { const { t, m } = usePreferences() - return

  • - + // Clicking a row opens its conversation; only the checkbox selects, so browsing a + // deletion list can never quietly add a conversation to what gets deleted. + return
  • { if (!fromControl(event)) onPreview() }} + onKeyDown={(event) => { + if (fromControl(event) || (event.key !== 'Enter' && event.key !== ' ')) return + event.preventDefault() + onPreview() + }}> +
    {sessionDisplayName(session)} {(session.isPinned || session.tags.length > 0) && @@ -212,14 +236,19 @@ function SessionRow({ session, checked, locale, onToggle, onPreview }: { session {formatBytes(session.fileBytes)} {formatBytes(sessionTotalBytes(session))} - +
  • } /** Read-only look at a conversation's messages, so it can be recognised before it is deleted. */ -function TranscriptDialog({ session, locale, onClose }: { session: SessionItem; locale: string; onClose: () => void }) { +function TranscriptDialog({ session, locale, onClose, checked, onToggle, position, count, onNavigate }: { + session: SessionItem; locale: string; onClose: () => void + checked: boolean; onToggle: () => void + /** Index within the filtered list, or -1 once filters hide the conversation. */ + position: number; count: number; onNavigate: (step: -1 | 1) => void +}) { const { t, e } = usePreferences() const [transcript, setTranscript] = useState(null) const [error, setError] = useState(null) @@ -227,6 +256,8 @@ function TranscriptDialog({ session, locale, onClose }: { session: SessionItem; useEffect(() => { let cancelled = false + setTranscript(null) + setError(null) window.cleanmycodex.sessionTranscript(session.id) .then((result) => { if (!cancelled) setTranscript(result) }) .catch((err) => { if (!cancelled) setError(err instanceof Error ? err.message : String(err)) }) @@ -235,13 +266,20 @@ function TranscriptDialog({ session, locale, onClose }: { session: SessionItem; useEffect(() => { const onKey = (event: KeyboardEvent): void => { - if (event.key !== 'Escape') return - if (zoomed) setZoomed(null) - else onClose() + if (event.key === 'Escape') { + // Captured first and stopped, so the app's own Escape (back to the overview) waits for the next press. + event.stopImmediatePropagation() + if (zoomed) setZoomed(null) + else onClose() + } else if (!zoomed && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) { + if (event.target instanceof HTMLInputElement && event.target.type !== 'checkbox') return + event.preventDefault() + onNavigate(event.key === 'ArrowLeft' ? -1 : 1) + } } - window.addEventListener('keydown', onKey) - return () => window.removeEventListener('keydown', onKey) - }, [onClose, zoomed]) + window.addEventListener('keydown', onKey, true) + return () => window.removeEventListener('keydown', onKey, true) + }, [onClose, onNavigate, zoomed]) const notes: string[] = [] if (transcript?.toolCalls) notes.push(t(`另有 ${transcript.toolCalls} 次工具调用未显示`, `${transcript.toolCalls} tool calls not shown`)) @@ -253,7 +291,7 @@ function TranscriptDialog({ session, locale, onClose }: { session: SessionItem;

    {sessionDisplayName(session)}

    -

    {[sessionProjectName(session), new Date(session.modifiedAt).toLocaleString(locale)].filter(Boolean).join(' · ')}

    +

    {[sessionProjectName(session), new Date(session.modifiedAt).toLocaleString(locale), formatBytes(sessionTotalBytes(session))].filter(Boolean).join(' · ')}

    {error &&

    {e(error)}

    } @@ -283,7 +321,16 @@ function TranscriptDialog({ session, locale, onClose }: { session: SessionItem;
    } {notes.length > 0 &&

    {notes.join(' · ')}

    } -
    +
    + + + + {position >= 0 && {position + 1} / {count}} + +
    From a45c1064e5c309b3393483374eb699d762b8a7af Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 23 Sep 2026 08:50:27 +0000 Subject: [PATCH 2/2] Close the conversation preview from a corner button The footer's Close and Show in File Manager buttons are replaced by a single close button in the dialog's top-right corner; the row keeps its own file-manager button. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T3VnHzsih7Qq4QPYwvLtBk --- src/App.css | 3 +++ src/icons.tsx | 6 ++++++ src/views/SessionsView.tsx | 5 ++--- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/App.css b/src/App.css index 6043a6e..b667650 100644 --- a/src/App.css +++ b/src/App.css @@ -1146,7 +1146,10 @@ label.entry-label { cursor: pointer; } /* --- conversation preview --- */ .transcript-dialog { width: min(720px, calc(100vw - 48px)); height: min(760px, calc(100vh - 48px)); display: flex; flex-direction: column; overflow: hidden; } +.transcript-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: start; column-gap: 12px; } .transcript-header h2 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.transcript-header .dialog-lead { grid-column: 1 / -1; } +.transcript-close { margin: -6px -8px 0 0; } .transcript-body { flex: 1; min-height: 0; margin-top: 14px; overflow: auto; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface-sunken); } .transcript-list { list-style: none; margin: 0; padding: 6px 14px; } .transcript-message { padding: 10px 0; border-top: 1px solid var(--line-soft); } diff --git a/src/icons.tsx b/src/icons.tsx index fc7f51d..379bba1 100644 --- a/src/icons.tsx +++ b/src/icons.tsx @@ -14,6 +14,12 @@ export function PreviewIcon() { } +export function CloseIcon() { + return +} + export function BackIcon() { return

    {sessionDisplayName(session)}

    +

    {[sessionProjectName(session), new Date(session.modifiedAt).toLocaleString(locale), formatBytes(sessionTotalBytes(session))].filter(Boolean).join(' · ')}

    @@ -331,8 +332,6 @@ function TranscriptDialog({ session, locale, onClose, checked, onToggle, positio {position >= 0 && {position + 1} / {count}} - -
    {zoomed &&
    setZoomed(null)}>
    }