Compare commits
7
Commits
5bfd759822
...
1ed4da5e47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1ed4da5e47 | ||
|
|
410a04da77 | ||
|
|
620b7c73c8 | ||
|
|
743e0e7325 | ||
|
|
3d9f0e02c6 | ||
|
|
70b826123c | ||
|
|
b182e0ac5b |
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
||||
"publisher": "clurdra",
|
||||
"name": "superpowers-vscode-clurdra",
|
||||
"displayName": "Superpowers-clurdra",
|
||||
"version": "0.2.131",
|
||||
"version": "0.2.135",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"description": "Superpowers specs and plans Kanban explorer",
|
||||
"author": "clurdra",
|
||||
|
||||
@@ -439,6 +439,10 @@ export class KanbanWebviewPanel {
|
||||
void sessions.handleResumeReviewSession(this, msg.sessionId, msg.issueNumber, msg.cwd)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'session/start-review') {
|
||||
void sessions.handleStartReviewSession(this, msg.issueNumber)
|
||||
return
|
||||
}
|
||||
if (msg.type === 'session/start-test') {
|
||||
void sessions.handleStartTestSession(this, msg.issueNumber)
|
||||
return
|
||||
@@ -696,7 +700,10 @@ export class KanbanWebviewPanel {
|
||||
* Persistence failures are logged and swallowed.
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
async resolveIssueIcon(issueNumber: number): Promise<{ themeColor: ThemeColor, iconUri: Uri }> {
|
||||
async resolveIssueIcon(
|
||||
issueNumber: number,
|
||||
presetState?: Record<string, unknown> | null,
|
||||
): Promise<{ themeColor: ThemeColor, iconUri: Uri }> {
|
||||
// Helper: produce both representations from a palette id.
|
||||
const pack = (id: string) => ({
|
||||
themeColor: new ThemeColor(id),
|
||||
@@ -707,24 +714,34 @@ export class KanbanWebviewPanel {
|
||||
// pick so the icon is still stable across sessions.
|
||||
const fallbackId = PALETTE[((issueNumber % PALETTE.length) + PALETTE.length) % PALETTE.length]
|
||||
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return pack(fallbackId)
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote)
|
||||
return pack(fallbackId)
|
||||
const token = await getToken(this.context, remote.host)
|
||||
if (!token)
|
||||
return pack(fallbackId)
|
||||
|
||||
let stored: string | undefined
|
||||
try {
|
||||
const state = await this.readIssueState(issueNumber)
|
||||
if (typeof state.color === 'string' && state.color.length > 0)
|
||||
stored = state.color
|
||||
// 调用方已经读过 state(开会话热路径就一次)时直接复用,省一次远程往返;
|
||||
// presetState 为 null 表示那次读取失败,按 tracker 不可达处理。
|
||||
if (presetState !== undefined) {
|
||||
if (presetState === null)
|
||||
return pack(fallbackId)
|
||||
if (typeof presetState.color === 'string' && presetState.color.length > 0)
|
||||
stored = presetState.color
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[superpowers] failed to read state JSON for color:', err)
|
||||
else {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return pack(fallbackId)
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (!remote)
|
||||
return pack(fallbackId)
|
||||
const token = await getToken(this.context, remote.host)
|
||||
if (!token)
|
||||
return pack(fallbackId)
|
||||
|
||||
try {
|
||||
const state = await this.readIssueState(issueNumber)
|
||||
if (typeof state.color === 'string' && state.color.length > 0)
|
||||
stored = state.color
|
||||
}
|
||||
catch (err) {
|
||||
console.warn('[superpowers] failed to read state JSON for color:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const { id, isNew } = resolveIssueColor(stored)
|
||||
@@ -871,6 +888,15 @@ export class KanbanWebviewPanel {
|
||||
* `{ locked: false }` so a transient Gitea hiccup can't lock the user out
|
||||
* of starting a session.
|
||||
*/
|
||||
/**
|
||||
* 主进程上一次算好的工单快照(issues/update 的 payload)。锁检查等只读
|
||||
* 判断直接用它,省掉一次全量 loadIssues 往返;面板还没加载过时为 undefined。
|
||||
*/
|
||||
// internal: handler 模块访问
|
||||
snapshotIssues(): Issue[] | undefined {
|
||||
return this.lastIssuesUpdate?.issues
|
||||
}
|
||||
|
||||
// internal: handler 模块访问
|
||||
async resolveLockedReason(
|
||||
issueNumber: number,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { IssueColumn } from '../../gitea/types'
|
||||
import type { LockedReason } from '../issueLock'
|
||||
import type { KanbanWebviewPanel } from '../KanbanPanel'
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
@@ -35,6 +36,7 @@ import { logger } from '../../logging/logger'
|
||||
import { getEffectivePrDiffSummaryProfilePath, getSettings } from '../../settings/store'
|
||||
import { webhookCoordinator } from '../../webhook/coordinator'
|
||||
import { pickRandomIssueColor, themeColorIdToIconUri } from '../issueColor'
|
||||
import { evaluateLockedReason } from '../issueLock'
|
||||
import { makeNonce } from '../KanbanPanel'
|
||||
import { cleanupFeatureBranch } from './branchCleanup'
|
||||
import * as sessions from './sessions'
|
||||
@@ -1572,12 +1574,10 @@ export async function syncCloseGiteaIssue(panel: KanbanWebviewPanel, opts: {
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side lock check for prerequisite gating. Fetches a fresh issues
|
||||
* snapshot (we don't trust webview state) and reports whether
|
||||
* `issueNumber` is blocked by an unfinished prerequisite. Mirrors the
|
||||
* webview-side `isIssueLocked` predicate: locked iff the issue has a
|
||||
* prerequisite that exists in the current snapshot and is not yet in the
|
||||
* `done` column.
|
||||
* Server-side lock check for prerequisite gating(不信 webview 传来的状态)。
|
||||
* 优先用主进程自己算好的工单快照——开会话是热路径,全量 loadIssues 要拉工单
|
||||
* 列表、每张工单的评论、依赖和 PR,几秒起步;快照没有该工单(面板还没加载
|
||||
* 完)时才退回一次全量拉取。
|
||||
*
|
||||
* Fail-open: any error fetching issues is logged and we return
|
||||
* `{ locked: false }` so a transient Gitea hiccup can't lock the user out
|
||||
@@ -1586,8 +1586,12 @@ export async function syncCloseGiteaIssue(panel: KanbanWebviewPanel, opts: {
|
||||
export async function resolveLockedReason(
|
||||
panel: KanbanWebviewPanel,
|
||||
issueNumber: number,
|
||||
): Promise<{ locked: boolean, prerequisiteNumber?: number, prerequisiteColumn?: string }> {
|
||||
): Promise<LockedReason> {
|
||||
try {
|
||||
const snapshot = panel.snapshotIssues()
|
||||
if (snapshot?.some(i => i.number === issueNumber))
|
||||
return evaluateLockedReason(snapshot, issueNumber)
|
||||
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return { locked: false }
|
||||
@@ -1604,21 +1608,7 @@ export async function resolveLockedReason(
|
||||
repo: remote.repo,
|
||||
workspaceRoot,
|
||||
})
|
||||
const issue = issues.find(i => i.number === issueNumber)
|
||||
if (!issue)
|
||||
return { locked: false }
|
||||
if (issue.prerequisite === undefined)
|
||||
return { locked: false }
|
||||
const prereq = issues.find(i => i.number === issue.prerequisite)
|
||||
if (!prereq)
|
||||
return { locked: false }
|
||||
if (prereq.column === 'done')
|
||||
return { locked: false }
|
||||
return {
|
||||
locked: true,
|
||||
prerequisiteNumber: prereq.number,
|
||||
prerequisiteColumn: prereq.column,
|
||||
}
|
||||
return evaluateLockedReason(issues, issueNumber)
|
||||
}
|
||||
catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err)
|
||||
|
||||
@@ -65,14 +65,17 @@ export interface CreateCcSessionTabOptions {
|
||||
cwd?: string
|
||||
/** 有 panel 时登记存活终端并推全量列表;panel 外触发(如 URI handler)传 null。 */
|
||||
panel: KanbanWebviewPanel | null
|
||||
/** 是否把会话登记进 managed store(会话管理列表);委派类一次性会话传 false。 */
|
||||
register: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 cc 会话终端 tab 并登记 managed store(核心逻辑,不要求 panel):
|
||||
* 创建 cc 会话终端 tab(核心逻辑,不要求 panel):
|
||||
* - 启动命令统一由 buildCcCommand 构建(claude --dangerously-skip-permissions
|
||||
* --settings '<profilePath>'),prompt 非空时作为位置参数传入。
|
||||
* - 用 pollForNewSession 捕获新 sessionId(共享 projects 目录用轮询更可靠),
|
||||
* - register 为 true 时用 pollForNewSession 捕获新 sessionId(共享 projects 目录用轮询更可靠),
|
||||
* 落进 workspaceRoot 的 .spx/session-names.json;有 panel 时再登记终端并推全量列表。
|
||||
* - register 为 false 时只开终端,不落 store、不进会话管理列表。
|
||||
*
|
||||
* 校验失败时 showErrorMessage 并返回 undefined;成功返回终端名。
|
||||
*/
|
||||
@@ -120,7 +123,10 @@ export async function createCcSessionTab(opts: CreateCcSessionTabOptions): Promi
|
||||
}
|
||||
|
||||
// managed 会话的 projects 目录是繁忙共享目录,fs.watch 会漏 rename;改用轮询 diff。
|
||||
const watchPromise = pollForNewSession({ projectsDir: projDir, timeoutMs: 120_000 })
|
||||
// 不登记的会话无需捕获 sessionId,省掉这趟轮询。
|
||||
const watchPromise = opts.register
|
||||
? pollForNewSession({ projectsDir: projDir, timeoutMs: 120_000 })
|
||||
: null
|
||||
|
||||
// 终端名:用户填了会话名字就用它,否则用默认 cc-会话。
|
||||
// (VS Code 终端创建后不能改名,所以只在 createTerminal 时设定。)
|
||||
@@ -140,7 +146,7 @@ export async function createCcSessionTab(opts: CreateCcSessionTabOptions): Promi
|
||||
terminal.sendText(cmd)
|
||||
|
||||
// Fire-and-forget:会话 jsonl 出现后写进 store;有 panel 再登记终端并推全量列表。
|
||||
watchPromise.then(async (sid) => {
|
||||
watchPromise?.then(async (sid) => {
|
||||
if (!sid) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
@@ -191,7 +197,7 @@ export async function handleManagedSessionsCreate(panel: KanbanWebviewPanel, pro
|
||||
return
|
||||
}
|
||||
|
||||
const created = await createCcSessionTab({ workspaceRoot, profilePath, name, prompt, panel })
|
||||
const created = await createCcSessionTab({ workspaceRoot, profilePath, name, prompt, panel, register: true })
|
||||
if (!created)
|
||||
return
|
||||
|
||||
@@ -313,9 +319,17 @@ export async function handleManagedSessionsDelete(panel: KanbanWebviewPanel, ses
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot)
|
||||
return
|
||||
const data = await readManagedSessions(workspaceRoot)
|
||||
const target = data.sessions.find(s => s.id === sessionId)
|
||||
const choice = await window.showWarningMessage(
|
||||
`确定从会话列表删除「${target?.name ?? sessionId}」?会关掉它的终端 tab,列表记录不可恢复(cc 会话本身的历史仍在 ~/.claude 里)。`,
|
||||
{ modal: true },
|
||||
'删除',
|
||||
)
|
||||
if (choice !== '删除')
|
||||
return
|
||||
// 删除前先关掉该会话的终端 tab。
|
||||
panel.managedTerminals.get(sessionId)?.dispose()
|
||||
const data = await readManagedSessions(workspaceRoot)
|
||||
const next = { sessions: data.sessions.filter(s => s.id !== sessionId) }
|
||||
await writeManagedSessions(workspaceRoot, next)
|
||||
panel.managedTerminals.delete(sessionId)
|
||||
|
||||
@@ -13,7 +13,7 @@ import { buildCcCommand } from '../../cc/ccCommand'
|
||||
import { buildCodexCommandString } from '../../cc/codexCommand'
|
||||
import { watchForNewCodexSession } from '../../cc/codexSessionWatcher'
|
||||
import { resolveProfilePath } from '../../cc/profiles'
|
||||
import { getBrainstormContinuePrompt, getImplementPlanPrompt } from '../../cc/prompts'
|
||||
import { getBrainstormContinuePrompt, getImplementPlanPrompt, getReviewPrompt } from '../../cc/prompts'
|
||||
import { projectsDirFor, watchForNewSession } from '../../cc/sessionWatcher'
|
||||
import { detectRepo } from '../../git/remote'
|
||||
import { ensureWorktree, resolveWorktreePath } from '../../git/worktree'
|
||||
@@ -73,6 +73,20 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
existing.show(false)
|
||||
return
|
||||
}
|
||||
// state JSON 一次读、两处用(钩子的 branch + 终端图标颜色)。提前发起,
|
||||
// 让这趟远程往返和后面的本地准备工作重叠;读失败为 null,两处各自退化。
|
||||
const statePromise: Promise<Record<string, unknown> | null> | undefined
|
||||
= issueNumber !== undefined
|
||||
? panel.readIssueState(issueNumber).catch((err) => {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: `resume #${issueNumber} 读 state JSON 失败`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
return null
|
||||
})
|
||||
: undefined
|
||||
// 实施 / 头脑风暴各自独立:调用方传入对应字段,空 = DEFAULT。
|
||||
const effectiveProfilePath
|
||||
= sessionKind === 'implement'
|
||||
@@ -127,28 +141,10 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
// (issueNumber === undefined) 钩子签名要 issueNumber,同样跳过。
|
||||
if (sessionKind === 'implement' && !worktreeMissing && issueNumber !== undefined && workspaceRoot) {
|
||||
const settingsForHook = getSettings(panel.context)
|
||||
// 从 state JSON 读 branch;任何一步读不到就用空字符串(脚本里
|
||||
// BRANCH env 仍能传,只是为空),不阻塞 resume 主流程。
|
||||
let branchForHook = ''
|
||||
try {
|
||||
const remote = await detectRepo(workspaceRoot)
|
||||
if (remote) {
|
||||
const token = await getToken(panel.context, remote.host)
|
||||
if (token) {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
if (stateObj && typeof stateObj.branch === 'string')
|
||||
branchForHook = stateObj.branch
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'panel',
|
||||
message: `resume #${issueNumber} 读 state JSON 失败,钩子 branch 用空字符串`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
// 从 state JSON 读 branch;读不到就用空字符串(脚本里 BRANCH env 仍能
|
||||
// 传,只是为空),不阻塞 resume 主流程。
|
||||
const stateObj = await statePromise
|
||||
const branchForHook = typeof stateObj?.branch === 'string' ? stateObj.branch : ''
|
||||
await panel.dispatchWorktreeHook('impl-tab-pre-create', {
|
||||
workspaceRoot,
|
||||
worktreePath: effectiveCwd!,
|
||||
@@ -164,7 +160,7 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
// harmless in the editor tab, still useful if the same terminal ever
|
||||
// gets rendered in panel-view mode.
|
||||
const issueIcon = issueNumber !== undefined
|
||||
? await panel.resolveIssueIcon(issueNumber)
|
||||
? await panel.resolveIssueIcon(issueNumber, await statePromise)
|
||||
: undefined
|
||||
const terminal = window.createTerminal({
|
||||
name: terminalName,
|
||||
@@ -198,6 +194,8 @@ export async function handleResumeSession(panel: KanbanWebviewPanel, sessionId:
|
||||
finally {
|
||||
if (lockKey !== undefined)
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
// 无论开成、复用已有 tab 还是被前置锁拒绝,都要告诉 webview 收 spinner。
|
||||
panel.postMessage({ type: 'session/tab-settled', sessionId })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +301,7 @@ export async function handleResumeReviewSession(panel: KanbanWebviewPanel, sessi
|
||||
}
|
||||
finally {
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
panel.postMessage({ type: 'session/tab-settled', sessionId })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1271,6 +1270,57 @@ export async function handleStartTestSession(panel: KanbanWebviewPanel, issueNum
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动启动审查会话:等同 PR 事件触发的自动审查,只是入口在详情面板的
|
||||
* 「审查会话id」行。必须已有 PR——审查提示词就是让 codex 去看这个 PR。
|
||||
*/
|
||||
export async function handleStartReviewSession(panel: KanbanWebviewPanel, issueNumber: number): Promise<void> {
|
||||
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||
if (!workspaceRoot) {
|
||||
void window.showErrorMessage('请先打开一个工作区文件夹')
|
||||
return
|
||||
}
|
||||
|
||||
const lockCheck = await panel.resolveLockedReason(issueNumber)
|
||||
if (lockCheck.locked) {
|
||||
await window.showWarningMessage(`等待前置工单 #${lockCheck.prerequisiteNumber} 完成`)
|
||||
return
|
||||
}
|
||||
|
||||
let pr: string | undefined
|
||||
let worktreePath = ''
|
||||
try {
|
||||
const stateObj = await panel.readIssueState(issueNumber)
|
||||
if (typeof stateObj.pr === 'string' && stateObj.pr.length > 0)
|
||||
pr = stateObj.pr.replace(/\(已合并\)\s*$/, '')
|
||||
if (typeof stateObj.worktreePath === 'string')
|
||||
worktreePath = stateObj.worktreePath
|
||||
}
|
||||
catch (err) {
|
||||
logger.add({
|
||||
level: 'warn',
|
||||
source: 'review',
|
||||
message: `读取 #${issueNumber} state JSON 失败,无法启动审查会话`,
|
||||
details: err instanceof Error ? err.message : String(err),
|
||||
})
|
||||
}
|
||||
|
||||
if (!pr) {
|
||||
void window.showWarningMessage('该工单还没有 PR,无法启动审查会话')
|
||||
return
|
||||
}
|
||||
|
||||
const ok = await triggerAutoReviewTab(panel, {
|
||||
issueNumber,
|
||||
prNumber: pr,
|
||||
prompt: getReviewPrompt(panel.context, { prNumber: pr }),
|
||||
worktreePath,
|
||||
workspaceRoot,
|
||||
})
|
||||
if (!ok)
|
||||
void window.showErrorMessage(`启动审查会话失败 #${issueNumber},详见扩展日志`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复一个已存在的测试 cc 会话:详情面板点测试会话行(testTabOpen=false 时)
|
||||
* 触发,照搬 handleResumeSession 的 implement 分支,但 kind='test'、终端名
|
||||
@@ -1379,6 +1429,7 @@ export async function handleResumeTestSession(panel: KanbanWebviewPanel, session
|
||||
}
|
||||
finally {
|
||||
panel.resumeInFlight.delete(lockKey)
|
||||
panel.postMessage({ type: 'session/tab-settled', sessionId })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Issue } from '../gitea/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { evaluateLockedReason } from './issueLock'
|
||||
|
||||
function issue(number: number, column: Issue['column'], prerequisite?: number): Issue {
|
||||
return { number, title: `#${number}`, column, prerequisite } as Issue
|
||||
}
|
||||
|
||||
describe('evaluateLockedReason', () => {
|
||||
it('没有前置 → 放行', () => {
|
||||
expect(evaluateLockedReason([issue(1, 'todo')], 1)).toEqual({ locked: false })
|
||||
})
|
||||
|
||||
it('前置未完成 → 锁定并带上前置号和所在列', () => {
|
||||
const issues = [issue(2, 'todo', 1), issue(1, 'in-progress')]
|
||||
expect(evaluateLockedReason(issues, 2)).toEqual({
|
||||
locked: true,
|
||||
prerequisiteNumber: 1,
|
||||
prerequisiteColumn: 'in-progress',
|
||||
})
|
||||
})
|
||||
|
||||
it('前置已完成 → 放行', () => {
|
||||
const issues = [issue(2, 'todo', 1), issue(1, 'done')]
|
||||
expect(evaluateLockedReason(issues, 2)).toEqual({ locked: false })
|
||||
})
|
||||
|
||||
it('快照里没有前置工单 → 放行(不误锁)', () => {
|
||||
expect(evaluateLockedReason([issue(2, 'todo', 99)], 2)).toEqual({ locked: false })
|
||||
})
|
||||
|
||||
it('快照里没有这张工单 → 放行', () => {
|
||||
expect(evaluateLockedReason([issue(1, 'todo')], 7)).toEqual({ locked: false })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Issue } from '../gitea/types'
|
||||
|
||||
export interface LockedReason {
|
||||
locked: boolean
|
||||
prerequisiteNumber?: number
|
||||
prerequisiteColumn?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯判定:工单被未完成的前置挡住才算锁定。快照里找不到工单或前置一律放行,
|
||||
* 与 webview 的 `isIssueLocked` 保持同一语义。
|
||||
*/
|
||||
export function evaluateLockedReason(issues: Issue[], issueNumber: number): LockedReason {
|
||||
const issue = issues.find(i => i.number === issueNumber)
|
||||
if (!issue || issue.prerequisite === undefined)
|
||||
return { locked: false }
|
||||
const prereq = issues.find(i => i.number === issue.prerequisite)
|
||||
if (!prereq || prereq.column === 'done')
|
||||
return { locked: false }
|
||||
return {
|
||||
locked: true,
|
||||
prerequisiteNumber: prereq.number,
|
||||
prerequisiteColumn: prereq.column,
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,8 @@ export type ExtensionToWebview
|
||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
||||
| { type: 'issues/error', message: string }
|
||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
||||
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
|
||||
| { type: 'session/tab-settled', sessionId: string }
|
||||
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
||||
| { type: 'handoff/users-result', issueNumber: number, users: string[] }
|
||||
| { type: 'handoff/done', issueNumber: number }
|
||||
@@ -191,6 +193,7 @@ export type WebviewToExtension
|
||||
| { type: 'session/focus', issueNumber: number }
|
||||
| { type: 'session/resume-review', sessionId: string, issueNumber: number, cwd?: string }
|
||||
| { type: 'session/resume-test', sessionId: string, issueNumber: number, cwd?: string }
|
||||
| { type: 'session/start-review', issueNumber: number }
|
||||
| { type: 'session/start-test', issueNumber: number }
|
||||
| { type: 'editor/open-file', path: string }
|
||||
| { type: 'profiles/list' }
|
||||
|
||||
@@ -14,6 +14,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
|
||||
'session/focus',
|
||||
'session/resume-review',
|
||||
'session/resume-test',
|
||||
'session/start-review',
|
||||
'session/start-test',
|
||||
'session/close-tab',
|
||||
'brainstorm/continue',
|
||||
|
||||
@@ -81,6 +81,7 @@ async function handleCreateSessionUri(uri: Uri): Promise<void> {
|
||||
prompt,
|
||||
cwd,
|
||||
panel: null,
|
||||
register: false,
|
||||
})
|
||||
if (!created)
|
||||
return
|
||||
|
||||
@@ -40,6 +40,8 @@ export function App() {
|
||||
dismissToast,
|
||||
openUrl,
|
||||
resumeSession,
|
||||
isSessionTabPending,
|
||||
startReviewSession,
|
||||
resumeReviewSession,
|
||||
startTestSession,
|
||||
resumeTestSession,
|
||||
@@ -373,6 +375,8 @@ export function App() {
|
||||
globalAutoReview={globalAutoReview}
|
||||
onOpenInBrowser={openUrl}
|
||||
onResumeSession={resumeSession}
|
||||
isSessionTabPending={isSessionTabPending}
|
||||
onStartReviewSession={startReviewSession}
|
||||
onResumeReviewSession={resumeReviewSession}
|
||||
onStartTestSession={startTestSession}
|
||||
onResumeTestSession={resumeTestSession}
|
||||
|
||||
@@ -29,7 +29,9 @@ interface BottomTabsProps {
|
||||
globalAutoReview: boolean
|
||||
onOpenInBrowser: (url: string) => void
|
||||
onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
|
||||
isSessionTabPending: (sessionId: string) => boolean
|
||||
onResumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
||||
onStartReviewSession: (issueNumber: number) => void
|
||||
onStartTestSession: (issueNumber: number) => void
|
||||
onResumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
||||
onOpenFile: (path: string) => void
|
||||
@@ -127,7 +129,9 @@ export function BottomTabs(props: BottomTabsProps) {
|
||||
globalAutoReview={props.globalAutoReview}
|
||||
onOpenInBrowser={props.onOpenInBrowser}
|
||||
onResumeSession={props.onResumeSession}
|
||||
isSessionTabPending={props.isSessionTabPending}
|
||||
onResumeReviewSession={props.onResumeReviewSession}
|
||||
onStartReviewSession={props.onStartReviewSession}
|
||||
onStartTestSession={props.onStartTestSession}
|
||||
onResumeTestSession={props.onResumeTestSession}
|
||||
onOpenFile={props.onOpenFile}
|
||||
|
||||
@@ -39,6 +39,8 @@ interface IssueDetailPanelProps {
|
||||
globalAutoReview: boolean
|
||||
onOpenInBrowser: (url: string) => void
|
||||
onResumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
|
||||
/** 会话 id 行的 spinner:该会话的开 tab 请求还没被扩展处理完就为 true。 */
|
||||
isSessionTabPending: (sessionId: string) => boolean
|
||||
/**
|
||||
* Open a new terminal that runs `codex resume <id>` for the auto-review
|
||||
* conversation associated with this issue.
|
||||
@@ -53,6 +55,7 @@ interface IssueDetailPanelProps {
|
||||
* Spawn a fresh "测试" cc tab for an issue whose PR has merged. Bound to the
|
||||
* Play button next to the "测试会话 id" row (only shown when `issue.pr` exists).
|
||||
*/
|
||||
onStartReviewSession: (issueNumber: number) => void
|
||||
onStartTestSession: (issueNumber: number) => void
|
||||
/** Open a workspace-relative file in the editor. */
|
||||
onOpenFile: (path: string) => void
|
||||
@@ -131,6 +134,7 @@ export function IssueDetailPanel({
|
||||
onResumeSession,
|
||||
onResumeReviewSession,
|
||||
onResumeTestSession,
|
||||
onStartReviewSession,
|
||||
onStartTestSession,
|
||||
onOpenFile,
|
||||
onImplement,
|
||||
@@ -153,6 +157,7 @@ export function IssueDetailPanel({
|
||||
onUpdateTestProfilePath,
|
||||
onOpenLogs,
|
||||
me,
|
||||
isSessionTabPending,
|
||||
onStartHandoff,
|
||||
onAcceptHandoff,
|
||||
isHandoffRunning,
|
||||
@@ -252,6 +257,7 @@ export function IssueDetailPanel({
|
||||
key: 'sessionId',
|
||||
label: '头脑风暴会话id',
|
||||
type: 'action' as const,
|
||||
pending: hasSession ? isSessionTabPending(issue!.sessionId!) : false,
|
||||
description: hasSession
|
||||
? '双击在新终端运行 claude --resume <id> 恢复对话;点 ▶ 续聊'
|
||||
: '工单尚未关联 cc 会话;点 ▶ 新开一个规划会话并续聊',
|
||||
@@ -274,6 +280,7 @@ export function IssueDetailPanel({
|
||||
key: 'implementSessionId',
|
||||
label: '实施会话id',
|
||||
type: 'action',
|
||||
pending: issue?.implementSessionId ? isSessionTabPending(issue.implementSessionId) : false,
|
||||
description: issue?.worktreeExists
|
||||
? '双击在新终端运行 claude --resume <id> 恢复实施对话(cwd 为 worktree)'
|
||||
: 'worktree 已清理,将在工作区根目录恢复(cc 可能提示原 cwd 不存在)',
|
||||
@@ -285,19 +292,34 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeSession(v, 'implement', issue?.profilePath, issue?.worktreePath, issue?.number)
|
||||
},
|
||||
secondaryActions: issue?.implementTabOpen
|
||||
? [{
|
||||
secondaryActions: (() => {
|
||||
if (issue?.implementTabOpen) {
|
||||
return [{
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭实施 tab',
|
||||
onClick: () => onCloseSessionTab(issue.number, 'implement'),
|
||||
}]
|
||||
: [],
|
||||
}
|
||||
// 还没实施会话时给个 ▶,省去把卡片拖进「实施中」列。
|
||||
if (issue?.implementSessionId || !issue?.planFile)
|
||||
return []
|
||||
return [{
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: locked ? lockTitle : '启动实施会话',
|
||||
disabled:
|
||||
issue.implementStatus === 'running'
|
||||
|| issue.implementStatus === 'done'
|
||||
|| locked,
|
||||
onClick: () => onImplement(issue.number, issue.planFile ?? '', issue.profilePath, issue.sessionId),
|
||||
}]
|
||||
})(),
|
||||
},
|
||||
issue?.reviewSessionFileExists
|
||||
? {
|
||||
key: 'reviewSessionId',
|
||||
label: '审查会话id',
|
||||
type: 'action',
|
||||
pending: issue.reviewSessionId ? isSessionTabPending(issue.reviewSessionId) : false,
|
||||
description: '双击在新终端运行 codex resume <id> 查看审查会话(worktree 在则用之,否则工作区根目录)',
|
||||
actionIcon: <Terminal className="size-3.5" />,
|
||||
onAction: (v) => {
|
||||
@@ -318,9 +340,19 @@ export function IssueDetailPanel({
|
||||
: {
|
||||
key: 'reviewSessionId',
|
||||
label: '审查会话id',
|
||||
type: 'string',
|
||||
type: 'action',
|
||||
readOnly: true,
|
||||
description: '本机没有该 codex 会话文件,无法 resume;仅保留 id 文本',
|
||||
description: issue?.reviewSessionId
|
||||
? '本机没有该 codex 会话文件,无法 resume;点 ▶ 对当前 PR 重新起一个审查会话'
|
||||
: '还没有审查会话;点 ▶ 对当前 PR 启动 codex 审查(等同 PR 事件自动触发的那次)',
|
||||
secondaryActions: issue?.pr
|
||||
? [{
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: locked ? lockTitle : '启动审查会话',
|
||||
disabled: locked,
|
||||
onClick: () => onStartReviewSession(issue.number),
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
(() => {
|
||||
// 测试会话 id 行有三种状态(照搬头脑风暴行的三态):
|
||||
@@ -355,6 +387,7 @@ export function IssueDetailPanel({
|
||||
key: 'testSessionId',
|
||||
label: '测试会话id',
|
||||
type: 'action' as const,
|
||||
pending: hasSession ? isSessionTabPending(issue!.testSessionId!) : false,
|
||||
description: hasSession
|
||||
? '双击在新终端运行 claude --resume <id> 在工作区根(main)恢复测试对话'
|
||||
: '工单 PR 合并后点击右侧 ▶ 在工作区根(main)启动一个测试 cc tab(让 cc 了解代码后告诉你怎么测试)',
|
||||
@@ -535,7 +568,7 @@ export function IssueDetailPanel({
|
||||
],
|
||||
},
|
||||
],
|
||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onClearBrainstormSession, 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, isSessionTabPending, onStartReviewSession],
|
||||
)
|
||||
|
||||
// 同事的工单只读:属性表统一消毒,顶部只留查看类按钮。
|
||||
|
||||
@@ -60,6 +60,11 @@ export interface PropertyDef {
|
||||
onAction?: (value: unknown) => void
|
||||
/** type='action' 专用:值前的图标 */
|
||||
actionIcon?: ReactNode
|
||||
/**
|
||||
* 主动作是否还在进行中。给了就由外部决定 spinner 何时收(真实进度),
|
||||
* 不给则沿用点击后固定 1.5s 的即时反馈。
|
||||
*/
|
||||
pending?: boolean
|
||||
/** type='file-link' 专用:双击文件名时调用,参数为 value(workspace 相对路径) */
|
||||
onOpen?: (path: string) => void
|
||||
/** type='file-link' 专用:点击右侧图标按钮时调用,用于重新扫描/加载 */
|
||||
@@ -174,7 +179,8 @@ function PropertyRow({
|
||||
registerCellRef: (fullKey: string, el: HTMLDivElement | null) => void
|
||||
}) {
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [actionPending, setActionPending] = useState(false)
|
||||
const [localActionPending, setLocalActionPending] = useState(false)
|
||||
const actionPending = propDef.pending ?? localActionPending
|
||||
const pendingTimerRef = useRef<number | null>(null)
|
||||
const ro = propDef.readOnly
|
||||
|
||||
@@ -183,14 +189,16 @@ function PropertyRow({
|
||||
window.clearTimeout(pendingTimerRef.current)
|
||||
}, [])
|
||||
|
||||
/** 触发主动作并给出即时反馈:行内图标临时切换为 spinner,约 1.5s 后恢复 */
|
||||
/** 触发主动作:外部受控时 spinner 交给 propDef.pending,否则本地转约 1.5s */
|
||||
function firePrimaryAction(fire: () => void): void {
|
||||
fire()
|
||||
setActionPending(true)
|
||||
if (propDef.pending !== undefined)
|
||||
return
|
||||
setLocalActionPending(true)
|
||||
if (pendingTimerRef.current != null)
|
||||
window.clearTimeout(pendingTimerRef.current)
|
||||
pendingTimerRef.current = window.setTimeout(() => {
|
||||
setActionPending(false)
|
||||
setLocalActionPending(false)
|
||||
pendingTimerRef.current = null
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
@@ -184,7 +184,10 @@ export interface UseIssuesResult {
|
||||
dismissToast: (id: string) => void
|
||||
openUrl: (url: string) => void
|
||||
resumeSession: (sessionId: string, kind: 'implement' | 'brainstorm', profilePath?: string, cwd?: string, issueNumber?: number) => void
|
||||
/** 该会话 id 的开 tab 请求还没被扩展处理完 → 行内转 spinner。 */
|
||||
isSessionTabPending: (sessionId: string) => boolean
|
||||
resumeReviewSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
||||
startReviewSession: (issueNumber: number) => void
|
||||
startTestSession: (issueNumber: number) => void
|
||||
resumeTestSession: (sessionId: string, issueNumber: number, cwd?: string) => void
|
||||
focusSession: (issueNumber: number) => void
|
||||
@@ -479,6 +482,50 @@ export function useIssues(): UseIssuesResult {
|
||||
postMessage({ type: 'toast/open-url', url })
|
||||
}, [])
|
||||
|
||||
// 开 tab 是秒级操作(要读 state JSON、跑 pre-create 钩子),行内 spinner 要
|
||||
// 真实反映进度:发请求时标 pending,扩展回 session/tab-settled 时清。消息万一
|
||||
// 丢了就靠 PENDING_TIMEOUT_MS 兜底,spinner 不会永远转下去。
|
||||
const [pendingSessionTabs, setPendingSessionTabs] = useState<Set<string>>(() => new Set())
|
||||
const pendingTimersRef = useRef<Map<string, number>>(new Map())
|
||||
const PENDING_TIMEOUT_MS = 60_000
|
||||
|
||||
const clearSessionTabPending = useCallback((sessionId: string): void => {
|
||||
const timer = pendingTimersRef.current.get(sessionId)
|
||||
if (timer !== undefined) {
|
||||
window.clearTimeout(timer)
|
||||
pendingTimersRef.current.delete(sessionId)
|
||||
}
|
||||
setPendingSessionTabs((prev) => {
|
||||
if (!prev.has(sessionId))
|
||||
return prev
|
||||
const next = new Set(prev)
|
||||
next.delete(sessionId)
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
const markSessionTabPending = useCallback((sessionId: string): void => {
|
||||
const existing = pendingTimersRef.current.get(sessionId)
|
||||
if (existing !== undefined)
|
||||
window.clearTimeout(existing)
|
||||
pendingTimersRef.current.set(
|
||||
sessionId,
|
||||
window.setTimeout(() => clearSessionTabPending(sessionId), PENDING_TIMEOUT_MS),
|
||||
)
|
||||
setPendingSessionTabs(prev => new Set(prev).add(sessionId))
|
||||
}, [clearSessionTabPending])
|
||||
|
||||
const isSessionTabPending = useCallback(
|
||||
(sessionId: string): boolean => pendingSessionTabs.has(sessionId),
|
||||
[pendingSessionTabs],
|
||||
)
|
||||
|
||||
useEffect(() => () => {
|
||||
for (const timer of pendingTimersRef.current.values())
|
||||
window.clearTimeout(timer)
|
||||
pendingTimersRef.current.clear()
|
||||
}, [])
|
||||
|
||||
const resumeSession = useCallback((
|
||||
sessionId: string,
|
||||
kind: 'implement' | 'brainstorm',
|
||||
@@ -486,15 +533,21 @@ export function useIssues(): UseIssuesResult {
|
||||
cwd?: string,
|
||||
issueNumber?: number,
|
||||
): void => {
|
||||
markSessionTabPending(sessionId)
|
||||
postMessage({ type: 'session/resume', sessionId, kind, profilePath, cwd, issueNumber })
|
||||
}, [])
|
||||
}, [markSessionTabPending])
|
||||
|
||||
const focusSession = useCallback((issueNumber: number): void => {
|
||||
postMessage({ type: 'session/focus', issueNumber })
|
||||
}, [])
|
||||
|
||||
const resumeReviewSession = useCallback((sessionId: string, issueNumber: number, cwd?: string): void => {
|
||||
markSessionTabPending(sessionId)
|
||||
postMessage({ type: 'session/resume-review', sessionId, issueNumber, cwd })
|
||||
}, [markSessionTabPending])
|
||||
|
||||
const startReviewSession = useCallback((issueNumber: number): void => {
|
||||
postMessage({ type: 'session/start-review', issueNumber })
|
||||
}, [])
|
||||
|
||||
const startTestSession = useCallback((issueNumber: number): void => {
|
||||
@@ -502,8 +555,9 @@ export function useIssues(): UseIssuesResult {
|
||||
}, [])
|
||||
|
||||
const resumeTestSession = useCallback((sessionId: string, issueNumber: number, cwd?: string): void => {
|
||||
markSessionTabPending(sessionId)
|
||||
postMessage({ type: 'session/resume-test', sessionId, issueNumber, cwd })
|
||||
}, [])
|
||||
}, [markSessionTabPending])
|
||||
|
||||
const openFile = useCallback((path: string): void => {
|
||||
postMessage({ type: 'editor/open-file', path })
|
||||
@@ -722,6 +776,9 @@ export function useIssues(): UseIssuesResult {
|
||||
useEffect(() => {
|
||||
const cleanup = onMessage((msg) => {
|
||||
switch (msg.type) {
|
||||
case 'session/tab-settled':
|
||||
clearSessionTabPending(msg.sessionId)
|
||||
break
|
||||
case 'issues/loading':
|
||||
setRefreshing(true)
|
||||
setState(prev => prev.status === 'ready' ? prev : { status: 'loading' })
|
||||
@@ -938,5 +995,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, 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 }
|
||||
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, 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 }
|
||||
}
|
||||
@@ -84,6 +84,8 @@ export type ExtensionToWebview
|
||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
||||
| { type: 'issues/error', message: string }
|
||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
||||
/** 某个会话 id 的开 tab 请求已处理完(开成、复用或被拒都算),webview 据此停掉行内 spinner。 */
|
||||
| { type: 'session/tab-settled', sessionId: string }
|
||||
| { type: 'issue/pr-diff-summary-done', issueNumber: number }
|
||||
| { type: 'handoff/users-result', issueNumber: number, users: string[] }
|
||||
| { type: 'session-handoff/users-result', users: string[] }
|
||||
@@ -222,6 +224,7 @@ export type WebviewToExtension
|
||||
| { type: 'session/focus', issueNumber: number }
|
||||
| { type: 'session/resume-review', sessionId: string, issueNumber: number, cwd?: string }
|
||||
| { type: 'session/resume-test', sessionId: string, issueNumber: number, cwd?: string }
|
||||
| { type: 'session/start-review', issueNumber: number }
|
||||
| { type: 'session/start-test', issueNumber: number }
|
||||
| { type: 'editor/open-file', path: string }
|
||||
| { type: 'profiles/list' }
|
||||
|
||||
Reference in new issue
Block a user