✨ 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
+276
-266
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
||||
"publisher": "clurdra",
|
||||
"name": "superpowers-vscode-clurdra",
|
||||
"displayName": "Superpowers-clurdra",
|
||||
"version": "0.2.116",
|
||||
"version": "0.2.117",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"description": "Superpowers specs and plans Kanban explorer",
|
||||
"author": "clurdra",
|
||||
|
||||
@@ -16,9 +16,13 @@ export interface CcCommandOpts {
|
||||
profilePath: string
|
||||
/** e.g. 'high' → `--effort high`; omitted means no `--effort`. */
|
||||
effort?: string
|
||||
/** When set, append `--resume <id>`. Mutually exclusive with prompt; resume wins. */
|
||||
/** When set, append `--resume <id>` so cc restores that conversation. */
|
||||
resumeSessionId?: string
|
||||
/** When set (and no resume), append the prompt as a positional `'<prompt>'` arg. */
|
||||
/**
|
||||
* When set, append the prompt as a positional `'<prompt>'` arg. Combined
|
||||
* with `resumeSessionId` it is submitted as the next message of the
|
||||
* restored conversation.
|
||||
*/
|
||||
prompt?: string
|
||||
}
|
||||
|
||||
@@ -31,11 +35,9 @@ export function buildCcCommand(opts: CcCommandOpts): string {
|
||||
if (opts.effort)
|
||||
parts.push(`--effort ${opts.effort}`)
|
||||
parts.push('--dangerously-skip-permissions', `--settings '${opts.profilePath}'`)
|
||||
// resume and prompt are mutually exclusive; a resumed session restores its
|
||||
// own conversation, so a fresh prompt would be meaningless.
|
||||
if (opts.resumeSessionId)
|
||||
parts.push(`--resume ${opts.resumeSessionId}`)
|
||||
else if (opts.prompt)
|
||||
if (opts.prompt)
|
||||
parts.push(`'${opts.prompt}'`)
|
||||
return parts.join(' ')
|
||||
}
|
||||
@@ -562,8 +562,8 @@ export class KanbanWebviewPanel {
|
||||
void issues.handleCloseIssue(this, msg.issueNumber)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'brainstorm/start') {
|
||||
void sessions.handleStartBrainstormSession(this, msg.issueNumber)
|
||||
if (msg.type === 'brainstorm/continue') {
|
||||
void sessions.handleContinueBrainstorm(this, msg.issueNumber)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'profiles/get') {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 「续聊」按钮的三条路径。终端还活着就直接把提示词打进去(不管有没有
|
||||
* 记录到 sessionId,新建工单流程里 id 可能还没落盘);终端没了但有
|
||||
* sessionId 就 `--resume` 并带上提示词;两者都没有才新开会话。
|
||||
*/
|
||||
export type BrainstormContinuePath = 'inject' | 'resume' | 'fresh'
|
||||
|
||||
export function pickBrainstormContinuePath(opts: { liveTerminal: boolean, sessionId: string | undefined }): BrainstormContinuePath {
|
||||
if (opts.liveTerminal)
|
||||
return 'inject'
|
||||
if (opts.sessionId)
|
||||
return 'resume'
|
||||
return 'fresh'
|
||||
}
|
||||
@@ -22,8 +22,10 @@ import { logger } from '../../logging/logger'
|
||||
import { getSettings } from '../../settings/store'
|
||||
import { webhookCoordinator } from '../../webhook/coordinator'
|
||||
import { decideAutoReviewPath } from '../autoReviewDecision'
|
||||
import { pickBrainstormContinuePath } from '../brainstormContinue'
|
||||
import { makeNonce } from '../KanbanPanel'
|
||||
import { cleanupFeatureBranch } from './branchCleanup'
|
||||
import { submitToSessionTerminal } from './terminals'
|
||||
|
||||
export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId: string, profilePath?: string, relCwd?: string, issueNumber?: number): Promise<void> {
|
||||
// kind 跟着 sessionRole 提前判定(原来散落在方法中部,提到入口是为了构造
|
||||
@@ -446,15 +448,7 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
terminal.show(false)
|
||||
// codex TUI already running inside this tab; sending the full
|
||||
// startup command would be received as a long redundant user message.
|
||||
// Mirror the two-step submit pattern from injectIntoImplTerminal:
|
||||
// push the body first, then send a standalone \r 250ms later so codex
|
||||
// TUI registers it as Enter (single-shot \r packed with the body is
|
||||
// sometimes consumed as a newline, not a submit).
|
||||
const body = 'PR 更新了,再审一下'
|
||||
terminal.sendText(body, false)
|
||||
setTimeout(() => {
|
||||
terminal.sendText('\r', false)
|
||||
}, 250)
|
||||
submitToSessionTerminal(terminal, 'PR 更新了,再审一下')
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'terminal',
|
||||
@@ -493,12 +487,8 @@ async function triggerAutoReviewTabUnlocked(panel: KanbanWebviewPanel, opts: {
|
||||
// before the TUI accepts input — empirically much slower than the
|
||||
// 250ms gap the reuse path uses. 8s is a conservative guess that
|
||||
// covers a cold codex start without feeling laggy.
|
||||
const body = 'PR 更新了,再审一下'
|
||||
setTimeout(() => {
|
||||
terminal.sendText(body, false)
|
||||
setTimeout(() => {
|
||||
terminal.sendText('\r', false)
|
||||
}, 250)
|
||||
submitToSessionTerminal(terminal, 'PR 更新了,再审一下')
|
||||
}, 8000)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
@@ -954,34 +944,13 @@ export async function handleImplement(
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a fresh "规划" (brainstorm continuation) cc tab for an existing
|
||||
* issue whose body has no sessionId yet. Used when the issue was created
|
||||
* outside the panel (e.g. user ran spx from another cc session, body
|
||||
* lacks the nonce marker so the webhook took the "external" branch and
|
||||
* didn't link a terminal). Click the Play button next to the empty
|
||||
* "头脑风暴会话 id" row to spawn a new cc session anchored to this issue.
|
||||
*
|
||||
* Differs from `handleImplement`:
|
||||
* - No worktree / no feature branch (this is discussion, not coding).
|
||||
* - cwd = workspaceRoot (not a worktree).
|
||||
* - Watches the workspace-root `~/.claude/projects/...` dir for the new
|
||||
* session jsonl and writes the captured id back as `sessionId`
|
||||
* (not `implementSessionId`).
|
||||
* 详情面板「▶ 续聊」入口:把续聊 prompt 送进这个工单的头脑风暴会话。
|
||||
* - 规划终端还开着 → 直接打进去(含新建工单流程留下的 issue-new-* tab)
|
||||
* - 终端关了但 state JSON 有 sessionId → `--resume <id>` 并带上 prompt
|
||||
* - 两者都没有 → 新开会话,捕获到的 id 写回 `sessionId`
|
||||
* 全程在 workspaceRoot,不建 worktree / 分支(这是讨论,不是编码)。
|
||||
*/
|
||||
export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
const terminalName = `issue-${issueNumber}-规划`
|
||||
const existing = panel.findExistingTerminal(terminalName)
|
||||
if (existing) {
|
||||
panel.trackSessionTerminal(existing, issueNumber, 'brainstorm')
|
||||
existing.show(false)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'brainstorm',
|
||||
message: `复用已有规划终端 #${issueNumber},跳过 cc 启动`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
export async function handleContinueBrainstorm(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||
@@ -1000,10 +969,13 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
|
||||
|
||||
// 头脑风暴只用 brainstormProfilePath;空 = DEFAULT,不回退实施 profile。
|
||||
let brainstormProfilePath: string | undefined
|
||||
let sessionId: string | undefined
|
||||
try {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
if (stateObj && typeof stateObj.brainstormProfilePath === 'string' && stateObj.brainstormProfilePath.length > 0)
|
||||
brainstormProfilePath = stateObj.brainstormProfilePath
|
||||
if (stateObj && typeof stateObj.sessionId === 'string' && stateObj.sessionId.length > 0)
|
||||
sessionId = stateObj.sessionId
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
@@ -1014,22 +986,41 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
|
||||
})
|
||||
}
|
||||
|
||||
const prompt = getBrainstormContinuePrompt(panel.context, { issueNumber, issueRef: panel.refFor(issueNumber).externalId })
|
||||
const terminalName = `issue-${issueNumber}-规划`
|
||||
// 新建工单流程的 tab 叫 issue-new-{nonce}-规划,只能靠 newIssueTerminals 反查。
|
||||
const linked = panel.newIssueTerminals.get(issueNumber)
|
||||
const live = linked && linked.exitStatus === undefined ? linked : panel.findExistingTerminal(terminalName)
|
||||
const continuePath = pickBrainstormContinuePath({ liveTerminal: !!live, sessionId })
|
||||
|
||||
if (continuePath === 'inject' && live) {
|
||||
panel.trackSessionTerminal(live, issueNumber, 'brainstorm')
|
||||
live.show(false)
|
||||
submitToSessionTerminal(live, prompt)
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'brainstorm',
|
||||
message: `已把续聊 prompt 打进 #${issueNumber} 的规划终端`,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const effectiveProfilePath = resolveBrainstormProfilePath(panel, brainstormProfilePath)
|
||||
if (effectiveProfilePath.includes('\'')) {
|
||||
void window.showErrorMessage(
|
||||
`启动规划失败:brainstormProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
`续聊失败:brainstormProfilePath 含单引号,拒绝执行 (${effectiveProfilePath})`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const prompt = getBrainstormContinuePrompt(panel.context, { issueNumber, issueRef: panel.refFor(issueNumber).externalId })
|
||||
if (prompt.includes('\'')) {
|
||||
void window.showErrorMessage('启动规划失败:prompt 含单引号,拒绝执行')
|
||||
void window.showErrorMessage('续聊失败:prompt 含单引号,拒绝执行')
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure the projects dir exists so the watcher can't miss the create
|
||||
// event. Workspace root (no worktree here).
|
||||
// resume 沿用原 id,只有新开会话才要盯着 session jsonl 出现;
|
||||
// 监听要在 createTerminal 之前挂上,否则会漏掉创建事件。
|
||||
let watchPromise: ReturnType<typeof watchForNewSession> | undefined
|
||||
if (continuePath === 'fresh') {
|
||||
const projDir = projectsDirFor(workspaceRoot)
|
||||
try {
|
||||
await fsp.mkdir(projDir, { recursive: true })
|
||||
@@ -1037,9 +1028,8 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
|
||||
catch (err) {
|
||||
console.warn('[superpowers] failed to mkdir claude projects dir:', err)
|
||||
}
|
||||
|
||||
// Kick off the watcher before spawning the terminal so we don't race.
|
||||
const watchPromise = watchForNewSession({ projectsDir: projDir, timeoutMs: 120_000 })
|
||||
watchPromise = watchForNewSession({ projectsDir: projDir, timeoutMs: 120_000 })
|
||||
}
|
||||
|
||||
const { themeColor, iconUri } = await panel.resolveIssueIcon(issueNumber)
|
||||
const terminal = window.createTerminal({
|
||||
@@ -1049,6 +1039,8 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
|
||||
iconPath: iconUri,
|
||||
color: themeColor,
|
||||
})
|
||||
if (continuePath === 'resume' && sessionId)
|
||||
panel.terminals.set(sessionId, terminal)
|
||||
panel.trackSessionTerminal(terminal, issueNumber, 'brainstorm')
|
||||
terminal.show(false)
|
||||
logger.add({
|
||||
@@ -1057,18 +1049,21 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
|
||||
message: `已创建终端 "${terminal.name}"`,
|
||||
})
|
||||
|
||||
const cmd = buildCcCommand({ profilePath: effectiveProfilePath, prompt })
|
||||
terminal.sendText(cmd)
|
||||
terminal.sendText(buildCcCommand({
|
||||
profilePath: effectiveProfilePath,
|
||||
resumeSessionId: continuePath === 'resume' ? sessionId : undefined,
|
||||
prompt,
|
||||
}))
|
||||
logger.add({
|
||||
level: 'info',
|
||||
source: 'brainstorm',
|
||||
message: `已发送规划 prompt 到终端 #${issueNumber}`,
|
||||
message: continuePath === 'resume'
|
||||
? `已带续聊 prompt 恢复 #${issueNumber} 规划会话 ${sessionId}`
|
||||
: `已发送规划 prompt 到终端 #${issueNumber}`,
|
||||
})
|
||||
|
||||
// Fire-and-forget: when the session jsonl materializes, persist the id
|
||||
// as the issue's brainstorm `sessionId` so the X / Play button toggles
|
||||
// and the user can later resume via the existing UI.
|
||||
watchPromise.then(async (sid) => {
|
||||
// 拿到 session jsonl 后把 id 写回 state JSON,详情面板据此切到 resume 入口。
|
||||
watchPromise?.then(async (sid) => {
|
||||
if (!sid) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
@@ -1104,7 +1099,7 @@ export async function handleStartBrainstormSession(panel: KanbanWebviewPanel, is
|
||||
* 手动启动一个"测试"cc 会话:工单 PR 合并后,用户在详情面板点测试会话行的
|
||||
* 启动按钮,开一个 claude tab 让 cc 先了解代码、再告诉用户怎么测试。
|
||||
*
|
||||
* 与 handleStartBrainstormSession 的差异:
|
||||
* 与 handleContinueBrainstorm 的差异:
|
||||
* - 首条 prompt 依赖 state JSON 里的 `pr`(合并的 PR 号);没有 PR 直接 toast 拒绝。
|
||||
* - cwd 优先用 worktree(存在时),否则退回 workspaceRoot——测试本质要读真实代码,
|
||||
* profile 走 resolveTestProfilePath(testProfilePath > 默认,不回退实施)。
|
||||
|
||||
@@ -56,19 +56,25 @@ export function injectIntoImplTerminal(panel: KanbanWebviewPanel, issueNumber: n
|
||||
}
|
||||
if (!terminal)
|
||||
return false
|
||||
// cc 的 TUI 在 raw 模式下,LF (\n) 只算输入框内的换行,CR (\r)
|
||||
// 才会被识别为 Enter(提交消息)。实测把多行内容 + 末尾 \r 在同一次
|
||||
// sendText 里发出去时,cc 进入多行输入模式后并不会把紧跟的 \r 当成
|
||||
// 提交键,结果就是反馈只粘贴到输入框、没提交。
|
||||
// 拆两次发:先把内容完整推进输入框,250ms 后再独立发一个 \r 作为 Enter。
|
||||
const body = `\n[审查反馈]\n${text}`
|
||||
terminal.sendText(body, false)
|
||||
setTimeout(() => {
|
||||
terminal!.sendText('\r', false)
|
||||
}, 250)
|
||||
submitToSessionTerminal(terminal, `\n[审查反馈]\n${text}`)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 往正在跑 cc / codex TUI 的终端里提交一条消息。
|
||||
*
|
||||
* TUI 在 raw 模式下,LF (\n) 只算输入框内的换行,CR (\r) 才会被识别为
|
||||
* Enter。实测多行内容 + 末尾 \r 在同一次 sendText 里发出去时,TUI 进入
|
||||
* 多行输入模式后不会把紧跟的 \r 当成提交键,内容只粘贴到输入框、没提交。
|
||||
* 所以拆两次发:先把内容完整推进输入框,250ms 后再独立发一个 \r 作为 Enter。
|
||||
*/
|
||||
export function submitToSessionTerminal(terminal: Terminal, text: string): void {
|
||||
terminal.sendText(text, false)
|
||||
setTimeout(() => {
|
||||
terminal.sendText('\r', false)
|
||||
}, 250)
|
||||
}
|
||||
|
||||
/**
|
||||
* 本机是否开着该工单的存活终端(exitStatus undefined)——webhook 门禁对
|
||||
* 无 assignee 存量工单的兜底证据。不依赖 panel:看板关闭时实施终端
|
||||
|
||||
@@ -220,7 +220,7 @@ export type WebviewToExtension
|
||||
| { type: 'env-lock/toggle' }
|
||||
| { type: 'issue/delete', issueNumber: number }
|
||||
| { type: 'issue/close', issueNumber: number }
|
||||
| { type: 'brainstorm/start', issueNumber: number }
|
||||
| { type: 'brainstorm/continue', issueNumber: number }
|
||||
| { type: 'profiles/get' }
|
||||
| { type: 'profiles/save', data: ProfilesData }
|
||||
| { type: 'profiles/open', value: string }
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { buildCcCommand } from '../../src/cc/ccCommand'
|
||||
|
||||
vi.mock('../../src/cc/claudeBin', () => ({
|
||||
resolveClaudeBin: () => 'claude',
|
||||
}))
|
||||
|
||||
describe('buildCcCommand', () => {
|
||||
it('appends the prompt as a positional arg for a fresh session', () => {
|
||||
expect(buildCcCommand({ profilePath: '/p.json', prompt: 'hello' }))
|
||||
.toBe('claude --dangerously-skip-permissions --settings \'/p.json\' \'hello\'')
|
||||
})
|
||||
|
||||
it('resumes without a prompt when only a session id is given', () => {
|
||||
expect(buildCcCommand({ profilePath: '/p.json', resumeSessionId: 'abc' }))
|
||||
.toBe('claude --dangerously-skip-permissions --settings \'/p.json\' --resume abc')
|
||||
})
|
||||
|
||||
it('resumes and hands the prompt to the restored session', () => {
|
||||
expect(buildCcCommand({ profilePath: '/p.json', resumeSessionId: 'abc', prompt: 'go on' }))
|
||||
.toBe('claude --dangerously-skip-permissions --settings \'/p.json\' --resume abc \'go on\'')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { pickBrainstormContinuePath } from '../../src/panel/brainstormContinue'
|
||||
|
||||
describe('pickBrainstormContinuePath', () => {
|
||||
it('types the prompt into the live terminal when the brainstorm tab is still open', () => {
|
||||
expect(pickBrainstormContinuePath({ liveTerminal: true, sessionId: 'abc' })).toBe('inject')
|
||||
})
|
||||
|
||||
it('resumes the recorded session with the prompt when the tab is closed', () => {
|
||||
expect(pickBrainstormContinuePath({ liveTerminal: false, sessionId: 'abc' })).toBe('resume')
|
||||
})
|
||||
|
||||
it('starts a fresh session when the issue has no brainstorm session yet', () => {
|
||||
expect(pickBrainstormContinuePath({ liveTerminal: false, sessionId: undefined })).toBe('fresh')
|
||||
})
|
||||
|
||||
it('prefers the live terminal even when no session id has been recorded yet', () => {
|
||||
expect(pickBrainstormContinuePath({ liveTerminal: true, sessionId: undefined })).toBe('inject')
|
||||
})
|
||||
})
|
||||
@@ -53,7 +53,7 @@ export function App() {
|
||||
deleteIssue,
|
||||
closeIssue,
|
||||
closeSessionTab,
|
||||
startBrainstormSession,
|
||||
continueBrainstorm,
|
||||
changeColumn,
|
||||
setDependency,
|
||||
clearDependency,
|
||||
@@ -381,7 +381,7 @@ export function App() {
|
||||
onCloseIssue={closeIssue}
|
||||
onResetToTodo={n => changeColumn(n, 'todo', selectedIssue?.source, selectedIssue?.externalId)}
|
||||
onCloseSessionTab={closeSessionTab}
|
||||
onStartBrainstormSession={startBrainstormSession}
|
||||
onContinueBrainstorm={continueBrainstorm}
|
||||
onUpdateAutoReview={updateIssueAutoReview}
|
||||
onUpdateProfilePath={updateIssueProfilePath}
|
||||
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
|
||||
|
||||
@@ -44,7 +44,7 @@ interface BottomTabsProps {
|
||||
onCloseIssue: (issueNumber: number) => void
|
||||
onResetToTodo: (issueNumber: number) => void
|
||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
|
||||
onStartBrainstormSession: (issueNumber: number) => void
|
||||
onContinueBrainstorm: (issueNumber: number) => void
|
||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
|
||||
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||
@@ -139,7 +139,7 @@ export function BottomTabs(props: BottomTabsProps) {
|
||||
onCloseIssue={props.onCloseIssue}
|
||||
onResetToTodo={props.onResetToTodo}
|
||||
onCloseSessionTab={props.onCloseSessionTab}
|
||||
onStartBrainstormSession={props.onStartBrainstormSession}
|
||||
onContinueBrainstorm={props.onContinueBrainstorm}
|
||||
onUpdateAutoReview={props.onUpdateAutoReview}
|
||||
onUpdateProfilePath={props.onUpdateProfilePath}
|
||||
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
||||
|
||||
@@ -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
|
||||
const secondaryActions: RowAction[] = []
|
||||
if (issue?.column !== 'done') {
|
||||
secondaryActions.push({
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: '续聊:把续聊提示词发给头脑风暴会话',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onContinueBrainstorm(issue.number)
|
||||
},
|
||||
})
|
||||
}
|
||||
if (hasSession && issue?.brainstormTabOpen) {
|
||||
secondaryActionIcon = <X className="size-3.5" />
|
||||
secondaryActionTitle = '关闭头脑风暴 tab'
|
||||
onSecondaryAction = () => {
|
||||
secondaryActions.push({
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭头脑风暴 tab',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'brainstorm')
|
||||
}
|
||||
}
|
||||
else if (canStart) {
|
||||
secondaryActionIcon = <Play className="size-3.5" />
|
||||
secondaryActionTitle = '启动头脑风暴会话'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onStartBrainstormSession(issue.number)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
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 = () => {
|
||||
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 = () => {
|
||||
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 : '实施此计划',
|
||||
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.
|
||||
// 锁定态(前置工单未完成)同样禁止启动实施。
|
||||
secondaryDisabled:
|
||||
!issue?.planFile
|
||||
|| issue?.implementStatus === 'running'
|
||||
|| issue?.implementStatus === 'done'
|
||||
disabled:
|
||||
issue.implementStatus === 'running'
|
||||
|| issue.implementStatus === 'done'
|
||||
|| locked,
|
||||
onSecondaryAction: () => {
|
||||
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
|
||||
secondaryActions: issue?.pr && !issue?.prDiffFile
|
||||
? [{
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: prDiffSummaryRunning
|
||||
? 'PR 变更摘要生成中'
|
||||
: '生成 PR 变更摘要',
|
||||
secondaryDisabled: prDiffSummaryRunning,
|
||||
onSecondaryAction: () => {
|
||||
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: () => {
|
||||
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>(() => {
|
||||
|
||||
@@ -66,14 +66,41 @@ export interface PropertyDef {
|
||||
onReload?: () => void
|
||||
/** type='file-link' 专用:重新加载按钮的图标,默认为 RefreshCw */
|
||||
reloadIcon?: ReactNode
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮的图标,缺省则不渲染该按钮 */
|
||||
secondaryActionIcon?: ReactNode
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮的 tooltip */
|
||||
secondaryActionTitle?: string
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮的点击回调 */
|
||||
onSecondaryAction?: () => void
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮是否禁用 */
|
||||
secondaryDisabled?: boolean
|
||||
/** type='file-link' / 'action' / 'pr-link' / 只读 string 通用:值右侧的图标按钮,按数组顺序渲染 */
|
||||
secondaryActions?: RowAction[]
|
||||
}
|
||||
|
||||
/** 属性行右侧的图标按钮 */
|
||||
export interface RowAction {
|
||||
icon: ReactNode
|
||||
/** tooltip,同时作为 aria-label */
|
||||
title?: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function RowActionButtons({ actions, className }: { actions: RowAction[] | undefined, className?: string }): ReactNode {
|
||||
if (!actions || actions.length === 0)
|
||||
return null
|
||||
return actions.map((action, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (action.disabled)
|
||||
return
|
||||
action.onClick()
|
||||
}}
|
||||
disabled={action.disabled}
|
||||
title={action.title}
|
||||
aria-label={action.title ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${className ?? ''} ${
|
||||
action.disabled ? 'cursor-not-allowed opacity-30' : 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{action.icon}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
|
||||
/** 属性分组定义 */
|
||||
@@ -294,8 +321,7 @@ function PropertyRow({
|
||||
)
|
||||
|
||||
case 'action': {
|
||||
const hasSecondaryAction = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabledAction = !!propDef.secondaryDisabled
|
||||
const hasSecondaryAction = (propDef.secondaryActions?.length ?? 0) > 0
|
||||
const hasValue = value != null && value !== ''
|
||||
// Empty-value branch: still render the secondary button when defined
|
||||
// (e.g. "Play" to start a brainstorm session for an issue that has
|
||||
@@ -329,26 +355,7 @@ function PropertyRow({
|
||||
: (
|
||||
<span className="min-w-0 flex-1 truncate px-2 py-1.5 text-xs opacity-50">—</span>
|
||||
)}
|
||||
{hasSecondaryAction && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabledAction)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabledAction}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`mr-2 grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabledAction
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
)}
|
||||
<RowActionButtons actions={propDef.secondaryActions} className="mr-2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -356,8 +363,6 @@ function PropertyRow({
|
||||
case 'file-link': {
|
||||
const hasValue = typeof value === 'string' && value.length > 0
|
||||
const reloadIcon = propDef.reloadIcon ?? <RefreshCw className="size-3.5" />
|
||||
const hasSecondary = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabled = !!propDef.secondaryDisabled
|
||||
return (
|
||||
<div className="flex w-full items-center gap-1.5 px-2 py-1.5">
|
||||
{hasValue
|
||||
@@ -389,34 +394,14 @@ function PropertyRow({
|
||||
{reloadIcon}
|
||||
</button>
|
||||
)}
|
||||
{hasSecondary && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabled)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabled}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabled
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
)}
|
||||
<RowActionButtons actions={propDef.secondaryActions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
case 'pr-link': {
|
||||
const hasValue = typeof value === 'string' && value.length > 0
|
||||
const hasSecondary = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabled = !!propDef.secondaryDisabled
|
||||
const hasSecondary = (propDef.secondaryActions?.length ?? 0) > 0
|
||||
if (!hasValue && !hasSecondary) {
|
||||
return (
|
||||
<span className="block truncate px-2 py-1.5 text-xs opacity-50">未关联 PR</span>
|
||||
@@ -442,26 +427,7 @@ function PropertyRow({
|
||||
: (
|
||||
<span className="min-w-0 flex-1 truncate text-xs opacity-50">未关联 PR</span>
|
||||
)}
|
||||
{hasSecondary && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabled)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabled}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabled
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
)}
|
||||
<RowActionButtons actions={propDef.secondaryActions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -469,30 +435,11 @@ function PropertyRow({
|
||||
case 'string':
|
||||
default:
|
||||
if (ro) {
|
||||
const hasSecondary = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabled = !!propDef.secondaryDisabled
|
||||
if (hasSecondary) {
|
||||
if ((propDef.secondaryActions?.length ?? 0) > 0) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-1.5 px-2 py-1.5">
|
||||
<span className="min-w-0 flex-1 truncate text-xs">{String(value ?? '')}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabled)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabled}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabled
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
<RowActionButtons actions={propDef.secondaryActions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ export interface UseIssuesResult {
|
||||
* watches `~/.claude/projects/<encoded-workspaceRoot>` for the new
|
||||
* session jsonl, then writes the id back as `sessionId`.
|
||||
*/
|
||||
startBrainstormSession: (issueNumber: number) => void
|
||||
continueBrainstorm: (issueNumber: number) => void
|
||||
changeColumn: (issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string) => void
|
||||
setDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
||||
clearDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
||||
@@ -562,8 +562,8 @@ export function useIssues(): UseIssuesResult {
|
||||
[],
|
||||
)
|
||||
|
||||
const startBrainstormSession = useCallback((issueNumber: number): void => {
|
||||
postMessage({ type: 'brainstorm/start', issueNumber })
|
||||
const continueBrainstorm = useCallback((issueNumber: number): void => {
|
||||
postMessage({ type: 'brainstorm/continue', issueNumber })
|
||||
}, [])
|
||||
|
||||
const changeColumn = useCallback((issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string): void => {
|
||||
@@ -894,5 +894,5 @@ export function useIssues(): UseIssuesResult {
|
||||
return cleanup
|
||||
}, [clearPrDiffSummaryRunning, markHandoffRunning])
|
||||
|
||||
return { state, settings, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, 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, startBrainstormSession, 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, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, 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 }
|
||||
}
|
||||
@@ -249,7 +249,7 @@ export type WebviewToExtension
|
||||
| { type: 'env-lock/toggle' }
|
||||
| { type: 'issue/delete', issueNumber: number }
|
||||
| { type: 'issue/close', issueNumber: number }
|
||||
| { type: 'brainstorm/start', issueNumber: number }
|
||||
| { type: 'brainstorm/continue', issueNumber: number }
|
||||
| { type: 'profiles/get' }
|
||||
| { type: 'profiles/save', data: ProfilesData }
|
||||
| { type: 'profiles/open', value: string }
|
||||
|
||||
Reference in new issue
Block a user