✨ feat(vscode): 头脑风暴会话一键续聊,终端开着直接打进去、关了带提示词恢复
详情面板「头脑风暴会话id」行的 ▶ 改为续聊:规划终端还开着就把续聊提示词 打进去;终端关了但有会话 id 就 `claude --resume <id>` 带提示词恢复;没会话 才新开。属性行右侧按钮改为数组,续聊与关闭 tab 可并存。 Claude-Session: https://claude.ai/code/session_01KDcq9HcrVLs2c9tkRxasub
This commit is contained in:
1 parent
90600cf070
commit
ae875756a9
15 files changed
+307
-297
No files matched your search
@@ -1,7 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ClaudeProfile } from '../hooks/useIssues'
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
import type { PropertyGroup } from './property-grid'
|
||||
import type { PropertyGroup, RowAction } from './property-grid'
|
||||
import { ArrowRightLeft, CircleSlash, Download, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { isIssueLocked } from '../lib/dependencies'
|
||||
@@ -87,11 +86,10 @@ interface IssueDetailPanelProps {
|
||||
*/
|
||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
|
||||
/**
|
||||
* Spawn a fresh "规划" cc tab for an issue whose sessionId is still empty
|
||||
* (created out-of-band via spx without webhook linking). Bound to the Play
|
||||
* button next to the "头脑风暴会话 id" row.
|
||||
* 「▶ 续聊」:由插件把续聊提示词送进头脑风暴会话——规划 tab 开着就直接
|
||||
* 打进去,关了就 `claude --resume <id>` 带着提示词恢复,没会话就新开一个。
|
||||
*/
|
||||
onStartBrainstormSession: (issueNumber: number) => void
|
||||
onContinueBrainstorm: (issueNumber: number) => void
|
||||
/** Persist a per-issue `autoReview` override into the state JSON. */
|
||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
|
||||
@@ -140,7 +138,7 @@ export function IssueDetailPanel({
|
||||
onCloseIssue,
|
||||
onResetToTodo,
|
||||
onCloseSessionTab,
|
||||
onStartBrainstormSession,
|
||||
onContinueBrainstorm,
|
||||
onUpdateAutoReview,
|
||||
profiles,
|
||||
onUpdateProfilePath,
|
||||
@@ -206,39 +204,39 @@ export function IssueDetailPanel({
|
||||
})),
|
||||
},
|
||||
(() => {
|
||||
// 头脑风暴会话 id 行有三种状态:
|
||||
// 1. sessionId 已存在 + brainstormTabOpen=true → 显示 id + X 关闭 tab
|
||||
// 2. sessionId 已存在 + brainstormTabOpen=false → 显示 id(无右侧按钮)
|
||||
// 3. sessionId 为空 + column !== 'done' → 显示 Play 按钮(启动一个新规划 cc tab)
|
||||
// 4. sessionId 为空 + column === 'done' → 占位 —,不显示按钮
|
||||
// 头脑风暴会话 id 行:
|
||||
// - 工单未完成 → 一直有 ▶ 续聊。插件把续聊提示词送进对应会话:
|
||||
// 规划终端开着就直接打进去,关了就 --resume 带提示词,没会话就新开。
|
||||
// - brainstormTabOpen → 再加一个 X 关闭 tab
|
||||
const hasSession = !!(issue?.sessionId && issue.sessionId.length > 0)
|
||||
const canStart = !hasSession && issue?.column !== 'done'
|
||||
let secondaryActionIcon: ReactNode | undefined
|
||||
let secondaryActionTitle: string | undefined
|
||||
let onSecondaryAction: (() => void) | undefined
|
||||
if (hasSession && issue?.brainstormTabOpen) {
|
||||
secondaryActionIcon = <X className="size-3.5" />
|
||||
secondaryActionTitle = '关闭头脑风暴 tab'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'brainstorm')
|
||||
}
|
||||
const secondaryActions: RowAction[] = []
|
||||
if (issue?.column !== 'done') {
|
||||
secondaryActions.push({
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: '续聊:把续聊提示词发给头脑风暴会话',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onContinueBrainstorm(issue.number)
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (canStart) {
|
||||
secondaryActionIcon = <Play className="size-3.5" />
|
||||
secondaryActionTitle = '启动头脑风暴会话'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onStartBrainstormSession(issue.number)
|
||||
}
|
||||
if (hasSession && issue?.brainstormTabOpen) {
|
||||
secondaryActions.push({
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭头脑风暴 tab',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'brainstorm')
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
key: 'sessionId',
|
||||
label: '头脑风暴会话id',
|
||||
type: 'action' as const,
|
||||
description: hasSession
|
||||
? '双击在新终端运行 claude --resume <id> 恢复对话'
|
||||
: '工单尚未关联 cc 会话;点击右侧 ▶ 启动一个新的规划 cc tab',
|
||||
? '双击在新终端运行 claude --resume <id> 恢复对话;点 ▶ 续聊'
|
||||
: '工单尚未关联 cc 会话;点 ▶ 新开一个规划会话并续聊',
|
||||
actionIcon: <Terminal className="size-3.5" />,
|
||||
onAction: (v: unknown) => {
|
||||
if (typeof v !== 'string' || v.length === 0)
|
||||
@@ -247,9 +245,7 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeSession(v, issue?.brainstormProfilePath, undefined, issue?.number)
|
||||
},
|
||||
secondaryActionIcon,
|
||||
secondaryActionTitle,
|
||||
onSecondaryAction,
|
||||
secondaryActions,
|
||||
}
|
||||
})(),
|
||||
// 实施会话即使 worktree 已被清掉也保留可双击的 resume 入口:
|
||||
@@ -271,14 +267,13 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeSession(v, issue?.profilePath, issue?.worktreePath, issue?.number)
|
||||
},
|
||||
secondaryActionIcon: issue?.implementTabOpen
|
||||
? <X className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: '关闭实施 tab',
|
||||
onSecondaryAction: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'implement')
|
||||
},
|
||||
secondaryActions: issue?.implementTabOpen
|
||||
? [{
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭实施 tab',
|
||||
onClick: () => onCloseSessionTab(issue.number, 'implement'),
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
issue?.reviewSessionFileExists
|
||||
? {
|
||||
@@ -294,14 +289,13 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeReviewSession(v, issue.number, issue?.worktreePath)
|
||||
},
|
||||
secondaryActionIcon: issue?.reviewTabOpen
|
||||
? <X className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: '关闭审查 tab',
|
||||
onSecondaryAction: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'review')
|
||||
},
|
||||
secondaryActions: issue.reviewTabOpen
|
||||
? [{
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭审查 tab',
|
||||
onClick: () => onCloseSessionTab(issue.number, 'review'),
|
||||
}]
|
||||
: [],
|
||||
}
|
||||
: {
|
||||
key: 'reviewSessionId',
|
||||
@@ -318,24 +312,26 @@ export function IssueDetailPanel({
|
||||
// (prompt 依赖已合并的 PR 号,没有 PR 不显示启动按钮)
|
||||
const hasSession = !!(issue?.testSessionId && issue.testSessionId.length > 0)
|
||||
const canStart = !hasSession && !!issue?.pr
|
||||
let secondaryActionIcon: ReactNode | undefined
|
||||
let secondaryActionTitle: string | undefined
|
||||
let onSecondaryAction: (() => void) | undefined
|
||||
const secondaryActions: RowAction[] = []
|
||||
if (hasSession && issue?.testTabOpen) {
|
||||
secondaryActionIcon = <X className="size-3.5" />
|
||||
secondaryActionTitle = '关闭测试 tab'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'test')
|
||||
}
|
||||
secondaryActions.push({
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭测试 tab',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'test')
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (canStart) {
|
||||
secondaryActionIcon = <Play className="size-3.5" />
|
||||
secondaryActionTitle = '启动测试会话'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onStartTestSession(issue.number)
|
||||
}
|
||||
secondaryActions.push({
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: '启动测试会话',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onStartTestSession(issue.number)
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
key: 'testSessionId',
|
||||
@@ -352,9 +348,7 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeTestSession(v, issue.number)
|
||||
},
|
||||
secondaryActionIcon,
|
||||
secondaryActionTitle,
|
||||
onSecondaryAction,
|
||||
secondaryActions,
|
||||
}
|
||||
})(),
|
||||
{
|
||||
@@ -427,26 +421,27 @@ export function IssueDetailPanel({
|
||||
description: '双击文件名在编辑器打开;点击实施按钮启动实施流程;cc 通过 issue body 的 <!-- spx:plan=路径 --> 注释实时同步',
|
||||
onOpen: (p: string) => onOpenFile(p),
|
||||
// Hide the "实施" button entirely when there's no plan file.
|
||||
secondaryActionIcon: issue?.planFile
|
||||
? <Play className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: locked ? lockTitle : '实施此计划',
|
||||
// Disable while running or already done. The user can still click
|
||||
// when status === 'failed' or never been run. Re-clicking while
|
||||
// 'running' is blocked at the UI to avoid registering duplicate
|
||||
// webhooks. To retry after a perceived failure with status still
|
||||
// 'running', edit the state-JSON comment manually for now.
|
||||
// 锁定态(前置工单未完成)同样禁止启动实施。
|
||||
secondaryDisabled:
|
||||
!issue?.planFile
|
||||
|| issue?.implementStatus === 'running'
|
||||
|| issue?.implementStatus === 'done'
|
||||
|| locked,
|
||||
onSecondaryAction: () => {
|
||||
if (!issue?.planFile)
|
||||
return
|
||||
onImplement(issue.number, issue.planFile, issue.profilePath, issue.sessionId)
|
||||
},
|
||||
secondaryActions: issue?.planFile
|
||||
? [{
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: locked ? lockTitle : '实施此计划',
|
||||
// Disable while running or already done. The user can still click
|
||||
// when status === 'failed' or never been run. Re-clicking while
|
||||
// 'running' is blocked at the UI to avoid registering duplicate
|
||||
// webhooks. To retry after a perceived failure with status still
|
||||
// 'running', edit the state-JSON comment manually for now.
|
||||
// 锁定态(前置工单未完成)同样禁止启动实施。
|
||||
disabled:
|
||||
issue.implementStatus === 'running'
|
||||
|| issue.implementStatus === 'done'
|
||||
|| locked,
|
||||
onClick: () => {
|
||||
if (!issue?.planFile)
|
||||
return
|
||||
onImplement(issue.number, issue.planFile, issue.profilePath, issue.sessionId)
|
||||
},
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
key: 'pr',
|
||||
@@ -461,17 +456,19 @@ export function IssueDetailPanel({
|
||||
if (pr.length > 0)
|
||||
onOpenPr(pr)
|
||||
},
|
||||
secondaryActionIcon: issue?.pr && !issue?.prDiffFile
|
||||
? <Play className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: prDiffSummaryRunning
|
||||
? 'PR 变更摘要生成中'
|
||||
: '生成 PR 变更摘要',
|
||||
secondaryDisabled: prDiffSummaryRunning,
|
||||
onSecondaryAction: () => {
|
||||
if (issue?.pr && !issue.prDiffFile && !prDiffSummaryRunning)
|
||||
onGeneratePrDiffSummary(issue.number)
|
||||
},
|
||||
secondaryActions: issue?.pr && !issue?.prDiffFile
|
||||
? [{
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: prDiffSummaryRunning
|
||||
? 'PR 变更摘要生成中'
|
||||
: '生成 PR 变更摘要',
|
||||
disabled: prDiffSummaryRunning,
|
||||
onClick: () => {
|
||||
if (issue?.pr && !issue.prDiffFile && !prDiffSummaryRunning)
|
||||
onGeneratePrDiffSummary(issue.number)
|
||||
},
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
key: 'prDiffFile',
|
||||
@@ -486,12 +483,16 @@ export function IssueDetailPanel({
|
||||
type: 'string',
|
||||
readOnly: true,
|
||||
description: '实施流程创建的分支名',
|
||||
secondaryActionIcon: issue?.branch ? <GitMerge className="size-3.5" /> : undefined,
|
||||
secondaryActionTitle: '本地预合并 (git merge --no-commit --no-ff)',
|
||||
onSecondaryAction: () => {
|
||||
if (issue?.branch)
|
||||
onMergeBranch(issue.number, issue.branch)
|
||||
},
|
||||
secondaryActions: issue?.branch
|
||||
? [{
|
||||
icon: <GitMerge className="size-3.5" />,
|
||||
title: '本地预合并 (git merge --no-commit --no-ff)',
|
||||
onClick: () => {
|
||||
if (issue?.branch)
|
||||
onMergeBranch(issue.number, issue.branch)
|
||||
},
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
issue?.worktreeExists
|
||||
? {
|
||||
@@ -500,9 +501,11 @@ export function IssueDetailPanel({
|
||||
type: 'file-link',
|
||||
description: '双击文件名在新 VS Code 窗口打开;点击垃圾桶删除 worktree',
|
||||
onOpen: (p: string) => onOpenWorktree(p),
|
||||
onSecondaryAction: () => issue && onDeleteWorktree(issue.number, issue.worktreePath ?? ''),
|
||||
secondaryActionIcon: <Trash2 className="size-3.5" />,
|
||||
secondaryActionTitle: '删除 worktree',
|
||||
secondaryActions: [{
|
||||
icon: <Trash2 className="size-3.5" />,
|
||||
title: '删除 worktree',
|
||||
onClick: () => onDeleteWorktree(issue.number, issue.worktreePath ?? ''),
|
||||
}],
|
||||
}
|
||||
: {
|
||||
key: 'worktreePath',
|
||||
@@ -514,7 +517,7 @@ export function IssueDetailPanel({
|
||||
],
|
||||
},
|
||||
],
|
||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onStartBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
|
||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
|
||||
)
|
||||
|
||||
const data = useMemo<Record<string, unknown> | null>(() => {
|
||||
|
||||
Reference in new issue
Block a user