✨ feat(vscode): 五种会话 id 行都能用橡皮擦清除本机记录,别的机器上的旧会话不再挡着新起
This commit is contained in:
1 parent
4d905ff16e
commit
d604b6f426
12 files changed
+169
-64
No files matched your search
@@ -565,8 +565,8 @@ export class KanbanWebviewPanel {
|
|||||||
void issues.handleUpdateBrainstormProfilePath(this, msg.issueNumber, msg.brainstormProfilePath)
|
void issues.handleUpdateBrainstormProfilePath(this, msg.issueNumber, msg.brainstormProfilePath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (msg.type === 'issue/clear-brainstorm-session') {
|
if (msg.type === 'issue/clear-session') {
|
||||||
void issues.handleClearBrainstormSession(this, msg.issueNumber)
|
void issues.handleClearSession(this, msg.issueNumber, msg.kind)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (msg.type === 'issue/update-test-profile-path') {
|
if (msg.type === 'issue/update-test-profile-path') {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { IssueColumn } from '../../gitea/types'
|
import type { IssueColumn } from '../../gitea/types'
|
||||||
import type { LockedReason } from '../issueLock'
|
import type { LockedReason } from '../issueLock'
|
||||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||||
|
import type { SessionTabKind } from './terminals'
|
||||||
import { Buffer } from 'node:buffer'
|
import { Buffer } from 'node:buffer'
|
||||||
import { randomUUID } from 'node:crypto'
|
import { randomUUID } from 'node:crypto'
|
||||||
import * as fs from 'node:fs'
|
import * as fs from 'node:fs'
|
||||||
@@ -39,6 +40,7 @@ import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
|
|||||||
import { evaluateLockedReason } from '../issueLock'
|
import { evaluateLockedReason } from '../issueLock'
|
||||||
import { makeNonce } from '../KanbanPanel'
|
import { makeNonce } from '../KanbanPanel'
|
||||||
import { cleanupFeatureBranch } from './branchCleanup'
|
import { cleanupFeatureBranch } from './branchCleanup'
|
||||||
|
import { clearSessionConfirmText, clearSessionDoneText, SESSION_KINDS } from './sessionKinds'
|
||||||
import * as sessions from './sessions'
|
import * as sessions from './sessions'
|
||||||
import { canResetToTodo, todoResetStateExtra, todoResetUiPatch } from './todoReset'
|
import { canResetToTodo, todoResetStateExtra, todoResetUiPatch } from './todoReset'
|
||||||
|
|
||||||
@@ -1971,17 +1973,20 @@ export async function handleUpdateProfilePath(panel: KanbanWebviewPanel, issueNu
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清掉工单记的头脑风暴会话 id,下次点 ▶ 续聊就新开一个会话。
|
* 清掉工单记的某种会话 id,之后该行可以新起一个会话。
|
||||||
*
|
*
|
||||||
* 用处:旧会话的历史被 `claude --resume` 原样带回来(里面可能还留着几个版本
|
* 用处:会话 id 是别的机器上开的、本机没有会话文件,resume 不了又挡着新起;
|
||||||
* 之前的提示词和它报的错),想从干净的上下文重新聊需求时用。会话文件本身不动,
|
* 或者旧会话历史太杂(头脑风暴里还留着几个版本之前的提示词和报错),想从干净
|
||||||
* 手上有 id 仍可用 `claude --resume <id>` 找回。
|
* 的上下文重来。只写本机记录('' 墓碑盖住共享值),别的机器不受影响;会话文件
|
||||||
|
* 本身不动,手上有 id 仍可 resume 找回。
|
||||||
*/
|
*/
|
||||||
export async function handleClearBrainstormSession(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
export async function handleClearSession(panel: KanbanWebviewPanel, issueNumber: number, kind: SessionTabKind): Promise<void> {
|
||||||
|
const { field, role } = SESSION_KINDS[kind]
|
||||||
let previousValue: string | undefined
|
let previousValue: string | undefined
|
||||||
try {
|
try {
|
||||||
const existingState = await panel.readIssueState(issueNumber)
|
const existingState = await panel.readIssueState(issueNumber)
|
||||||
previousValue = typeof existingState.sessionId === 'string' ? existingState.sessionId : undefined
|
const value = existingState[field]
|
||||||
|
previousValue = typeof value === 'string' && value.length > 0 ? value : undefined
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
previousValue = undefined
|
previousValue = undefined
|
||||||
@@ -1992,14 +1997,14 @@ export async function handleClearBrainstormSession(panel: KanbanWebviewPanel, is
|
|||||||
type: 'toast/show',
|
type: 'toast/show',
|
||||||
id: makeNonce(),
|
id: makeNonce(),
|
||||||
level: 'info',
|
level: 'info',
|
||||||
message: `工单 #${issueNumber} 本来就没有头脑风暴会话`,
|
message: `工单 #${issueNumber} 本来就没有${role}会话`,
|
||||||
dismissOnTimer: 4000,
|
dismissOnTimer: 4000,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const choice = await window.showWarningMessage(
|
const choice = await window.showWarningMessage(
|
||||||
`确定清除工单 #${issueNumber} 的头脑风暴会话 id(${previousValue})?下次续聊会新开一个会话,旧会话记录还在,可用 claude --resume 找回。`,
|
clearSessionConfirmText(issueNumber, kind, previousValue),
|
||||||
{ modal: true },
|
{ modal: true },
|
||||||
'清除',
|
'清除',
|
||||||
)
|
)
|
||||||
@@ -2007,21 +2012,21 @@ export async function handleClearBrainstormSession(panel: KanbanWebviewPanel, is
|
|||||||
return
|
return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await panel.mergeIssueState(issueNumber, { sessionId: '' })
|
await panel.mergeIssueState(issueNumber, { [field]: '' })
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err)
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
logger.add({
|
logger.add({
|
||||||
level: 'error',
|
level: 'error',
|
||||||
source: 'panel',
|
source: 'panel',
|
||||||
message: `清除 sessionId 失败 (issue #${issueNumber})`,
|
message: `清除 ${field} 失败 (issue #${issueNumber})`,
|
||||||
details: message,
|
details: message,
|
||||||
})
|
})
|
||||||
panel.postMessage({
|
panel.postMessage({
|
||||||
type: 'toast/show',
|
type: 'toast/show',
|
||||||
id: makeNonce(),
|
id: makeNonce(),
|
||||||
level: 'error',
|
level: 'error',
|
||||||
message: `清除工单 #${issueNumber} 头脑风暴会话失败: ${message}`,
|
message: `清除工单 #${issueNumber} ${role}会话失败: ${message}`,
|
||||||
dismissOnTimer: 6000,
|
dismissOnTimer: 6000,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
@@ -2030,13 +2035,13 @@ export async function handleClearBrainstormSession(panel: KanbanWebviewPanel, is
|
|||||||
panel.postMessage({
|
panel.postMessage({
|
||||||
type: 'issue/patch',
|
type: 'issue/patch',
|
||||||
issueNumber,
|
issueNumber,
|
||||||
patch: { sessionId: null },
|
patch: { [field]: null },
|
||||||
})
|
})
|
||||||
panel.postMessage({
|
panel.postMessage({
|
||||||
type: 'toast/show',
|
type: 'toast/show',
|
||||||
id: makeNonce(),
|
id: makeNonce(),
|
||||||
level: 'success',
|
level: 'success',
|
||||||
message: `工单 #${issueNumber} 头脑风暴会话已清除,下次续聊将新开会话`,
|
message: clearSessionDoneText(issueNumber, kind),
|
||||||
dismissOnTimer: 5000,
|
dismissOnTimer: 5000,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { clearSessionConfirmText, clearSessionDoneText, SESSION_KINDS } from './sessionKinds'
|
||||||
|
|
||||||
|
describe('会话种类表', () => {
|
||||||
|
it('五种会话各自对应 state 里的会话 id 字段,互不重复', () => {
|
||||||
|
expect(SESSION_KINDS.brainstorm.field).toBe('sessionId')
|
||||||
|
expect(SESSION_KINDS.implement.field).toBe('implementSessionId')
|
||||||
|
expect(SESSION_KINDS.review.field).toBe('reviewSessionId')
|
||||||
|
expect(SESSION_KINDS.test.field).toBe('testSessionId')
|
||||||
|
expect(SESSION_KINDS.prototype.field).toBe('prototypeSessionId')
|
||||||
|
const fields = Object.values(SESSION_KINDS).map(k => k.field)
|
||||||
|
expect(new Set(fields).size).toBe(fields.length)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('清除会话 id 的提示文案', () => {
|
||||||
|
it('确认框:角色 + id + 只清本机,末尾接该种会话的后续提示', () => {
|
||||||
|
expect(clearSessionConfirmText(12, 'brainstorm', 'abc')).toBe(
|
||||||
|
'确定清除工单 #12 的头脑风暴会话 id(abc)?只清本机记录,别的机器不受影响;会话文件不删。下次续聊会新开会话。',
|
||||||
|
)
|
||||||
|
expect(clearSessionConfirmText(12, 'implement', 'abc')).toContain('重新实施会删掉远程 feature 分支')
|
||||||
|
expect(clearSessionConfirmText(12, 'test', 'abc')).toBe(
|
||||||
|
'确定清除工单 #12 的测试会话 id(abc)?只清本机记录,别的机器不受影响;会话文件不删。清除后可点 ▶ 新起一个。',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('成功 toast:简短复述后续提示', () => {
|
||||||
|
expect(clearSessionDoneText(12, 'brainstorm')).toBe('工单 #12 的头脑风暴会话 id 已清除,下次续聊将新开会话')
|
||||||
|
expect(clearSessionDoneText(12, 'review')).toBe('工单 #12 的审查会话 id 已清除,可点 ▶ 新起一个')
|
||||||
|
expect(clearSessionDoneText(12, 'implement')).toContain('会删掉远程 feature 分支')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { SessionTabKind } from './terminals'
|
||||||
|
|
||||||
|
/** 工单 state 里存各种会话 id 的字段。 */
|
||||||
|
export type SessionIdField = 'sessionId' | 'implementSessionId' | 'reviewSessionId' | 'testSessionId' | 'prototypeSessionId'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每种会话对应的 state 字段、中文角色名,以及清除该会话 id 后的后续提示:
|
||||||
|
* confirmHint 进确认框,doneHint 进成功 toast。
|
||||||
|
*/
|
||||||
|
export const SESSION_KINDS: Record<SessionTabKind, { field: SessionIdField, role: string, confirmHint: string, doneHint: string }> = {
|
||||||
|
brainstorm: {
|
||||||
|
field: 'sessionId',
|
||||||
|
role: '头脑风暴',
|
||||||
|
confirmHint: '下次续聊会新开会话',
|
||||||
|
doneHint: '下次续聊将新开会话',
|
||||||
|
},
|
||||||
|
implement: {
|
||||||
|
field: 'implementSessionId',
|
||||||
|
role: '实施',
|
||||||
|
confirmHint: '清除后可点 ▶ 重新实施,注意重新实施会删掉远程 feature 分支,别的机器上的实施进度会丢',
|
||||||
|
doneHint: '可点 ▶ 重新实施(会删掉远程 feature 分支)',
|
||||||
|
},
|
||||||
|
review: {
|
||||||
|
field: 'reviewSessionId',
|
||||||
|
role: '审查',
|
||||||
|
confirmHint: '清除后可点 ▶ 新起一个',
|
||||||
|
doneHint: '可点 ▶ 新起一个',
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
field: 'testSessionId',
|
||||||
|
role: '测试',
|
||||||
|
confirmHint: '清除后可点 ▶ 新起一个',
|
||||||
|
doneHint: '可点 ▶ 新起一个',
|
||||||
|
},
|
||||||
|
prototype: {
|
||||||
|
field: 'prototypeSessionId',
|
||||||
|
role: '原型',
|
||||||
|
confirmHint: '清除后可点 ▶ 新起一个',
|
||||||
|
doneHint: '可点 ▶ 新起一个',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSessionConfirmText(issueNumber: number, kind: SessionTabKind, sessionId: string): string {
|
||||||
|
const { role, confirmHint } = SESSION_KINDS[kind]
|
||||||
|
return `确定清除工单 #${issueNumber} 的${role}会话 id(${sessionId})?只清本机记录,别的机器不受影响;会话文件不删。${confirmHint}。`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSessionDoneText(issueNumber: number, kind: SessionTabKind): string {
|
||||||
|
const { role, doneHint } = SESSION_KINDS[kind]
|
||||||
|
return `工单 #${issueNumber} 的${role}会话 id 已清除,${doneHint}`
|
||||||
|
}
|
||||||
@@ -223,7 +223,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
||||||
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
|
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
|
||||||
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
|
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
|
||||||
| { type: 'issue/clear-brainstorm-session', issueNumber: number }
|
| { type: 'issue/clear-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }
|
||||||
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
|
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
|
||||||
| { type: 'issue/update-prototype-profile-path', issueNumber: number, prototypeProfilePath: string }
|
| { type: 'issue/update-prototype-profile-path', issueNumber: number, prototypeProfilePath: string }
|
||||||
| { type: 'logs/fetch' }
|
| { type: 'logs/fetch' }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe('isOwnerOnlyMessage', () => {
|
|||||||
expect(isOwnerOnlyMessage('issue/close')).toBe(true)
|
expect(isOwnerOnlyMessage('issue/close')).toBe(true)
|
||||||
expect(isOwnerOnlyMessage('dependency/set')).toBe(true)
|
expect(isOwnerOnlyMessage('dependency/set')).toBe(true)
|
||||||
expect(isOwnerOnlyMessage('issue/update-auto-review')).toBe(true)
|
expect(isOwnerOnlyMessage('issue/update-auto-review')).toBe(true)
|
||||||
|
expect(isOwnerOnlyMessage('issue/clear-session')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('在本机替别人跑会话的消息要门禁', () => {
|
it('在本机替别人跑会话的消息要门禁', () => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
|
|||||||
'issue/update-auto-review',
|
'issue/update-auto-review',
|
||||||
'issue/update-profile-path',
|
'issue/update-profile-path',
|
||||||
'issue/update-brainstorm-profile-path',
|
'issue/update-brainstorm-profile-path',
|
||||||
'issue/clear-brainstorm-session',
|
'issue/clear-session',
|
||||||
'issue/update-test-profile-path',
|
'issue/update-test-profile-path',
|
||||||
'issue/update-prototype-profile-path',
|
'issue/update-prototype-profile-path',
|
||||||
'dependency/set',
|
'dependency/set',
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export function App() {
|
|||||||
closeIssue,
|
closeIssue,
|
||||||
closeSessionTab,
|
closeSessionTab,
|
||||||
continueBrainstorm,
|
continueBrainstorm,
|
||||||
clearBrainstormSession,
|
clearSession,
|
||||||
changeColumn,
|
changeColumn,
|
||||||
setDependency,
|
setDependency,
|
||||||
clearDependency,
|
clearDependency,
|
||||||
@@ -419,7 +419,7 @@ export function App() {
|
|||||||
onResetToTodo={n => changeColumn(n, 'todo', selectedIssue?.source, selectedIssue?.externalId)}
|
onResetToTodo={n => changeColumn(n, 'todo', selectedIssue?.source, selectedIssue?.externalId)}
|
||||||
onCloseSessionTab={closeSessionTab}
|
onCloseSessionTab={closeSessionTab}
|
||||||
onContinueBrainstorm={continueBrainstorm}
|
onContinueBrainstorm={continueBrainstorm}
|
||||||
onClearBrainstormSession={clearBrainstormSession}
|
onClearSession={clearSession}
|
||||||
onUpdateAutoReview={updateIssueAutoReview}
|
onUpdateAutoReview={updateIssueAutoReview}
|
||||||
onUpdateProfilePath={updateIssueProfilePath}
|
onUpdateProfilePath={updateIssueProfilePath}
|
||||||
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
|
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ interface BottomTabsProps {
|
|||||||
onResetToTodo: (issueNumber: number) => void
|
onResetToTodo: (issueNumber: number) => void
|
||||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
||||||
onContinueBrainstorm: (issueNumber: number) => void
|
onContinueBrainstorm: (issueNumber: number) => void
|
||||||
onClearBrainstormSession: (issueNumber: number) => void
|
onClearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
||||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||||
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
|
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
|
||||||
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||||
@@ -154,7 +154,7 @@ export function BottomTabs(props: BottomTabsProps) {
|
|||||||
onResetToTodo={props.onResetToTodo}
|
onResetToTodo={props.onResetToTodo}
|
||||||
onCloseSessionTab={props.onCloseSessionTab}
|
onCloseSessionTab={props.onCloseSessionTab}
|
||||||
onContinueBrainstorm={props.onContinueBrainstorm}
|
onContinueBrainstorm={props.onContinueBrainstorm}
|
||||||
onClearBrainstormSession={props.onClearBrainstormSession}
|
onClearSession={props.onClearSession}
|
||||||
onUpdateAutoReview={props.onUpdateAutoReview}
|
onUpdateAutoReview={props.onUpdateAutoReview}
|
||||||
onUpdateProfilePath={props.onUpdateProfilePath}
|
onUpdateProfilePath={props.onUpdateProfilePath}
|
||||||
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ interface IssueDetailPanelProps {
|
|||||||
* 打进去,关了就 `claude --resume <id>` 带着提示词恢复,没会话就新开一个。
|
* 打进去,关了就 `claude --resume <id>` 带着提示词恢复,没会话就新开一个。
|
||||||
*/
|
*/
|
||||||
onContinueBrainstorm: (issueNumber: number) => void
|
onContinueBrainstorm: (issueNumber: number) => void
|
||||||
onClearBrainstormSession: (issueNumber: number) => void
|
/** 清除某行的会话 id:扩展侧弹确认框,只清本机记录,之后该行可新起一个。 */
|
||||||
|
onClearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
||||||
/** Persist a per-issue `autoReview` override into the state JSON. */
|
/** Persist a per-issue `autoReview` override into the state JSON. */
|
||||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||||
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
|
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
|
||||||
@@ -173,7 +174,7 @@ export function IssueDetailPanel({
|
|||||||
onResetToTodo,
|
onResetToTodo,
|
||||||
onCloseSessionTab,
|
onCloseSessionTab,
|
||||||
onContinueBrainstorm,
|
onContinueBrainstorm,
|
||||||
onClearBrainstormSession,
|
onClearSession,
|
||||||
onUpdateAutoReview,
|
onUpdateAutoReview,
|
||||||
profiles,
|
profiles,
|
||||||
onUpdateProfilePath,
|
onUpdateProfilePath,
|
||||||
@@ -213,6 +214,14 @@ export function IssueDetailPanel({
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 五行会话 id 同一规则:有 id 就给 Eraser,排在该行按钮最后。会话文件在别的
|
||||||
|
// 机器上时,resume 不了的旧 id 靠它清掉,才能新起一个。
|
||||||
|
const clearSessionAction = (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype'): RowAction => ({
|
||||||
|
icon: <Eraser className="size-3.5" />,
|
||||||
|
title: '清除会话 id:只清本机记录,之后可新起一个',
|
||||||
|
onClick: () => onClearSession(issueNumber, kind),
|
||||||
|
})
|
||||||
|
|
||||||
const schema = useMemo<PropertyGroup[]>(
|
const schema = useMemo<PropertyGroup[]>(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@@ -246,6 +255,7 @@ export function IssueDetailPanel({
|
|||||||
// - 工单未完成 → 一直有 ▶ 续聊。插件把续聊提示词送进对应会话:
|
// - 工单未完成 → 一直有 ▶ 续聊。插件把续聊提示词送进对应会话:
|
||||||
// 规划终端开着就直接打进去,关了就 --resume 带提示词,没会话就新开。
|
// 规划终端开着就直接打进去,关了就 --resume 带提示词,没会话就新开。
|
||||||
// - brainstormTabOpen → 再加一个 X 关闭 tab
|
// - brainstormTabOpen → 再加一个 X 关闭 tab
|
||||||
|
// - 有会话 id → 最后再加一个 Eraser 清除本机记录
|
||||||
// - 会话 id 本机没有会话文件 → 只读不可双击,续聊会新开会话
|
// - 会话 id 本机没有会话文件 → 只读不可双击,续聊会新开会话
|
||||||
const hasSession = !!(issue?.sessionId && issue.sessionId.length > 0)
|
const hasSession = !!(issue?.sessionId && issue.sessionId.length > 0)
|
||||||
const missing = isSessionMissing(issue, issue?.sessionId)
|
const missing = isSessionMissing(issue, issue?.sessionId)
|
||||||
@@ -271,16 +281,8 @@ export function IssueDetailPanel({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (hasSession) {
|
if (hasSession && issue)
|
||||||
secondaryActions.push({
|
secondaryActions.push(clearSessionAction(issue.number, 'brainstorm'))
|
||||||
icon: <Eraser className="size-3.5" />,
|
|
||||||
title: '清除会话:下次续聊新开一个,不再带旧会话的历史',
|
|
||||||
onClick: () => {
|
|
||||||
if (issue)
|
|
||||||
onClearBrainstormSession(issue.number)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (missing) {
|
if (missing) {
|
||||||
return {
|
return {
|
||||||
key: 'sessionId',
|
key: 'sessionId',
|
||||||
@@ -288,7 +290,6 @@ export function IssueDetailPanel({
|
|||||||
type: 'action' as const,
|
type: 'action' as const,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
description: canContinue ? `${MISSING_SESSION_DESC};点 ▶ 续聊会新开一个规划会话` : MISSING_SESSION_DESC,
|
description: canContinue ? `${MISSING_SESSION_DESC};点 ▶ 续聊会新开一个规划会话` : MISSING_SESSION_DESC,
|
||||||
actionIcon: <Terminal className="size-3.5" />,
|
|
||||||
secondaryActions,
|
secondaryActions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -316,8 +317,9 @@ export function IssueDetailPanel({
|
|||||||
// extension 端 handleResumeSession 会在 worktree 路径不存在时退回
|
// extension 端 handleResumeSession 会在 worktree 路径不存在时退回
|
||||||
// 工作区根目录,并通过 toast 告知用户;description 在两种状态下
|
// 工作区根目录,并通过 toast 告知用户;description 在两种状态下
|
||||||
// 给出不同的提示文案。
|
// 给出不同的提示文案。
|
||||||
// 本机没有会话文件时只读且不给 ▶:重新实施会先删远程 feature 分支,
|
// 有会话 id 就在最后加 Eraser 清除本机记录。本机没有会话文件时只读且
|
||||||
// 会把别的机器上的实施进度一并抹掉。
|
// 不直接给 ▶:重新实施会先删远程 feature 分支,会把别的机器上的实施
|
||||||
|
// 进度一并抹掉;确要重来得先用 Eraser 清掉 id,▶ 再按无会话的规则出现。
|
||||||
const secondaryActions: RowAction[] = (() => {
|
const secondaryActions: RowAction[] = (() => {
|
||||||
if (issue?.implementTabOpen) {
|
if (issue?.implementTabOpen) {
|
||||||
return [{
|
return [{
|
||||||
@@ -339,6 +341,8 @@ export function IssueDetailPanel({
|
|||||||
onClick: () => onImplement(issue.number, issue.planFile ?? '', issue.profilePath, issue.sessionId),
|
onClick: () => onImplement(issue.number, issue.planFile ?? '', issue.profilePath, issue.sessionId),
|
||||||
}]
|
}]
|
||||||
})()
|
})()
|
||||||
|
if (issue?.implementSessionId)
|
||||||
|
secondaryActions.push(clearSessionAction(issue.number, 'implement'))
|
||||||
if (isSessionMissing(issue, issue?.implementSessionId)) {
|
if (isSessionMissing(issue, issue?.implementSessionId)) {
|
||||||
return {
|
return {
|
||||||
key: 'implementSessionId',
|
key: 'implementSessionId',
|
||||||
@@ -346,7 +350,6 @@ export function IssueDetailPanel({
|
|||||||
type: 'action' as const,
|
type: 'action' as const,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
description: `${MISSING_SESSION_DESC};要在本机接着实施,请让对方把工单移交过来`,
|
description: `${MISSING_SESSION_DESC};要在本机接着实施,请让对方把工单移交过来`,
|
||||||
actionIcon: <Terminal className="size-3.5" />,
|
|
||||||
secondaryActions,
|
secondaryActions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -384,13 +387,16 @@ export function IssueDetailPanel({
|
|||||||
return
|
return
|
||||||
onResumeReviewSession(v, issue.number, issue?.worktreePath)
|
onResumeReviewSession(v, issue.number, issue?.worktreePath)
|
||||||
},
|
},
|
||||||
secondaryActions: issue.reviewTabOpen
|
secondaryActions: [
|
||||||
? [{
|
...(issue.reviewTabOpen
|
||||||
icon: <X className="size-3.5" />,
|
? [{
|
||||||
title: '关闭审查 tab',
|
icon: <X className="size-3.5" />,
|
||||||
onClick: () => onCloseSessionTab(issue.number, 'review'),
|
title: '关闭审查 tab',
|
||||||
}]
|
onClick: () => onCloseSessionTab(issue.number, 'review'),
|
||||||
: [],
|
}]
|
||||||
|
: []),
|
||||||
|
clearSessionAction(issue.number, 'review'),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
key: 'reviewSessionId',
|
key: 'reviewSessionId',
|
||||||
@@ -400,14 +406,17 @@ export function IssueDetailPanel({
|
|||||||
description: issue?.reviewSessionId
|
description: issue?.reviewSessionId
|
||||||
? `${MISSING_SESSION_DESC};点 ▶ 对当前 PR 重新起一个审查会话`
|
? `${MISSING_SESSION_DESC};点 ▶ 对当前 PR 重新起一个审查会话`
|
||||||
: '还没有审查会话;点 ▶ 对当前 PR 启动 codex 审查(等同 PR 事件自动触发的那次)',
|
: '还没有审查会话;点 ▶ 对当前 PR 启动 codex 审查(等同 PR 事件自动触发的那次)',
|
||||||
secondaryActions: issue?.pr
|
secondaryActions: [
|
||||||
? [{
|
...(issue?.pr
|
||||||
icon: <Play className="size-3.5" />,
|
? [{
|
||||||
title: locked ? lockTitle : '启动审查会话',
|
icon: <Play className="size-3.5" />,
|
||||||
disabled: locked,
|
title: locked ? lockTitle : '启动审查会话',
|
||||||
onClick: () => onStartReviewSession(issue.number),
|
disabled: locked,
|
||||||
}]
|
onClick: () => onStartReviewSession(issue.number),
|
||||||
: [],
|
}]
|
||||||
|
: []),
|
||||||
|
...(issue?.reviewSessionId ? [clearSessionAction(issue.number, 'review')] : []),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
(() => {
|
(() => {
|
||||||
// 测试会话 id 行有三种状态(照搬头脑风暴行的三态):
|
// 测试会话 id 行有三种状态(照搬头脑风暴行的三态):
|
||||||
@@ -419,6 +428,7 @@ export function IssueDetailPanel({
|
|||||||
// 那条路径要靠 PR 让 cc 了解合并进来的改动。
|
// 那条路径要靠 PR 让 cc 了解合并进来的改动。
|
||||||
// 会话 id 本机没有会话文件 → 只读不可双击,按状态 3 的条件给 ▶ 重新起一个
|
// 会话 id 本机没有会话文件 → 只读不可双击,按状态 3 的条件给 ▶ 重新起一个
|
||||||
// (启动会覆盖旧 testSessionId)。
|
// (启动会覆盖旧 testSessionId)。
|
||||||
|
// 有会话 id 时最后再加一个 Eraser 清除本机记录。
|
||||||
const hasSession = !!(issue?.testSessionId && issue.testSessionId.length > 0)
|
const hasSession = !!(issue?.testSessionId && issue.testSessionId.length > 0)
|
||||||
const missing = isSessionMissing(issue, issue?.testSessionId)
|
const missing = isSessionMissing(issue, issue?.testSessionId)
|
||||||
const canStart = (!hasSession || missing) && (!!issue?.worktreeExists || !!issue?.pr)
|
const canStart = (!hasSession || missing) && (!!issue?.worktreeExists || !!issue?.pr)
|
||||||
@@ -443,6 +453,8 @@ export function IssueDetailPanel({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (hasSession && issue)
|
||||||
|
secondaryActions.push(clearSessionAction(issue.number, 'test'))
|
||||||
if (missing) {
|
if (missing) {
|
||||||
return {
|
return {
|
||||||
key: 'testSessionId',
|
key: 'testSessionId',
|
||||||
@@ -450,7 +462,6 @@ export function IssueDetailPanel({
|
|||||||
type: 'action' as const,
|
type: 'action' as const,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
description: `${MISSING_SESSION_DESC};点 ▶ 新起一个测试会话(worktree 还在就在 worktree 里测,已清理则需要工单有已合并的 PR)`,
|
description: `${MISSING_SESSION_DESC};点 ▶ 新起一个测试会话(worktree 还在就在 worktree 里测,已清理则需要工单有已合并的 PR)`,
|
||||||
actionIcon: <Terminal className="size-3.5" />,
|
|
||||||
secondaryActions,
|
secondaryActions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,7 +488,8 @@ export function IssueDetailPanel({
|
|||||||
// 原型会话 id 行:与测试行同构的三态,差别是启动不看 PR——原型只照
|
// 原型会话 id 行:与测试行同构的三态,差别是启动不看 PR——原型只照
|
||||||
// 工单正文画界面,任何阶段都能起;前置工单没做完时按钮禁用。
|
// 工单正文画界面,任何阶段都能起;前置工单没做完时按钮禁用。
|
||||||
// 会话 id 本机没有会话文件 → 只读不可双击,同样给 ▶ 重新起一个
|
// 会话 id 本机没有会话文件 → 只读不可双击,同样给 ▶ 重新起一个
|
||||||
// (启动会覆盖旧 prototypeSessionId)。
|
// (启动会覆盖旧 prototypeSessionId)。有会话 id 时最后再加一个 Eraser
|
||||||
|
// 清除本机记录。
|
||||||
const hasSession = !!(issue?.prototypeSessionId && issue.prototypeSessionId.length > 0)
|
const hasSession = !!(issue?.prototypeSessionId && issue.prototypeSessionId.length > 0)
|
||||||
const missing = isSessionMissing(issue, issue?.prototypeSessionId)
|
const missing = isSessionMissing(issue, issue?.prototypeSessionId)
|
||||||
const secondaryActions: RowAction[] = []
|
const secondaryActions: RowAction[] = []
|
||||||
@@ -502,6 +514,8 @@ export function IssueDetailPanel({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (hasSession && issue)
|
||||||
|
secondaryActions.push(clearSessionAction(issue.number, 'prototype'))
|
||||||
if (missing) {
|
if (missing) {
|
||||||
return {
|
return {
|
||||||
key: 'prototypeSessionId',
|
key: 'prototypeSessionId',
|
||||||
@@ -509,7 +523,6 @@ export function IssueDetailPanel({
|
|||||||
type: 'action' as const,
|
type: 'action' as const,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
description: `${MISSING_SESSION_DESC};点 ▶ 新起一个原型会话`,
|
description: `${MISSING_SESSION_DESC};点 ▶ 新起一个原型会话`,
|
||||||
actionIcon: <Terminal className="size-3.5" />,
|
|
||||||
secondaryActions,
|
secondaryActions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -729,7 +742,7 @@ export function IssueDetailPanel({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onResumePrototypeSession, onStartPrototypeSession, onOpenFile, onOpenPrototypeInFigma, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending, onStartReviewSession],
|
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onResumePrototypeSession, onStartPrototypeSession, onOpenFile, onOpenPrototypeInFigma, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles, isSessionTabPending, onStartReviewSession],
|
||||||
)
|
)
|
||||||
|
|
||||||
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
|
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
|
||||||
|
|||||||
@@ -241,8 +241,8 @@ export interface UseIssuesResult {
|
|||||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||||
*/
|
*/
|
||||||
updateIssueBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
updateIssueBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||||
/** 清掉工单记的头脑风暴会话 id,下次续聊新开会话。 */
|
/** 清掉工单在本机记的某种会话 id,之后该行可新起一个会话。 */
|
||||||
clearBrainstormSession: (issueNumber: number) => void
|
clearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
||||||
/**
|
/**
|
||||||
* 覆盖该工单测试会话专用的 Claude 配置文件,乐观更新本地 state 后
|
* 覆盖该工单测试会话专用的 Claude 配置文件,乐观更新本地 state 后
|
||||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||||
@@ -736,11 +736,14 @@ export function useIssues(): UseIssuesResult {
|
|||||||
postMessage({ type: 'issue/update-brainstorm-profile-path', issueNumber, brainstormProfilePath })
|
postMessage({ type: 'issue/update-brainstorm-profile-path', issueNumber, brainstormProfilePath })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 清除头脑风暴会话 id:扩展侧先弹确认框,确认后才落库并推 issue/patch,
|
// 清除会话 id:扩展侧先弹确认框,确认后才落库并推 issue/patch,
|
||||||
// 所以这里不做乐观更新。
|
// 所以这里不做乐观更新。
|
||||||
const clearBrainstormSession = useCallback((issueNumber: number): void => {
|
const clearSession = useCallback(
|
||||||
postMessage({ type: 'issue/clear-brainstorm-session', issueNumber })
|
(issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype'): void => {
|
||||||
}, [])
|
postMessage({ type: 'issue/clear-session', issueNumber, kind })
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
const updateIssueTestProfilePath = useCallback((issueNumber: number, testProfilePath: string): void => {
|
const updateIssueTestProfilePath = useCallback((issueNumber: number, testProfilePath: string): void => {
|
||||||
// Optimistically update local state so the dropdown reflects immediately
|
// Optimistically update local state so the dropdown reflects immediately
|
||||||
@@ -1039,5 +1042,5 @@ export function useIssues(): UseIssuesResult {
|
|||||||
return cleanup
|
return cleanup
|
||||||
}, [clearPrDiffSummaryRunning, markHandoffRunning])
|
}, [clearPrDiffSummaryRunning, markHandoffRunning])
|
||||||
|
|
||||||
return { state, settings, avatars, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startReviewSession, startTestSession, resumeTestSession, startPrototypeSession, resumePrototypeSession, focusSession, openFile, openPrototypeInFigma, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearBrainstormSession, updateIssueTestProfilePath, updateIssuePrototypeProfilePath, 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, isSessionTabPending, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startReviewSession, startTestSession, resumeTestSession, startPrototypeSession, resumePrototypeSession, focusSession, openFile, openPrototypeInFigma, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearSession, updateIssueTestProfilePath, updateIssuePrototypeProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
|
||||||
}
|
}
|
||||||
@@ -254,7 +254,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
||||||
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
|
| { type: 'issue/update-profile-path', issueNumber: number, profilePath: string }
|
||||||
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
|
| { type: 'issue/update-brainstorm-profile-path', issueNumber: number, brainstormProfilePath: string }
|
||||||
| { type: 'issue/clear-brainstorm-session', issueNumber: number }
|
| { type: 'issue/clear-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }
|
||||||
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
|
| { type: 'issue/update-test-profile-path', issueNumber: number, testProfilePath: string }
|
||||||
| { type: 'issue/update-prototype-profile-path', issueNumber: number, prototypeProfilePath: string }
|
| { type: 'issue/update-prototype-profile-path', issueNumber: number, prototypeProfilePath: string }
|
||||||
| { type: 'logs/fetch' }
|
| { type: 'logs/fetch' }
|
||||||
|
|||||||
Reference in new issue
Block a user