✨ feat(vscode): 头脑风暴会话可清除,悬停提示改窄改紧凑

工单记着的头脑风暴会话 id 此前没有清除入口,续聊只能 resume 旧会话,
把几个版本前的提示词和它当时报的错一起带回来。会话 id 行新增清除按钮,
确认后把 state JSON 的 sessionId 置空,下次续聊新开会话;会话文件不动,
手上有 id 仍可 claude --resume 找回。

悬停气泡此前贴着视口右边测量尺寸:fixed 元素的可用宽度是「视口宽 - left」,
贴边测会被挤窄、行数变多,按那个高度定位就浮高一截。改成先在左上角量再落位,
字号行高收紧,提示文案压成一行。顺带修掉「重置为待办」的错误说明——它会停
会话、删 worktree 和未合并 PR,不是原样保留。

Claude-Session: https://claude.ai/code/session_01REdhpbaNyqWASF2HWsnTFv
This commit is contained in:
cruldra committed 2026-09-13 09:21:53 +08:00
1 parent 9fadcac12b
commit 5bfd759822
11 files changed
+124 -23

No files matched your search

@@ -45,6 +45,7 @@ interface BottomTabsProps {
onResetToTodo: (issueNumber: number) => void
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
onContinueBrainstorm: (issueNumber: number) => void
onClearBrainstormSession: (issueNumber: number) => void
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
@@ -142,6 +143,7 @@ export function BottomTabs(props: BottomTabsProps) {
onResetToTodo={props.onResetToTodo}
onCloseSessionTab={props.onCloseSessionTab}
onContinueBrainstorm={props.onContinueBrainstorm}
onClearBrainstormSession={props.onClearBrainstormSession}
onUpdateAutoReview={props.onUpdateAutoReview}
onUpdateProfilePath={props.onUpdateProfilePath}
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
@@ -1,7 +1,7 @@
import type { ClaudeProfile } from '../hooks/useIssues'
import type { Issue, IssueColumn } from '../types'
import type { PropertyGroup, RowAction } from './property-grid'
import { ArrowRightLeft, CircleSlash, Download, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
import { ArrowRightLeft, CircleSlash, Download, Eraser, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
import { useMemo, useRef } from 'react'
import { isIssueLocked } from '../lib/dependencies'
import { isIssueMine, issueOwnerLogin } from '../lib/issueOwnership'
@@ -93,6 +93,7 @@ interface IssueDetailPanelProps {
* 打进去,关了就 `claude --resume <id>` 带着提示词恢复,没会话就新开一个。
*/
onContinueBrainstorm: (issueNumber: number) => void
onClearBrainstormSession: (issueNumber: number) => void
/** Persist a per-issue `autoReview` override into the state JSON. */
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
@@ -144,6 +145,7 @@ export function IssueDetailPanel({
onResetToTodo,
onCloseSessionTab,
onContinueBrainstorm,
onClearBrainstormSession,
onUpdateAutoReview,
profiles,
onUpdateProfilePath,
@@ -236,6 +238,16 @@ export function IssueDetailPanel({
},
})
}
if (hasSession) {
secondaryActions.push({
icon: <Eraser className="size-3.5" />,
title: '清除会话:下次续聊新开一个,不再带旧会话的历史',
onClick: () => {
if (issue)
onClearBrainstormSession(issue.number)
},
})
}
return {
key: 'sessionId',
label: '头脑风暴会话id',
@@ -523,7 +535,7 @@ export function IssueDetailPanel({
],
},
],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
)
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
@@ -617,7 +629,7 @@ export function IssueDetailPanel({
</span>
)
: null}
<Tooltip label={issue.source === 'youtrack' ? '在浏览器里打开这张 YouTrack 卡' : '在浏览器里打开这个 Gitea 工单'}>
<Tooltip label="在浏览器里打开">
<button
type="button"
onClick={() => onOpenInBrowser(issue.htmlUrl)}
@@ -632,7 +644,7 @@ export function IssueDetailPanel({
{issue.source !== 'youtrack' && !readOnly && (
<>
{issue.handoffAttachmentId && me !== undefined && (issue.assignees ?? []).includes(me) && (
<Tooltip label={`接管 ${issue.handoffFrom ?? '?'} 移交过来的工单:拉回会话记录和 worktree`}>
<Tooltip label={`接管 ${issue.handoffFrom ?? '?'} 的移交`}>
<button
type="button"
disabled={isHandoffRunning(issue.number)}
@@ -646,7 +658,7 @@ export function IssueDetailPanel({
</Tooltip>
)}
{!issue.handoffAttachmentId && issue.column !== 'done' && ((issue.assignees ?? []).length === 0 || (me !== undefined && (issue.assignees ?? []).includes(me))) && (
<Tooltip label="移交给同事:把会话记录打包传过去,对方接管后继续做">
<Tooltip label="移交给同事">
<button
type="button"
disabled={isHandoffRunning(issue.number)}
@@ -659,14 +671,14 @@ export function IssueDetailPanel({
</Tooltip>
)}
{issue.handoffAttachmentId && !(me !== undefined && (issue.assignees ?? []).includes(me)) && (
<Tooltip label={`${issue.handoffFrom ?? '?'} 已移交,等待 ${(issue.assignees ?? []).join(', ') || '?'} 接管`}>
<Tooltip label={`${issue.handoffFrom ?? '?'} 已移交,等 ${(issue.assignees ?? []).join(', ') || '?'} 接管`}>
<span className="inline-flex shrink-0 items-center rounded border border-[var(--vscode-panel-border)] px-1.5 py-0.5 text-[10px] opacity-70">
待接管
</span>
</Tooltip>
)}
{(issue.column === 'in-progress' || issue.column === 'review') && (
<Tooltip label="打回待办:卡片挪回待办列,会话和 worktree 不动">
<Tooltip label="重置为待办(停会话、删 worktree 和未合并 PR)">
<button
type="button"
onClick={() => onResetToTodo(issue.number)}
@@ -677,7 +689,7 @@ export function IssueDetailPanel({
</button>
</Tooltip>
)}
<Tooltip label="关闭工单:Gitea 上置为 closed,卡片从看板消失,可在 Gitea 里重开">
<Tooltip label="关闭工单(Gitea 里可重开)">
<button
type="button"
onClick={() => onCloseIssue(issue.number)}
@@ -687,7 +699,7 @@ export function IssueDetailPanel({
<CircleSlash className="size-3.5" />
</button>
</Tooltip>
<Tooltip label="删除工单:从 Gitea 彻底删掉,不可撤销">
<Tooltip label="删除工单(不可撤销)">
<button
type="button"
onClick={() => issue && onDeleteIssue?.(issue.number)}
+11 -12
View File
@@ -25,14 +25,14 @@ interface TooltipProps {
* 上方放不下就翻到下方,左右钳在视口内。
*/
export function Tooltip({ label, children, className }: TooltipProps): ReactElement {
const [pos, setPos] = useState<{ top: number, left: number } | null>(null)
const [visible, setVisible] = useState(false)
const wrapRef = useRef<HTMLSpanElement | null>(null)
const bubbleRef = useRef<HTMLDivElement | null>(null)
const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const hide = useCallback((): void => {
clearTimeout(timerRef.current)
setPos(null)
setVisible(false)
}, [])
const show = useCallback((): void => {
@@ -40,18 +40,17 @@ export function Tooltip({ label, children, className }: TooltipProps): ReactElem
return
clearTimeout(timerRef.current)
timerRef.current = setTimeout(() => {
const rect = wrapRef.current?.getBoundingClientRect()
if (rect)
setPos({ top: rect.top, left: rect.left + rect.width / 2 })
if (wrapRef.current)
setVisible(true)
}, SHOW_DELAY_MS)
}, [label])
useEffect(() => () => clearTimeout(timerRef.current), [])
// 气泡渲染后才知道自身尺寸,这时才能定最终位置:默认在上方,
// 上方空间不够就翻到触发元素下方;水平居中后钳进视口。
// 气泡先在左上角量一次尺寸再落位。必须量在 left:0:fixed 元素的可用宽度是
// 「视口宽 - left」,贴右边量会被挤窄、行数变多,按那个高度定位就会浮高一截。
useEffect(() => {
if (!pos)
if (!visible)
return
const bubble = bubbleRef.current
const rect = wrapRef.current?.getBoundingClientRect()
@@ -65,7 +64,7 @@ export function Tooltip({ label, children, className }: TooltipProps): ReactElem
bubble.style.top = `${top}px`
bubble.style.left = `${left}px`
bubble.style.visibility = 'visible'
}, [pos])
}, [visible])
return (
<>
@@ -80,12 +79,12 @@ export function Tooltip({ label, children, className }: TooltipProps): ReactElem
>
{children}
</span>
{pos && label && createPortal(
{visible && label && createPortal(
<div
ref={bubbleRef}
role="tooltip"
style={{ top: pos.top, left: pos.left, visibility: 'hidden' }}
className="pointer-events-none fixed z-50 max-w-64 rounded border border-[var(--vscode-editorHoverWidget-border,var(--vscode-panel-border))] bg-[var(--vscode-editorHoverWidget-background,var(--vscode-editorWidget-background))] px-2 py-1 text-xs leading-snug text-[var(--vscode-editorHoverWidget-foreground,var(--vscode-foreground))] shadow-md"
style={{ top: 0, left: 0, visibility: 'hidden' }}
className="pointer-events-none fixed z-50 max-w-[22rem] rounded border border-[var(--vscode-editorHoverWidget-border,var(--vscode-panel-border))] bg-[var(--vscode-editorHoverWidget-background,var(--vscode-editorWidget-background))] px-2 py-1 text-[11px] leading-tight break-words text-[var(--vscode-editorHoverWidget-foreground,var(--vscode-foreground))] shadow-md"
>
{label}
</div>,