Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4480f936f3 | ||
|
|
a1fac829f8 | ||
|
|
5bcbb87152 | ||
|
|
4094ffa49e | ||
|
|
20249d6a08 | ||
|
|
38d06153bb | ||
|
|
12d9080248 |
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"publisher": "clurdra",
|
"publisher": "clurdra",
|
||||||
"name": "superpowers-vscode-clurdra",
|
"name": "superpowers-vscode-clurdra",
|
||||||
"displayName": "Superpowers-clurdra",
|
"displayName": "Superpowers-clurdra",
|
||||||
"version": "0.2.232",
|
"version": "0.2.235",
|
||||||
"packageManager": "pnpm@10.27.0",
|
"packageManager": "pnpm@10.27.0",
|
||||||
"description": "Superpowers specs and plans Kanban explorer",
|
"description": "Superpowers specs and plans Kanban explorer",
|
||||||
"author": "clurdra",
|
"author": "clurdra",
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
/ecc:figma-plugin-prototyping 给 {{issueNumber}} 号工单画原型:先读工单正文锁定要画的界面和状态,再写本地 Figma 插件把画板和点击流做出来。画完汇报插件路径和怎么导入,不要改业务代码。
|
/ecc:ui-prototyping 给 {{issueNumber}} 号工单画原型。
|
||||||
|
|
||||||
画完用 `opencli spx issue marker --issue {{issueNumber}} --type prototype --value <manifest.json 的仓库相对路径>` 把原型插件挂到工单上,再汇报。
|
先读工单正文,锁定要确认的界面和状态。画法按这个技能来:工单正文已经指定 Figma 插件或 OpenPencil 就用指定的那个;没指定就先问用哪一个,再按技能里对应的做法画。不要改业务代码。
|
||||||
|
|
||||||
|
画完把原型源文件的仓库相对路径挂到工单上:
|
||||||
|
|
||||||
|
```
|
||||||
|
opencli spx issue marker --issue {{issueNumber}} --type prototype --value <路径>
|
||||||
|
```
|
||||||
|
|
||||||
|
Figma 插件填 `manifest.json` 的路径,OpenPencil 填画板文件的路径。挂完再汇报路径和怎么打开。
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
/pr-acceptance-testing 验收 {{issueNumber}} 号工单({{prRef}})。
|
/pr-acceptance-testing 验收 {{issueNumber}} 号工单({{prRef}})。
|
||||||
|
|
||||||
验收目标以工单正文为准,不要再问验收哪个。测出问题就停下报告,不要顺手改代码。
|
验收目标以工单正文为准,不要再问验收哪个。测出问题就停下报告,不要顺手改代码。
|
||||||
|
|
||||||
|
先看下目前有没有合进main 如果已经手动合过了 就发main到测试服验证 如果没合 就发当前pr对应的分支去测试服验证
|
||||||
@@ -11,6 +11,7 @@ import { setProfilesDirOverride } from './cc/profiles'
|
|||||||
import { registerCodeReader } from './codeReader'
|
import { registerCodeReader } from './codeReader'
|
||||||
import { logger } from './logging/logger'
|
import { logger } from './logging/logger'
|
||||||
import { KanbanWebviewPanel } from './panel/KanbanPanel'
|
import { KanbanWebviewPanel } from './panel/KanbanPanel'
|
||||||
|
import { offerPromptUpdates } from './settings/promptUpdate'
|
||||||
import { getSettings, migratePromptTemplates, persistSeededProfiles } from './settings/store'
|
import { getSettings, migratePromptTemplates, persistSeededProfiles } from './settings/store'
|
||||||
import { registerCreateSessionUriHandler } from './uri/createSessionUriHandler'
|
import { registerCreateSessionUriHandler } from './uri/createSessionUriHandler'
|
||||||
import { webhookCoordinator } from './webhook/coordinator'
|
import { webhookCoordinator } from './webhook/coordinator'
|
||||||
@@ -43,6 +44,7 @@ export function activate(context: ExtensionContext): SuperpowersApi {
|
|||||||
// 存量提示词先迁到 Handlebars,再让 persistSeededProfiles 在迁移结果上补 seed。
|
// 存量提示词先迁到 Handlebars,再让 persistSeededProfiles 在迁移结果上补 seed。
|
||||||
for (const note of migratePromptTemplates(context))
|
for (const note of migratePromptTemplates(context))
|
||||||
logger.add({ level: note.level, source: 'settings', message: note.message })
|
logger.add({ level: note.level, source: 'settings', message: note.message })
|
||||||
|
void offerPromptUpdates(context)
|
||||||
void persistSeededProfiles(context)
|
void persistSeededProfiles(context)
|
||||||
// Start the webhook server immediately so PR callbacks are received even
|
// Start the webhook server immediately so PR callbacks are received even
|
||||||
// when the Kanban panel is closed. Disposed automatically via subscriptions.
|
// when the Kanban panel is closed. Disposed automatically via subscriptions.
|
||||||
|
|||||||
@@ -598,6 +598,10 @@ export class KanbanWebviewPanel {
|
|||||||
void issues.handleUpdateAutoReview(this, msg.issueNumber, msg.value)
|
void issues.handleUpdateAutoReview(this, msg.issueNumber, msg.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'issue/update-review-passed') {
|
||||||
|
void issues.handleUpdateReviewPassed(this, msg.issueNumber, msg.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
if (msg.type === 'issue/update-profile-path') {
|
if (msg.type === 'issue/update-profile-path') {
|
||||||
void issues.handleUpdateProfilePath(this, msg.issueNumber, msg.profilePath)
|
void issues.handleUpdateProfilePath(this, msg.issueNumber, msg.profilePath)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest, handoffRequestNotices } from './handoffRequest'
|
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest, handoffRequestNotices, handoffRequestSystemNotice } from './handoffRequest'
|
||||||
|
|
||||||
const review = {
|
const review = {
|
||||||
source: 'gitea' as const,
|
source: 'gitea' as const,
|
||||||
@@ -37,6 +37,34 @@ describe('移交申请', () => {
|
|||||||
.toEqual(['#435 没交出来:会话太大'])
|
.toEqual(['#435 没交出来:会话太大'])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('系统通知只打给负责人,同一条申请不重复', () => {
|
||||||
|
expect(handoffRequestSystemNotice({
|
||||||
|
issueNumber: 435,
|
||||||
|
me: 'chw',
|
||||||
|
assignees: ['chw'],
|
||||||
|
from: 'cruldra',
|
||||||
|
})).toBe('cruldra 想要 #435')
|
||||||
|
expect(handoffRequestSystemNotice({
|
||||||
|
issueNumber: 435,
|
||||||
|
me: 'chw',
|
||||||
|
assignees: ['chw'],
|
||||||
|
from: 'cruldra',
|
||||||
|
seenFrom: 'cruldra',
|
||||||
|
})).toBeUndefined()
|
||||||
|
expect(handoffRequestSystemNotice({
|
||||||
|
issueNumber: 435,
|
||||||
|
me: 'cruldra',
|
||||||
|
assignees: ['chw'],
|
||||||
|
from: 'cruldra',
|
||||||
|
})).toBeUndefined()
|
||||||
|
expect(handoffRequestSystemNotice({
|
||||||
|
issueNumber: 435,
|
||||||
|
me: 'other',
|
||||||
|
assignees: ['chw'],
|
||||||
|
from: 'cruldra',
|
||||||
|
})).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('只有负责人能同意或拒绝', () => {
|
it('只有负责人能同意或拒绝', () => {
|
||||||
const pending = { ...review, handoffRequestFrom: 'cruldra' }
|
const pending = { ...review, handoffRequestFrom: 'cruldra' }
|
||||||
expect(canAnswerHandoffRequest(pending, 'chw')).toBe(true)
|
expect(canAnswerHandoffRequest(pending, 'chw')).toBe(true)
|
||||||
|
|||||||
@@ -50,6 +50,22 @@ export function handoffRequestNotices(before: HandoffRequestFacts & { number: nu
|
|||||||
return notices
|
return notices
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 负责人没开着看板也能看到的那一句。同一申请人不重复。 */
|
||||||
|
export function handoffRequestSystemNotice(opts: {
|
||||||
|
issueNumber: number
|
||||||
|
me?: string
|
||||||
|
assignees: string[]
|
||||||
|
from?: string
|
||||||
|
seenFrom?: string
|
||||||
|
}): string | undefined {
|
||||||
|
const from = opts.from?.trim()
|
||||||
|
if (!from || !opts.me || from === opts.me || from === opts.seenFrom)
|
||||||
|
return
|
||||||
|
if (!opts.assignees.includes(opts.me))
|
||||||
|
return
|
||||||
|
return `${from} 想要 #${opts.issueNumber}`
|
||||||
|
}
|
||||||
|
|
||||||
export function canAnswerHandoffRequest(issue: HandoffRequestFacts, me: string | undefined): boolean {
|
export function canAnswerHandoffRequest(issue: HandoffRequestFacts, me: string | undefined): boolean {
|
||||||
return !!me
|
return !!me
|
||||||
&& openReview(issue)
|
&& openReview(issue)
|
||||||
|
|||||||
@@ -1893,6 +1893,85 @@ export async function handleUpdateAutoReview(panel: KanbanWebviewPanel, issueNum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function handleUpdateReviewPassed(panel: KanbanWebviewPanel, issueNumber: number, value: boolean): Promise<void> {
|
||||||
|
const workspaceRoot = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
|
if (!workspaceRoot) {
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'toast/show',
|
||||||
|
id: makeNonce(),
|
||||||
|
level: 'error',
|
||||||
|
message: '请先打开一个工作区文件夹',
|
||||||
|
dismissOnTimer: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const remote = await detectRepo(workspaceRoot)
|
||||||
|
if (!remote) {
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'toast/show',
|
||||||
|
id: makeNonce(),
|
||||||
|
level: 'error',
|
||||||
|
message: '当前工作区没有 Gitea 远程仓库',
|
||||||
|
dismissOnTimer: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = await getToken(panel.context, remote.host)
|
||||||
|
if (!token) {
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'toast/show',
|
||||||
|
id: makeNonce(),
|
||||||
|
level: 'error',
|
||||||
|
message: '请先完成 Gitea 配置',
|
||||||
|
dismissOnTimer: 5000,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let previousValue: boolean | undefined
|
||||||
|
try {
|
||||||
|
const existingState = await panel.readIssueState(issueNumber)
|
||||||
|
previousValue = typeof existingState.reviewPassed === 'boolean'
|
||||||
|
? existingState.reviewPassed
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
previousValue = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await panel.mergeIssueState(issueNumber, { reviewPassed: value })
|
||||||
|
logger.add({
|
||||||
|
level: 'info',
|
||||||
|
source: 'panel',
|
||||||
|
message: `工单 #${issueNumber} reviewPassed=${value} 已持久化`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err)
|
||||||
|
logger.add({
|
||||||
|
level: 'error',
|
||||||
|
source: 'panel',
|
||||||
|
message: `持久化 reviewPassed 失败 (issue #${issueNumber})`,
|
||||||
|
details: message,
|
||||||
|
})
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'toast/show',
|
||||||
|
id: makeNonce(),
|
||||||
|
level: 'error',
|
||||||
|
message: `保存工单 #${issueNumber} 审查通过标记失败: ${message}`,
|
||||||
|
dismissOnTimer: 6000,
|
||||||
|
})
|
||||||
|
panel.postMessage({
|
||||||
|
type: 'issue/patch',
|
||||||
|
issueNumber,
|
||||||
|
patch: { reviewPassed: previousValue },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Persist a per-issue `profilePath` override into the issue's state JSON
|
* Persist a per-issue `profilePath` override into the issue's state JSON
|
||||||
* comment. Implement / implement-resume sessions use this profile
|
* comment. Implement / implement-resume sessions use this profile
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'dependency/set', issueNumber: number, prerequisiteNumber: number }
|
| { type: 'dependency/set', issueNumber: number, prerequisiteNumber: number }
|
||||||
| { type: 'dependency/clear', issueNumber: number, prerequisiteNumber: number }
|
| { type: 'dependency/clear', issueNumber: number, prerequisiteNumber: number }
|
||||||
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
||||||
|
| { type: 'issue/update-review-passed', 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-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }
|
| { type: 'issue/clear-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }
|
||||||
|
|||||||
@@ -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/update-review-passed')).toBe(true)
|
||||||
expect(isOwnerOnlyMessage('issue/clear-session')).toBe(true)
|
expect(isOwnerOnlyMessage('issue/clear-session')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const OWNER_ONLY_MESSAGE_TYPES: ReadonlySet<string> = new Set([
|
|||||||
'issue/close',
|
'issue/close',
|
||||||
'issue/generate-pr-diff-summary',
|
'issue/generate-pr-diff-summary',
|
||||||
'issue/update-auto-review',
|
'issue/update-auto-review',
|
||||||
|
'issue/update-review-passed',
|
||||||
'issue/update-profile-path',
|
'issue/update-profile-path',
|
||||||
'issue/update-brainstorm-profile-path',
|
'issue/update-brainstorm-profile-path',
|
||||||
'issue/clear-session',
|
'issue/clear-session',
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
import { promptDigest, promptUpdatesToAsk } from './promptUpdatePlan'
|
||||||
|
|
||||||
|
describe('promptUpdatesToAsk', () => {
|
||||||
|
it('和自带的不一样就问', () => {
|
||||||
|
const next = '新版原型提示词'
|
||||||
|
const out = promptUpdatesToAsk({
|
||||||
|
stored: { prototypePrompt: '旧的 Figma 插件提示词' },
|
||||||
|
defaults: { prototypePrompt: next },
|
||||||
|
decided: {},
|
||||||
|
})
|
||||||
|
expect(out).toEqual([{
|
||||||
|
key: 'prototypePrompt',
|
||||||
|
label: '原型提示词',
|
||||||
|
next,
|
||||||
|
hash: promptDigest(next),
|
||||||
|
}])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('空的、一样的、这一版已经选过的不问', () => {
|
||||||
|
const next = '新版'
|
||||||
|
expect(promptUpdatesToAsk({
|
||||||
|
stored: { testPrompt: '' },
|
||||||
|
defaults: { testPrompt: next },
|
||||||
|
decided: {},
|
||||||
|
})).toEqual([])
|
||||||
|
expect(promptUpdatesToAsk({
|
||||||
|
stored: { testPrompt: next },
|
||||||
|
defaults: { testPrompt: `${next}\n` },
|
||||||
|
decided: {},
|
||||||
|
})).toEqual([])
|
||||||
|
expect(promptUpdatesToAsk({
|
||||||
|
stored: { testPrompt: '我改过' },
|
||||||
|
defaults: { testPrompt: next },
|
||||||
|
decided: { testPrompt: promptDigest(next) },
|
||||||
|
})).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { ExtensionContext } from 'vscode'
|
||||||
|
import type { PromptUpdateKey } from './promptUpdatePlan'
|
||||||
|
import { window } from 'vscode'
|
||||||
|
import { PROMPT_UPDATE_FIELDS, promptUpdatesToAsk } from './promptUpdatePlan'
|
||||||
|
import { readDefaultPrompt, readStoredSettings, saveSettings } from './store'
|
||||||
|
|
||||||
|
function decisionsOf(stored: Record<string, unknown>): Record<string, string> {
|
||||||
|
const raw = stored.promptDefaultChoices
|
||||||
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
||||||
|
return {}
|
||||||
|
const out: Record<string, string> = {}
|
||||||
|
for (const [key, value] of Object.entries(raw)) {
|
||||||
|
if (typeof value === 'string')
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 自带提示词和设置里的不一样时问一句。选过的同一版不再问。 */
|
||||||
|
export async function offerPromptUpdates(ctx: ExtensionContext): Promise<void> {
|
||||||
|
const stored = readStoredSettings(ctx) as Record<string, unknown>
|
||||||
|
const defaults = Object.fromEntries(
|
||||||
|
PROMPT_UPDATE_FIELDS.map(field => [field.key, readDefaultPrompt(ctx.extensionPath, field.file)]),
|
||||||
|
) as Partial<Record<PromptUpdateKey, string>>
|
||||||
|
const pending = promptUpdatesToAsk({ stored, defaults, decided: decisionsOf(stored) })
|
||||||
|
if (pending.length === 0)
|
||||||
|
return
|
||||||
|
const choices = { ...decisionsOf(stored) }
|
||||||
|
const patch: Record<string, string> = {}
|
||||||
|
for (const item of pending) {
|
||||||
|
const pick = await window.showInformationMessage(
|
||||||
|
`${item.label}和这一版自带的不一样。要换成新版吗?`,
|
||||||
|
'使用新版',
|
||||||
|
'保留现有',
|
||||||
|
)
|
||||||
|
if (pick !== '使用新版' && pick !== '保留现有')
|
||||||
|
continue
|
||||||
|
choices[item.key] = item.hash
|
||||||
|
if (pick === '使用新版')
|
||||||
|
patch[item.key] = item.next
|
||||||
|
}
|
||||||
|
if (Object.keys(patch).length === 0 && Object.keys(choices).length === Object.keys(decisionsOf(stored)).length)
|
||||||
|
return
|
||||||
|
await saveSettings(ctx, { ...patch, promptDefaultChoices: choices })
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
|
||||||
|
export const PROMPT_UPDATE_FIELDS = [
|
||||||
|
{ key: 'brainstormPrompt', file: 'brainstorm', label: '头脑风暴提示词' },
|
||||||
|
{ key: 'implementPlanPrompt', file: 'implement-plan', label: '实施提示词' },
|
||||||
|
{ key: 'reviewPrompt', file: 'review', label: '审查提示词' },
|
||||||
|
{ key: 'testPrompt', file: 'test', label: '测试提示词' },
|
||||||
|
{ key: 'prototypePrompt', file: 'prototype', label: '原型提示词' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type PromptUpdateKey = typeof PROMPT_UPDATE_FIELDS[number]['key']
|
||||||
|
|
||||||
|
export interface PromptUpdateCandidate {
|
||||||
|
key: PromptUpdateKey
|
||||||
|
label: string
|
||||||
|
next: string
|
||||||
|
hash: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function promptDigest(text: string): string {
|
||||||
|
return createHash('sha256').update(text.replace(/\r\n/g, '\n').trim()).digest('hex')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 设置里有内容,且和这一版自带的不一样,并且这一版还没问过。 */
|
||||||
|
export function promptUpdatesToAsk(opts: {
|
||||||
|
stored: Record<string, unknown>
|
||||||
|
defaults: Partial<Record<PromptUpdateKey, string>>
|
||||||
|
decided: Record<string, string>
|
||||||
|
}): PromptUpdateCandidate[] {
|
||||||
|
const out: PromptUpdateCandidate[] = []
|
||||||
|
for (const field of PROMPT_UPDATE_FIELDS) {
|
||||||
|
const saved = opts.stored[field.key]
|
||||||
|
const next = opts.defaults[field.key]
|
||||||
|
if (typeof saved !== 'string' || saved.trim() === '')
|
||||||
|
continue
|
||||||
|
if (typeof next !== 'string' || next.trim() === '')
|
||||||
|
continue
|
||||||
|
if (promptDigest(saved) === promptDigest(next))
|
||||||
|
continue
|
||||||
|
const hash = promptDigest(next)
|
||||||
|
if (opts.decided[field.key] === hash)
|
||||||
|
continue
|
||||||
|
out.push({ key: field.key, label: field.label, next, hash })
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -101,6 +101,11 @@ export interface Settings {
|
|||||||
testPrompt: string
|
testPrompt: string
|
||||||
/** 原型会话首个提示词模板;常用 `{{issueNumber}}`。 */
|
/** 原型会话首个提示词模板;常用 `{{issueNumber}}`。 */
|
||||||
prototypePrompt: string
|
prototypePrompt: string
|
||||||
|
/**
|
||||||
|
* 某一版自带提示词用户已经选过「使用新版」或「保留现有」。
|
||||||
|
* 键是提示词字段,值是那一版自带正文的摘要。正文再变就再问。
|
||||||
|
*/
|
||||||
|
promptDefaultChoices?: Record<string, string>
|
||||||
/** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */
|
/** Day-to-day development branch (e.g. `main`). Jenkins doesn't watch it. */
|
||||||
devBranch: string
|
devBranch: string
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { hasReviewMarker, parseClosingIssueNumber, stripReviewMarker } from '../
|
|||||||
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
import { hasStateColumn, mergeStateJsonComment, mergeStateJsonCommentGuarded, readStateJsonComment } from '../gitea/stateJson'
|
||||||
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
import { getLocalIssueState, mergeLocalIssueState, overlayLocalIssueState } from '../issues/localState'
|
||||||
import { logger } from '../logging/logger'
|
import { logger } from '../logging/logger'
|
||||||
|
import { handoffRequestSystemNotice } from '../panel/handlers/handoffRequest'
|
||||||
import * as issueChat from '../panel/handlers/issueChat'
|
import * as issueChat from '../panel/handlers/issueChat'
|
||||||
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
|
import { hasLiveIssueSessionTerminal } from '../panel/handlers/terminals'
|
||||||
import { DEFAULT_KANBAN_SCOPE, KANBAN_SCOPE_KEY } from '../panel/kanbanScope'
|
import { DEFAULT_KANBAN_SCOPE, KANBAN_SCOPE_KEY } from '../panel/kanbanScope'
|
||||||
@@ -68,6 +69,16 @@ const BODY_MARKERS = [
|
|||||||
/** 本次从 body 里扫出来的 marker 值,按 state JSON 字段名索引。 */
|
/** 本次从 body 里扫出来的 marker 值,按 state JSON 字段名索引。 */
|
||||||
type MarkerPatch = { [K in typeof BODY_MARKERS[number]['field']]?: string }
|
type MarkerPatch = { [K in typeof BODY_MARKERS[number]['field']]?: string }
|
||||||
|
|
||||||
|
function handoffRequestFromOf(body: string): string {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(body) as { handoffRequestFrom?: unknown }
|
||||||
|
return typeof obj.handoffRequestFrom === 'string' ? obj.handoffRequestFrom.trim() : ''
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function parseBodyMarkers(body: string): MarkerPatch {
|
function parseBodyMarkers(body: string): MarkerPatch {
|
||||||
const out: MarkerPatch = {}
|
const out: MarkerPatch = {}
|
||||||
for (const { type, field, ext } of BODY_MARKERS) {
|
for (const { type, field, ext } of BODY_MARKERS) {
|
||||||
@@ -100,6 +111,8 @@ class WebhookCoordinator {
|
|||||||
private eventSubscription?: { dispose: () => void }
|
private eventSubscription?: { dispose: () => void }
|
||||||
/** 同 issue 自动审查启动中不重入(opened 与 synchronize 常连发)。 */
|
/** 同 issue 自动审查启动中不重入(opened 与 synchronize 常连发)。 */
|
||||||
private reviewInFlight = new Set<number>()
|
private reviewInFlight = new Set<number>()
|
||||||
|
/** 已经为这条移交申请通知过负责人。申请取消后清掉。 */
|
||||||
|
private handoffRequestSeen = new Map<number, string>()
|
||||||
|
|
||||||
/** Called once from extension.ts activate(). Idempotent. */
|
/** Called once from extension.ts activate(). Idempotent. */
|
||||||
init(ctx: ExtensionContext): void {
|
init(ctx: ExtensionContext): void {
|
||||||
@@ -1436,6 +1449,55 @@ class WebhookCoordinator {
|
|||||||
* first review and `injectIntoImplTerminal` appends the merge-to-main
|
* first review and `injectIntoImplTerminal` appends the merge-to-main
|
||||||
* suffix.
|
* suffix.
|
||||||
*/
|
*/
|
||||||
|
/** 申请移交时给负责人发系统通知。看板关着也发。 */
|
||||||
|
private async notifyHandoffRequest(event: IssueCommentWebhookEvent): Promise<void> {
|
||||||
|
const from = handoffRequestFromOf(event.commentBody)
|
||||||
|
if (!from) {
|
||||||
|
this.handoffRequestSeen.delete(event.issueNumber)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const seen = this.handoffRequestSeen.get(event.issueNumber) ?? ''
|
||||||
|
if (from === seen || !this.ctx)
|
||||||
|
return
|
||||||
|
const ws = workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
|
if (!ws)
|
||||||
|
return
|
||||||
|
const remote = await detectRepo(ws)
|
||||||
|
if (!remote)
|
||||||
|
return
|
||||||
|
const token = await getToken(this.ctx, remote.host)
|
||||||
|
if (!token)
|
||||||
|
return
|
||||||
|
const me = await loginForToken(remote.host, token)
|
||||||
|
const payload = event.raw as { issue?: { assignees?: Array<{ login?: string } | null> | null } | null } | null
|
||||||
|
const assignees = (payload?.issue?.assignees ?? []).flatMap(a => (a?.login ? [a.login] : []))
|
||||||
|
if (!me || assignees.length === 0)
|
||||||
|
return
|
||||||
|
const notice = handoffRequestSystemNotice({
|
||||||
|
issueNumber: event.issueNumber,
|
||||||
|
me,
|
||||||
|
assignees,
|
||||||
|
from,
|
||||||
|
seenFrom: seen,
|
||||||
|
})
|
||||||
|
this.handoffRequestSeen.set(event.issueNumber, from)
|
||||||
|
if (!notice)
|
||||||
|
return
|
||||||
|
execFile('osascript', [
|
||||||
|
'-e',
|
||||||
|
`display notification ${JSON.stringify('打开看板同意或拒绝')} with title ${JSON.stringify(notice)}`,
|
||||||
|
], (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.add({
|
||||||
|
level: 'warn',
|
||||||
|
source: 'webhook',
|
||||||
|
message: `移交申请通知发送失败 #${event.issueNumber}`,
|
||||||
|
details: err.message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private async handleIssueCommentCreated(event: IssueCommentWebhookEvent): Promise<void> {
|
private async handleIssueCommentCreated(event: IssueCommentWebhookEvent): Promise<void> {
|
||||||
if (event.action !== 'created') {
|
if (event.action !== 'created') {
|
||||||
logger.add({
|
logger.add({
|
||||||
@@ -1519,6 +1581,7 @@ class WebhookCoordinator {
|
|||||||
source: 'webhook',
|
source: 'webhook',
|
||||||
message: `issue_comment 是 state 评论,刷新 #${event.issueNumber} 的卡片`,
|
message: `issue_comment 是 state 评论,刷新 #${event.issueNumber} 的卡片`,
|
||||||
})
|
})
|
||||||
|
await this.notifyHandoffRequest(event)
|
||||||
await this.refreshIssueCard({
|
await this.refreshIssueCard({
|
||||||
issueNumber: event.issueNumber,
|
issueNumber: event.issueNumber,
|
||||||
raw: event.raw,
|
raw: event.raw,
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ export function App() {
|
|||||||
setDependency,
|
setDependency,
|
||||||
clearDependency,
|
clearDependency,
|
||||||
updateIssueAutoReview,
|
updateIssueAutoReview,
|
||||||
|
updateIssueReviewPassed,
|
||||||
updateIssueProfilePath,
|
updateIssueProfilePath,
|
||||||
updateIssueBrainstormProfilePath,
|
updateIssueBrainstormProfilePath,
|
||||||
updateIssueTestProfilePath,
|
updateIssueTestProfilePath,
|
||||||
@@ -399,6 +400,7 @@ export function App() {
|
|||||||
onColumnChange={changeColumn}
|
onColumnChange={changeColumn}
|
||||||
onDependencySet={setDependency}
|
onDependencySet={setDependency}
|
||||||
onDependencyClear={clearDependency}
|
onDependencyClear={clearDependency}
|
||||||
|
onSetReviewPassed={updateIssueReviewPassed}
|
||||||
me={me}
|
me={me}
|
||||||
avatars={avatars}
|
avatars={avatars}
|
||||||
/>
|
/>
|
||||||
@@ -435,6 +437,7 @@ export function App() {
|
|||||||
onContinueBrainstorm={continueBrainstorm}
|
onContinueBrainstorm={continueBrainstorm}
|
||||||
onClearSession={clearSession}
|
onClearSession={clearSession}
|
||||||
onUpdateAutoReview={updateIssueAutoReview}
|
onUpdateAutoReview={updateIssueAutoReview}
|
||||||
|
onUpdateReviewPassed={updateIssueReviewPassed}
|
||||||
onUpdateProfilePath={updateIssueProfilePath}
|
onUpdateProfilePath={updateIssueProfilePath}
|
||||||
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
|
onUpdateBrainstormProfilePath={updateIssueBrainstormProfilePath}
|
||||||
onUpdateTestProfilePath={updateIssueTestProfilePath}
|
onUpdateTestProfilePath={updateIssueTestProfilePath}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export interface BottomTabsProps {
|
|||||||
onContinueBrainstorm: (issueNumber: number) => void
|
onContinueBrainstorm: (issueNumber: number) => void
|
||||||
onClearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
onClearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
||||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||||
|
onUpdateReviewPassed: (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
|
||||||
onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void
|
onUpdateTestProfilePath: (issueNumber: number, testProfilePath: string) => void
|
||||||
@@ -171,6 +172,7 @@ export function BottomTabs(props: BottomTabsProps) {
|
|||||||
onContinueBrainstorm={props.onContinueBrainstorm}
|
onContinueBrainstorm={props.onContinueBrainstorm}
|
||||||
onClearSession={props.onClearSession}
|
onClearSession={props.onClearSession}
|
||||||
onUpdateAutoReview={props.onUpdateAutoReview}
|
onUpdateAutoReview={props.onUpdateAutoReview}
|
||||||
|
onUpdateReviewPassed={props.onUpdateReviewPassed}
|
||||||
onUpdateProfilePath={props.onUpdateProfilePath}
|
onUpdateProfilePath={props.onUpdateProfilePath}
|
||||||
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
||||||
onUpdateTestProfilePath={props.onUpdateTestProfilePath}
|
onUpdateTestProfilePath={props.onUpdateTestProfilePath}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Ref } from 'react'
|
|||||||
import type { Issue } from '../types'
|
import type { Issue } from '../types'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { Lock } from 'lucide-react'
|
import { Check, Lock } from 'lucide-react'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { Card } from './ui/card'
|
import { Card } from './ui/card'
|
||||||
@@ -26,6 +26,8 @@ interface IssueCardProps {
|
|||||||
ownerLogin?: string
|
ownerLogin?: string
|
||||||
/** 归属人头像 data URI;缺失时回退首字母圆点。 */
|
/** 归属人头像 data URI;缺失时回退首字母圆点。 */
|
||||||
ownerAvatar?: string
|
ownerAvatar?: string
|
||||||
|
/** 审查列:手动标通过或改回审查中。 */
|
||||||
|
onSetReviewPassed?: (issueNumber: number, value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 同事工单的归属人标记:有头像用头像,没有就用 login 首字母。 */
|
/** 同事工单的归属人标记:有头像用头像,没有就用 login 首字母。 */
|
||||||
@@ -52,7 +54,7 @@ function OwnerBadge({ login, avatar }: { login: string, avatar?: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
||||||
({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar }, ref) => {
|
({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar, onSetReviewPassed }, ref) => {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging }
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging }
|
||||||
= useSortable({ id: issue.id, data: { column: issue.column }, disabled: readOnly })
|
= useSortable({ id: issue.id, data: { column: issue.column }, disabled: readOnly })
|
||||||
|
|
||||||
@@ -72,6 +74,9 @@ export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
|||||||
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
(ref as React.MutableRefObject<HTMLDivElement | null>).current = node
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showMark = issue.column === 'review' && !readOnly && issue.source !== 'youtrack' && onSetReviewPassed != null
|
||||||
|
const cornerCount = (showMark ? 1 : 0) + (locked ? 1 : 0) + (readOnly && ownerLogin ? 1 : 0)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
ref={setRefs as unknown as Ref<HTMLDivElement>}
|
ref={setRefs as unknown as Ref<HTMLDivElement>}
|
||||||
@@ -91,6 +96,26 @@ export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
|||||||
: undefined}
|
: undefined}
|
||||||
>
|
>
|
||||||
<div className="absolute right-1.5 top-1.5 flex items-center gap-1">
|
<div className="absolute right-1.5 top-1.5 flex items-center gap-1">
|
||||||
|
{showMark
|
||||||
|
? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'grid size-3.5 place-items-center rounded-sm hover:bg-black/10 dark:hover:bg-white/10',
|
||||||
|
issue.reviewPassed === true ? 'text-green-400' : 'text-amber-400',
|
||||||
|
)}
|
||||||
|
data-tooltip={issue.reviewPassed === true ? '已通过,点击改回审查中' : '标记为已通过'}
|
||||||
|
aria-label={issue.reviewPassed === true ? '改回审查中' : '标记为已通过'}
|
||||||
|
onPointerDown={e => e.stopPropagation()}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onSetReviewPassed(issue.number, issue.reviewPassed !== true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check className="size-3" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
{locked
|
{locked
|
||||||
? (
|
? (
|
||||||
<span
|
<span
|
||||||
@@ -116,10 +141,12 @@ export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
|||||||
: null}
|
: null}
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
'truncate',
|
'truncate',
|
||||||
(locked || (readOnly && ownerLogin)) && 'pr-5',
|
cornerCount === 1 && 'pr-5',
|
||||||
locked && readOnly && ownerLogin && 'pr-10',
|
cornerCount === 2 && 'pr-10',
|
||||||
|
cornerCount >= 3 && 'pr-14',
|
||||||
issue.column === 'review' && (issue.reviewPassed === true ? 'text-green-400' : 'text-amber-400'),
|
issue.column === 'review' && (issue.reviewPassed === true ? 'text-green-400' : 'text-amber-400'),
|
||||||
)}>
|
)}
|
||||||
|
>
|
||||||
<span className="font-mono opacity-60">
|
<span className="font-mono opacity-60">
|
||||||
{issue.source === 'youtrack' ? issue.externalId : `#${issue.number}`}
|
{issue.source === 'youtrack' ? issue.externalId : `#${issue.number}`}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ interface IssueDetailPanelProps {
|
|||||||
onClearSession: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype') => void
|
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
|
||||||
|
/** 审查列手动标记通过。 */
|
||||||
|
onUpdateReviewPassed: (issueNumber: number, value: boolean) => void
|
||||||
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
|
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
|
||||||
profiles: ClaudeProfile[]
|
profiles: ClaudeProfile[]
|
||||||
/** Persist a per-issue `profilePath` override into the state JSON. */
|
/** Persist a per-issue `profilePath` override into the state JSON. */
|
||||||
@@ -180,6 +182,7 @@ export function IssueDetailPanel({
|
|||||||
onContinueBrainstorm,
|
onContinueBrainstorm,
|
||||||
onClearSession,
|
onClearSession,
|
||||||
onUpdateAutoReview,
|
onUpdateAutoReview,
|
||||||
|
onUpdateReviewPassed,
|
||||||
profiles,
|
profiles,
|
||||||
onUpdateProfilePath,
|
onUpdateProfilePath,
|
||||||
onUpdateBrainstormProfilePath,
|
onUpdateBrainstormProfilePath,
|
||||||
@@ -559,6 +562,14 @@ export function IssueDetailPanel({
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
description: `打开新 PR 时是否自动启动 codex 审查;未设置时跟随全局(当前全局:${globalAutoReview ? '开' : '关'})`,
|
description: `打开新 PR 时是否自动启动 codex 审查;未设置时跟随全局(当前全局:${globalAutoReview ? '开' : '关'})`,
|
||||||
},
|
},
|
||||||
|
...(issue?.column === 'review' && issue.source !== 'youtrack'
|
||||||
|
? [{
|
||||||
|
key: 'reviewPassed',
|
||||||
|
label: '审查通过',
|
||||||
|
type: 'boolean' as const,
|
||||||
|
description: '手动标成已通过,标题变绿。新的提交会清掉这个标记。',
|
||||||
|
}]
|
||||||
|
: []),
|
||||||
{
|
{
|
||||||
key: 'color',
|
key: 'color',
|
||||||
label: '颜色',
|
label: '颜色',
|
||||||
@@ -772,6 +783,7 @@ export function IssueDetailPanel({
|
|||||||
testSessionId: issue.testSessionId ?? null,
|
testSessionId: issue.testSessionId ?? null,
|
||||||
prototypeSessionId: issue.prototypeSessionId ?? null,
|
prototypeSessionId: issue.prototypeSessionId ?? null,
|
||||||
autoReview: issue.autoReview ?? globalAutoReview,
|
autoReview: issue.autoReview ?? globalAutoReview,
|
||||||
|
reviewPassed: issue.reviewPassed === true,
|
||||||
color: issue.color ?? null,
|
color: issue.color ?? null,
|
||||||
// 展示/选中值按 basename 映射到本机 profiles,避免跨机绝对路径变成「自定义」项。
|
// 展示/选中值按 basename 映射到本机 profiles,避免跨机绝对路径变成「自定义」项。
|
||||||
brainstormProfilePath: matchProfileOption(issue.brainstormProfilePath, profiles) || null,
|
brainstormProfilePath: matchProfileOption(issue.brainstormProfilePath, profiles) || null,
|
||||||
@@ -1008,6 +1020,8 @@ export function IssueDetailPanel({
|
|||||||
return
|
return
|
||||||
if (key === 'autoReview' && issue && typeof value === 'boolean')
|
if (key === 'autoReview' && issue && typeof value === 'boolean')
|
||||||
onUpdateAutoReview(issue.number, value)
|
onUpdateAutoReview(issue.number, value)
|
||||||
|
if (key === 'reviewPassed' && issue && typeof value === 'boolean')
|
||||||
|
onUpdateReviewPassed(issue.number, value)
|
||||||
if (key === 'brainstormProfilePath' && issue && typeof value === 'string')
|
if (key === 'brainstormProfilePath' && issue && typeof value === 'string')
|
||||||
onUpdateBrainstormProfilePath(issue.number, value)
|
onUpdateBrainstormProfilePath(issue.number, value)
|
||||||
if (key === 'profilePath' && issue && typeof value === 'string')
|
if (key === 'profilePath' && issue && typeof value === 'string')
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface KanbanBoardProps {
|
|||||||
onColumnChange?: (issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string) => void
|
onColumnChange?: (issueNumber: number, toColumn: IssueColumn, source?: 'gitea' | 'youtrack', externalId?: string) => void
|
||||||
onDependencySet?: (issueNumber: number, prerequisiteNumber: number) => void
|
onDependencySet?: (issueNumber: number, prerequisiteNumber: number) => void
|
||||||
onDependencyClear?: (issueNumber: number, prerequisiteNumber: number) => void
|
onDependencyClear?: (issueNumber: number, prerequisiteNumber: number) => void
|
||||||
|
onSetReviewPassed?: (issueNumber: number, value: boolean) => void
|
||||||
/** 当前 Gitea login;用于判定哪些卡是同事的(只读)。 */
|
/** 当前 Gitea login;用于判定哪些卡是同事的(只读)。 */
|
||||||
me?: string
|
me?: string
|
||||||
/** Gitea login → 头像 data URI。 */
|
/** Gitea login → 头像 data URI。 */
|
||||||
@@ -117,6 +118,7 @@ export function KanbanBoard({
|
|||||||
onColumnChange,
|
onColumnChange,
|
||||||
onDependencySet,
|
onDependencySet,
|
||||||
onDependencyClear,
|
onDependencyClear,
|
||||||
|
onSetReviewPassed,
|
||||||
me,
|
me,
|
||||||
avatars,
|
avatars,
|
||||||
}: KanbanBoardProps) {
|
}: KanbanBoardProps) {
|
||||||
@@ -281,6 +283,7 @@ export function KanbanBoard({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
ownerLogin={ownerLogin}
|
ownerLogin={ownerLogin}
|
||||||
ownerAvatar={ownerLogin ? avatars?.[ownerLogin] : undefined}
|
ownerAvatar={ownerLogin ? avatars?.[ownerLogin] : undefined}
|
||||||
|
onSetReviewPassed={onSetReviewPassed}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ export interface UseIssuesResult {
|
|||||||
setDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
setDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
||||||
clearDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
clearDependency: (issueNumber: number, prerequisiteNumber: number) => void
|
||||||
updateIssueAutoReview: (issueNumber: number, value: boolean) => void
|
updateIssueAutoReview: (issueNumber: number, value: boolean) => void
|
||||||
|
updateIssueReviewPassed: (issueNumber: number, value: boolean) => void
|
||||||
/**
|
/**
|
||||||
* 覆盖该工单实施会话使用的 Claude 配置文件,乐观更新本地 state 后
|
* 覆盖该工单实施会话使用的 Claude 配置文件,乐观更新本地 state 后
|
||||||
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
* postMessage 持久化到 state JSON。失败时扩展端 push `issue/patch` 回滚。
|
||||||
@@ -730,6 +731,20 @@ export function useIssues(): UseIssuesResult {
|
|||||||
postMessage({ type: 'issue/update-auto-review', issueNumber, value })
|
postMessage({ type: 'issue/update-auto-review', issueNumber, value })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const updateIssueReviewPassed = useCallback((issueNumber: number, value: boolean): void => {
|
||||||
|
setState((prev) => {
|
||||||
|
if (prev.status !== 'ready')
|
||||||
|
return prev
|
||||||
|
return {
|
||||||
|
status: 'ready',
|
||||||
|
issues: prev.issues.map(i =>
|
||||||
|
i.number === issueNumber ? { ...i, reviewPassed: value } : i,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
postMessage({ type: 'issue/update-review-passed', issueNumber, value })
|
||||||
|
}, [])
|
||||||
|
|
||||||
const updateIssueProfilePath = useCallback((issueNumber: number, profilePath: string): void => {
|
const updateIssueProfilePath = useCallback((issueNumber: number, profilePath: string): void => {
|
||||||
// Optimistically update local state so the dropdown reflects immediately
|
// Optimistically update local state so the dropdown reflects immediately
|
||||||
// without remounting the issues list / detail panel via a full reload.
|
// without remounting the issues list / detail panel via a full reload.
|
||||||
@@ -1097,5 +1112,5 @@ export function useIssues(): UseIssuesResult {
|
|||||||
])
|
])
|
||||||
}, [state, me])
|
}, [state, me])
|
||||||
|
|
||||||
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, pendingSelectTab, clearPendingSelectTab, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning, askHandoff, cancelHandoffRequest, acceptHandoffRequest, rejectHandoffRequest }
|
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, updateIssueReviewPassed, updateIssueProfilePath, updateIssueBrainstormProfilePath, clearSession, updateIssueTestProfilePath, updateIssuePrototypeProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, pendingSelectTab, clearPendingSelectTab, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning, askHandoff, cancelHandoffRequest, acceptHandoffRequest, rejectHandoffRequest }
|
||||||
}
|
}
|
||||||
@@ -267,6 +267,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'dependency/set', issueNumber: number, prerequisiteNumber: number }
|
| { type: 'dependency/set', issueNumber: number, prerequisiteNumber: number }
|
||||||
| { type: 'dependency/clear', issueNumber: number, prerequisiteNumber: number }
|
| { type: 'dependency/clear', issueNumber: number, prerequisiteNumber: number }
|
||||||
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
| { type: 'issue/update-auto-review', issueNumber: number, value: boolean }
|
||||||
|
| { type: 'issue/update-review-passed', 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-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }
|
| { type: 'issue/clear-session', issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test' | 'prototype' }
|
||||||
|
|||||||
Reference in new issue
Block a user