✨ 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

+1 -1
View File
@@ -2,7 +2,7 @@
"publisher": "clurdra",
"name": "superpowers-vscode-clurdra",
"displayName": "Superpowers-clurdra",
"version": "0.2.130",
"version": "0.2.131",
"packageManager": "pnpm@10.27.0",
"description": "Superpowers specs and plans Kanban explorer",
"author": "clurdra",
+4
View File
@@ -538,6 +538,10 @@ export class KanbanWebviewPanel {
void issues.handleUpdateBrainstormProfilePath(this, msg.issueNumber, msg.brainstormProfilePath)
return
}
if (msg.type === 'issue/clear-brainstorm-session') {
void issues.handleClearBrainstormSession(this, msg.issueNumber)
return
}
if (msg.type === 'issue/update-test-profile-path') {
void issues.handleUpdateTestProfilePath(this, msg.issueNumber, msg.testProfilePath)
return
+71
View File
@@ -1976,6 +1976,77 @@ export async function handleUpdateProfilePath(panel: KanbanWebviewPanel, issueNu
}
}
/**
* 清掉工单记的头脑风暴会话 id,下次点 ▶ 续聊就新开一个会话。
*
* 用处:旧会话的历史被 `claude --resume` 原样带回来(里面可能还留着几个版本
* 之前的提示词和它报的错),想从干净的上下文重新聊需求时用。会话文件本身不动,
* 手上有 id 仍可用 `claude --resume <id>` 找回。
*/
export async function handleClearBrainstormSession(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
let previousValue: string | undefined
try {
const existingState = await panel.readIssueState(issueNumber)
previousValue = typeof existingState.sessionId === 'string' ? existingState.sessionId : undefined
}
catch {
previousValue = undefined
}
if (!previousValue) {
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'info',
message: `工单 #${issueNumber} 本来就没有头脑风暴会话`,
dismissOnTimer: 4000,
})
return
}
const choice = await window.showWarningMessage(
`确定清除工单 #${issueNumber} 的头脑风暴会话 id(${previousValue})?下次续聊会新开一个会话,旧会话记录还在,可用 claude --resume 找回。`,
{ modal: true },
'清除',
)
if (choice !== '清除')
return
try {
await panel.mergeIssueState(issueNumber, { sessionId: '' })
}
catch (err) {
const message = err instanceof Error ? err.message : String(err)
logger.add({
level: 'error',
source: 'panel',
message: `清除 sessionId 失败 (issue #${issueNumber})`,
details: message,
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'error',
message: `清除工单 #${issueNumber} 头脑风暴会话失败: ${message}`,
dismissOnTimer: 6000,
})
return
}
panel.postMessage({
type: 'issue/patch',
issueNumber,
patch: { sessionId: null },
})
panel.postMessage({
type: 'toast/show',
id: makeNonce(),
level: 'success',
message: `工单 #${issueNumber} 头脑风暴会话已清除,下次续聊将新开会话`,
dismissOnTimer: 5000,
})
}
/**
* Persist a per-issue `brainstormProfilePath` override into the issue's state
* JSON. Brainstorm start/resume use this profile (see resolveBrainstormProfilePath);
+1
View File
@@ -213,6 +213,7 @@ export type WebviewToExtension
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
| { type: 'issue/clear-brainstorm-session', issueNumber: number }
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
| { type: 'logs/fetch' }
| { type: 'logs/clear' }
+1
View File
@@ -26,6 +26,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
'issue/update-auto-review',
'issue/update-profile-path',
'issue/update-brainstorm-profile-path',
'issue/clear-brainstorm-session',
'issue/update-test-profile-path',
'dependency/set',
'dependency/clear',
+2
View File
@@ -56,6 +56,7 @@ export function App() {
closeIssue,
closeSessionTab,
continueBrainstorm,
clearBrainstormSession,
changeColumn,
setDependency,
clearDependency,
@@ -388,6 +389,7 @@ export function App() {
onResetToTodo={n => changeColumn(n, 'todo', selectedIssue?.source, selectedIssue?.externalId)}
onCloseSessionTab={closeSessionTab}
onContinueBrainstorm={continueBrainstorm}
onClearBrainstormSession={clearBrainstormSession}
onUpdateAutoReview={updateIssueAutoReview}
onUpdateProfilePath={updateIssueProfilePath}
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
@@ -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>,
+9 -1
View File
@@ -230,6 +230,8 @@ export interface UseIssuesResult {
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
*/
updateIssueBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
/** 清掉工单记的头脑风暴会话 id,下次续聊新开会话。 */
clearBrainstormSession: (issueNumber: number) => void
/**
* 覆盖该工单测试会话专用的 Claude 配置文件,乐观更新本地 state 后
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
@@ -652,6 +654,12 @@ export function useIssues(): UseIssuesResult {
postMessage({ type: 'issue/update-brainstorm-profile-path', issueNumber, brainstormProfilePath })
}, [])
// 清除头脑风暴会话 id:扩展侧先弹确认框,确认后才落库并推 issue/patch,
// 所以这里不做乐观更新。
const clearBrainstormSession = useCallback((issueNumber: number): void => {
postMessage({ type: 'issue/clear-brainstorm-session', issueNumber })
}, [])
const updateIssueTestProfilePath = useCallback((issueNumber: number, testProfilePath: string): void => {
// Optimistically update local state so the dropdown reflects immediately
// without remounting the issues list / detail panel via a full reload.
@@ -930,5 +938,5 @@ export function useIssues(): UseIssuesResult {
return cleanup
}, [clearPrDiffSummaryRunning, markHandoffRunning])
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
}
+1
View File
@@ -244,6 +244,7 @@ export type WebviewToExtension
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
| { type: 'issue/clear-brainstorm-session', issueNumber: number }
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
| { type: 'logs/fetch' }
| { type: 'logs/clear' }