@cyber-dash-tech/revela 0.17.5 → 0.17.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -46
- package/README.zh-CN.md +26 -46
- package/bin/revela.ts +98 -0
- package/lib/commands/review.ts +8 -5
- package/lib/deck-html/foundation.ts +190 -0
- package/lib/edit/prompt.ts +6 -2
- package/lib/edit/server.ts +2 -2
- package/lib/inspect/prompt.ts +5 -1
- package/lib/qa/index.ts +12 -0
- package/lib/refine/comment-requests.ts +77 -0
- package/lib/refine/open.ts +5 -2
- package/lib/refine/prompt-bridge.ts +219 -0
- package/lib/refine/qa-suppression.ts +41 -0
- package/lib/refine/server.ts +122 -34
- package/lib/runtime/index.ts +225 -0
- package/lib/runtime/research.ts +175 -0
- package/lib/runtime/review.ts +270 -0
- package/lib/runtime/story.ts +53 -0
- package/package.json +6 -1
- package/plugin.ts +6 -2
- package/plugins/revela/.codex-plugin/plugin.json +37 -0
- package/plugins/revela/.mcp.json +11 -0
- package/plugins/revela/assets/README.md +2 -0
- package/plugins/revela/hooks/hooks.json +28 -0
- package/plugins/revela/hooks/revela_guard.ts +10 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +18 -0
- package/plugins/revela/mcp/revela-server.ts +504 -0
- package/plugins/revela/mcp/runtime-resolver.ts +109 -0
- package/plugins/revela/skills/revela-design/SKILL.md +20 -0
- package/plugins/revela/skills/revela-domain/SKILL.md +18 -0
- package/plugins/revela/skills/revela-export/SKILL.md +21 -0
- package/plugins/revela/skills/revela-init/SKILL.md +36 -0
- package/plugins/revela/skills/revela-make-deck/SKILL.md +37 -0
- package/plugins/revela/skills/revela-research/SKILL.md +38 -0
- package/plugins/revela/skills/revela-review-deck/SKILL.md +33 -0
- package/plugins/revela/skills/revela-story/SKILL.md +24 -0
- package/skill/SKILL.md +17 -8
- package/tools/deck-foundation.ts +48 -0
- package/tools/decks.ts +10 -78
- package/tools/research-save.ts +8 -72
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
export type CommentRequestStatus = "pending" | "completed" | "failed" | "expired"
|
|
2
|
+
|
|
3
|
+
export interface PendingCommentRequest {
|
|
4
|
+
requestId: string
|
|
5
|
+
status: CommentRequestStatus
|
|
6
|
+
deckVersion: string
|
|
7
|
+
createdAt: number
|
|
8
|
+
updatedAt: number
|
|
9
|
+
error?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const REQUEST_TTL_MS = 120 * 1000
|
|
13
|
+
const requests = new Map<string, PendingCommentRequest>()
|
|
14
|
+
|
|
15
|
+
export function createCommentRequest(input: {
|
|
16
|
+
requestId: string
|
|
17
|
+
deckVersion: string
|
|
18
|
+
}): PendingCommentRequest {
|
|
19
|
+
cleanupCommentRequests()
|
|
20
|
+
const now = Date.now()
|
|
21
|
+
const request: PendingCommentRequest = {
|
|
22
|
+
requestId: input.requestId,
|
|
23
|
+
status: "pending",
|
|
24
|
+
deckVersion: input.deckVersion,
|
|
25
|
+
createdAt: now,
|
|
26
|
+
updatedAt: now,
|
|
27
|
+
}
|
|
28
|
+
requests.set(input.requestId, request)
|
|
29
|
+
return request
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getCommentRequest(requestId: string): PendingCommentRequest | undefined {
|
|
33
|
+
cleanupCommentRequests()
|
|
34
|
+
const request = requests.get(requestId)
|
|
35
|
+
if (!request) return undefined
|
|
36
|
+
if (request.status === "pending" && Date.now() - request.createdAt > REQUEST_TTL_MS) {
|
|
37
|
+
request.status = "expired"
|
|
38
|
+
request.error = "Review agent timed out before completing the comment request."
|
|
39
|
+
request.updatedAt = Date.now()
|
|
40
|
+
}
|
|
41
|
+
return request
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function completeCommentRequest(requestId: string): PendingCommentRequest | undefined {
|
|
45
|
+
const request = getCommentRequest(requestId)
|
|
46
|
+
if (!request || request.status !== "pending") return request
|
|
47
|
+
request.status = "completed"
|
|
48
|
+
request.updatedAt = Date.now()
|
|
49
|
+
return request
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function failCommentRequest(requestId: string, error: string): PendingCommentRequest | undefined {
|
|
53
|
+
const request = getCommentRequest(requestId)
|
|
54
|
+
if (!request || request.status !== "pending") return request
|
|
55
|
+
request.status = "failed"
|
|
56
|
+
request.error = error
|
|
57
|
+
request.updatedAt = Date.now()
|
|
58
|
+
return request
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function cleanupCommentRequests(now = Date.now()): void {
|
|
62
|
+
for (const [requestId, request] of requests) {
|
|
63
|
+
if (request.status === "pending" && now - request.createdAt > REQUEST_TTL_MS) {
|
|
64
|
+
request.status = "expired"
|
|
65
|
+
request.error = "Review agent timed out before completing the comment request."
|
|
66
|
+
request.updatedAt = now
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
if (request.status !== "pending" && now - request.updatedAt > REQUEST_TTL_MS) {
|
|
70
|
+
requests.delete(requestId)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function clearCommentRequestsForTests(): void {
|
|
76
|
+
requests.clear()
|
|
77
|
+
}
|
package/lib/refine/open.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ensureEditableDeckState } from "../edit/deck-state"
|
|
|
8
8
|
import { openUrl } from "../edit/open"
|
|
9
9
|
import { resolveEditableDeck, type EditableDeck } from "../edit/resolve-deck"
|
|
10
10
|
import { buildPrompt } from "../prompt-builder"
|
|
11
|
+
import type { ReviewPromptBridge } from "./prompt-bridge"
|
|
11
12
|
import { startRefineServer, type RefineMode } from "./server"
|
|
12
13
|
|
|
13
14
|
export interface OpenRefineDeckResult {
|
|
@@ -27,12 +28,13 @@ export interface EnsureRefineDeckOpenResult extends OpenRefineDeckResult {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface OpenRefineDeckOptions {
|
|
30
|
-
client
|
|
31
|
-
sessionID
|
|
31
|
+
client?: any
|
|
32
|
+
sessionID?: string
|
|
32
33
|
workspaceRoot: string
|
|
33
34
|
mode?: RefineMode
|
|
34
35
|
openBrowser?: boolean
|
|
35
36
|
openUrl?: (url: string) => void
|
|
37
|
+
promptBridge?: ReviewPromptBridge
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export function openRefineDeck(target: string, options: OpenRefineDeckOptions): OpenRefineDeckResult {
|
|
@@ -70,6 +72,7 @@ function openRefineDeckInternal(
|
|
|
70
72
|
workspaceRoot: options.workspaceRoot,
|
|
71
73
|
deck,
|
|
72
74
|
mode,
|
|
75
|
+
promptBridge: options.promptBridge,
|
|
73
76
|
})
|
|
74
77
|
const url = `${refineServer.baseUrl}/refine?token=${encodeURIComponent(session.token)}`
|
|
75
78
|
const shouldOpen = options.openBrowser !== false && !(behavior.skipLiveSession && session.live)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { spawn } from "child_process"
|
|
2
|
+
import type { InspectionResult } from "../inspection-context/result"
|
|
3
|
+
|
|
4
|
+
export type ReviewPromptAction = "comment" | "inspect"
|
|
5
|
+
export type ReviewPromptBridgeKind = "opencode" | "codex-exec"
|
|
6
|
+
|
|
7
|
+
export interface ReviewPromptInput {
|
|
8
|
+
action: ReviewPromptAction
|
|
9
|
+
prompt: string
|
|
10
|
+
workspaceRoot: string
|
|
11
|
+
file: string
|
|
12
|
+
requestId?: string
|
|
13
|
+
timeoutMs?: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ReviewPromptResult =
|
|
17
|
+
| { ok: true; status: "sent" | "completed"; result?: InspectionResult; raw?: string }
|
|
18
|
+
| { ok: false; status: "failed" | "unsupported"; error: string; raw?: string }
|
|
19
|
+
|
|
20
|
+
export interface ReviewPromptBridge {
|
|
21
|
+
kind: ReviewPromptBridgeKind
|
|
22
|
+
send(input: ReviewPromptInput): Promise<ReviewPromptResult>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CodexExecRunResult {
|
|
26
|
+
exitCode: number | null
|
|
27
|
+
stdout: string
|
|
28
|
+
stderr: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type CodexExecRunner = (input: {
|
|
32
|
+
prompt: string
|
|
33
|
+
workspaceRoot: string
|
|
34
|
+
timeoutMs: number
|
|
35
|
+
}) => Promise<CodexExecRunResult>
|
|
36
|
+
|
|
37
|
+
export function createOpenCodeReviewPromptBridge(client: any, sessionID: string): ReviewPromptBridge {
|
|
38
|
+
return {
|
|
39
|
+
kind: "opencode",
|
|
40
|
+
async send(input) {
|
|
41
|
+
if (!client?.session?.prompt || !sessionID) {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
status: "failed",
|
|
45
|
+
error: "OpenCode Review bridge requires client.session.prompt and sessionID.",
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
await client.session.prompt({
|
|
49
|
+
path: { id: sessionID },
|
|
50
|
+
body: {
|
|
51
|
+
parts: [{ type: "text", text: input.prompt }],
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
return { ok: true, status: "sent" }
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function createCodexExecReviewPromptBridge(options: {
|
|
60
|
+
runner?: CodexExecRunner
|
|
61
|
+
timeoutMs?: number
|
|
62
|
+
} = {}): ReviewPromptBridge {
|
|
63
|
+
const runner = options.runner ?? runCodexExec
|
|
64
|
+
const timeoutMs = options.timeoutMs ?? 120_000
|
|
65
|
+
return {
|
|
66
|
+
kind: "codex-exec",
|
|
67
|
+
async send(input) {
|
|
68
|
+
const output = await runner({
|
|
69
|
+
prompt: input.prompt,
|
|
70
|
+
workspaceRoot: input.workspaceRoot,
|
|
71
|
+
timeoutMs: input.timeoutMs ?? timeoutMs,
|
|
72
|
+
})
|
|
73
|
+
const raw = [output.stdout, output.stderr].filter(Boolean).join("\n")
|
|
74
|
+
if (output.exitCode !== 0) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
status: "failed",
|
|
78
|
+
error: `codex exec failed with exit code ${output.exitCode ?? "unknown"}.`,
|
|
79
|
+
raw,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (input.action === "comment") return { ok: true, status: "completed", raw }
|
|
83
|
+
const result = extractInspectionResult(output.stdout)
|
|
84
|
+
if (!result) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
status: "failed",
|
|
88
|
+
error: "codex exec did not return a valid inspection result JSON object.",
|
|
89
|
+
raw,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, status: "completed", result, raw }
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function runCodexExec(input: {
|
|
98
|
+
prompt: string
|
|
99
|
+
workspaceRoot: string
|
|
100
|
+
timeoutMs: number
|
|
101
|
+
}): Promise<CodexExecRunResult> {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const child = spawn("codex", ["exec", "--json", "--ephemeral", "-C", input.workspaceRoot, input.prompt], {
|
|
104
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
105
|
+
})
|
|
106
|
+
let stdout = ""
|
|
107
|
+
let stderr = ""
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
child.kill()
|
|
110
|
+
resolve({
|
|
111
|
+
exitCode: 124,
|
|
112
|
+
stdout,
|
|
113
|
+
stderr: `${stderr}${stderr ? "\n" : ""}codex exec timed out after ${input.timeoutMs}ms.`,
|
|
114
|
+
})
|
|
115
|
+
}, input.timeoutMs)
|
|
116
|
+
child.stdout?.on("data", (chunk) => {
|
|
117
|
+
stdout += chunk.toString()
|
|
118
|
+
})
|
|
119
|
+
child.stderr?.on("data", (chunk) => {
|
|
120
|
+
stderr += chunk.toString()
|
|
121
|
+
})
|
|
122
|
+
child.on("error", (error) => {
|
|
123
|
+
clearTimeout(timer)
|
|
124
|
+
resolve({ exitCode: 127, stdout, stderr: error.message })
|
|
125
|
+
})
|
|
126
|
+
child.on("close", (code) => {
|
|
127
|
+
clearTimeout(timer)
|
|
128
|
+
resolve({ exitCode: code, stdout, stderr })
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function extractInspectionResult(stdout: string): InspectionResult | undefined {
|
|
134
|
+
const direct = parseJson(stdout)
|
|
135
|
+
const fromDirect = findInspectionResult(direct)
|
|
136
|
+
if (fromDirect) return fromDirect
|
|
137
|
+
|
|
138
|
+
for (const line of stdout.split(/\r?\n/).reverse()) {
|
|
139
|
+
const parsed = parseJson(line)
|
|
140
|
+
const found = findInspectionResult(parsed)
|
|
141
|
+
if (found) return found
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const block of extractJsonBlocks(stdout).reverse()) {
|
|
145
|
+
const parsed = parseJson(block)
|
|
146
|
+
const found = findInspectionResult(parsed)
|
|
147
|
+
if (found) return found
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findInspectionResult(value: unknown): InspectionResult | undefined {
|
|
152
|
+
if (!value) return undefined
|
|
153
|
+
if (typeof value === "string") return findInspectionResult(parseJson(value))
|
|
154
|
+
if (Array.isArray(value)) {
|
|
155
|
+
for (const item of value) {
|
|
156
|
+
const found = findInspectionResult(item)
|
|
157
|
+
if (found) return found
|
|
158
|
+
}
|
|
159
|
+
return undefined
|
|
160
|
+
}
|
|
161
|
+
if (typeof value !== "object") return undefined
|
|
162
|
+
const record = value as Record<string, unknown>
|
|
163
|
+
if (record.version === 1 && typeof record.status === "string" && record.cards && typeof record.cards === "object") {
|
|
164
|
+
return record as unknown as InspectionResult
|
|
165
|
+
}
|
|
166
|
+
for (const key of ["result", "output", "message", "content", "text", "final", "lastMessage"]) {
|
|
167
|
+
const found = findInspectionResult(record[key])
|
|
168
|
+
if (found) return found
|
|
169
|
+
}
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseJson(value: string | undefined): unknown {
|
|
174
|
+
const text = value?.trim()
|
|
175
|
+
if (!text) return undefined
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(text)
|
|
178
|
+
} catch {
|
|
179
|
+
return undefined
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractJsonBlocks(text: string): string[] {
|
|
184
|
+
const blocks: string[] = []
|
|
185
|
+
let depth = 0
|
|
186
|
+
let start = -1
|
|
187
|
+
let inString = false
|
|
188
|
+
let escaped = false
|
|
189
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
190
|
+
const char = text[index]
|
|
191
|
+
if (inString) {
|
|
192
|
+
if (escaped) {
|
|
193
|
+
escaped = false
|
|
194
|
+
} else if (char === "\\") {
|
|
195
|
+
escaped = true
|
|
196
|
+
} else if (char === "\"") {
|
|
197
|
+
inString = false
|
|
198
|
+
}
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
if (char === "\"") {
|
|
202
|
+
inString = true
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
if (char === "{") {
|
|
206
|
+
if (depth === 0) start = index
|
|
207
|
+
depth += 1
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
if (char === "}") {
|
|
211
|
+
depth -= 1
|
|
212
|
+
if (depth === 0 && start >= 0) {
|
|
213
|
+
blocks.push(text.slice(start, index + 1))
|
|
214
|
+
start = -1
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return blocks
|
|
219
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { resolve } from "path"
|
|
2
|
+
|
|
3
|
+
export interface ReviewApplyFixArtifactQaSuppressionInput {
|
|
4
|
+
workspaceRoot: string
|
|
5
|
+
file: string
|
|
6
|
+
sessionID?: string
|
|
7
|
+
ttlMs?: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000
|
|
11
|
+
const suppressions = new Map<string, number>()
|
|
12
|
+
|
|
13
|
+
export function suppressReviewApplyFixArtifactQa(input: ReviewApplyFixArtifactQaSuppressionInput): void {
|
|
14
|
+
const key = suppressionKey(input)
|
|
15
|
+
if (!key) return
|
|
16
|
+
suppressions.set(key, Date.now() + (input.ttlMs ?? DEFAULT_TTL_MS))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function shouldSuppressReviewApplyFixArtifactQa(input: ReviewApplyFixArtifactQaSuppressionInput): boolean {
|
|
20
|
+
const key = suppressionKey(input)
|
|
21
|
+
if (!key) return false
|
|
22
|
+
const expiresAt = suppressions.get(key)
|
|
23
|
+
if (!expiresAt) return false
|
|
24
|
+
if (Date.now() > expiresAt) {
|
|
25
|
+
suppressions.delete(key)
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function clearReviewApplyFixArtifactQaSuppressionsForTests(): void {
|
|
32
|
+
suppressions.clear()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function suppressionKey(input: ReviewApplyFixArtifactQaSuppressionInput): string {
|
|
36
|
+
const sessionID = input.sessionID?.trim()
|
|
37
|
+
if (!sessionID) return ""
|
|
38
|
+
const workspaceRoot = resolve(input.workspaceRoot)
|
|
39
|
+
const file = resolve(workspaceRoot, input.file)
|
|
40
|
+
return `${workspaceRoot}\0${file}\0${sessionID}`
|
|
41
|
+
}
|
package/lib/refine/server.ts
CHANGED
|
@@ -9,10 +9,13 @@ import { buildPrompt } from "../prompt-builder"
|
|
|
9
9
|
import type { InspectionElementSnapshot } from "../inspection-context/match"
|
|
10
10
|
import { buildInspectionPrompt } from "../inspect/prompt"
|
|
11
11
|
import { projectWorkspaceElement } from "../inspect/request"
|
|
12
|
-
import { createInspectRequest, failInspectRequest, getInspectRequest } from "../inspect/requests"
|
|
12
|
+
import { completeInspectRequest, createInspectRequest, failInspectRequest, getInspectRequest } from "../inspect/requests"
|
|
13
13
|
import { saveMediaAsset } from "../media/save"
|
|
14
14
|
import { searchRemoteImages, type ImageCandidate } from "../media/search"
|
|
15
15
|
import type { MediaAssetRecord, MediaPurpose } from "../media/types"
|
|
16
|
+
import { completeCommentRequest, createCommentRequest, failCommentRequest, getCommentRequest } from "./comment-requests"
|
|
17
|
+
import { createOpenCodeReviewPromptBridge, type ReviewPromptBridge } from "./prompt-bridge"
|
|
18
|
+
import { suppressReviewApplyFixArtifactQa } from "./qa-suppression"
|
|
16
19
|
import { annotateVisualEditTargets, applyVisualTargetChanges, type VisualEditTarget } from "./visual-targets"
|
|
17
20
|
|
|
18
21
|
const TOKEN_BYTES = 24
|
|
@@ -27,8 +30,9 @@ interface EditAsset {
|
|
|
27
30
|
|
|
28
31
|
interface EditSession {
|
|
29
32
|
token: string
|
|
30
|
-
client
|
|
31
|
-
sessionID
|
|
33
|
+
client?: any
|
|
34
|
+
sessionID?: string
|
|
35
|
+
promptBridge: ReviewPromptBridge
|
|
32
36
|
deck: string
|
|
33
37
|
file: string
|
|
34
38
|
absoluteFile: string
|
|
@@ -47,7 +51,14 @@ export type RefineMode = "edit" | "inspect"
|
|
|
47
51
|
|
|
48
52
|
export interface RefineServerHandle {
|
|
49
53
|
baseUrl: string
|
|
50
|
-
getOrCreateSession(input: {
|
|
54
|
+
getOrCreateSession(input: {
|
|
55
|
+
client?: any
|
|
56
|
+
sessionID?: string
|
|
57
|
+
workspaceRoot: string
|
|
58
|
+
deck: EditableDeck
|
|
59
|
+
mode?: RefineMode
|
|
60
|
+
promptBridge?: ReviewPromptBridge
|
|
61
|
+
}): EditServerSessionResult
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
export interface EditServerSessionResult {
|
|
@@ -80,6 +91,7 @@ export function startRefineServer(): RefineServerHandle {
|
|
|
80
91
|
if (existing) {
|
|
81
92
|
existing.session.client = input.client
|
|
82
93
|
existing.session.sessionID = input.sessionID
|
|
94
|
+
existing.session.promptBridge = input.promptBridge ?? createOpenCodeReviewPromptBridge(input.client, input.sessionID ?? "")
|
|
83
95
|
existing.session.deck = input.deck.slug
|
|
84
96
|
existing.session.file = input.deck.file
|
|
85
97
|
existing.session.workspaceRoot = resolve(input.workspaceRoot)
|
|
@@ -97,6 +109,7 @@ export function startRefineServer(): RefineServerHandle {
|
|
|
97
109
|
token,
|
|
98
110
|
client: input.client,
|
|
99
111
|
sessionID: input.sessionID,
|
|
112
|
+
promptBridge: input.promptBridge ?? createOpenCodeReviewPromptBridge(input.client, input.sessionID ?? ""),
|
|
100
113
|
deck: input.deck.slug,
|
|
101
114
|
file: input.deck.file,
|
|
102
115
|
absoluteFile: input.deck.absoluteFile,
|
|
@@ -182,6 +195,12 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
182
195
|
return handleComment(req, session.value)
|
|
183
196
|
}
|
|
184
197
|
|
|
198
|
+
if (url.pathname === "/api/comment-result" && req.method === "GET") {
|
|
199
|
+
const session = validateSession(url.searchParams.get("token"))
|
|
200
|
+
if (!session.ok) return session.response
|
|
201
|
+
return handleCommentResult(url.searchParams.get("requestId"), session.value)
|
|
202
|
+
}
|
|
203
|
+
|
|
185
204
|
if (url.pathname === "/api/inspect" && req.method === "POST") {
|
|
186
205
|
const session = validateSession(url.searchParams.get("token"))
|
|
187
206
|
if (!session.ok) return session.response
|
|
@@ -727,19 +746,51 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
|
|
|
727
746
|
comment,
|
|
728
747
|
elements,
|
|
729
748
|
comments,
|
|
749
|
+
suppressAutomaticArtifactQa: true,
|
|
730
750
|
})
|
|
731
751
|
const deckVersion = readDeckVersion(session).version
|
|
752
|
+
const requestId = typeof (body as any).requestId === "string" && (body as any).requestId.trim()
|
|
753
|
+
? (body as any).requestId.trim()
|
|
754
|
+
: randomBytes(10).toString("base64url")
|
|
755
|
+
createCommentRequest({ requestId, deckVersion })
|
|
756
|
+
suppressReviewApplyFixArtifactQa({
|
|
757
|
+
workspaceRoot: session.workspaceRoot,
|
|
758
|
+
file: session.file,
|
|
759
|
+
sessionID: session.sessionID,
|
|
760
|
+
})
|
|
732
761
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
762
|
+
void session.promptBridge.send({
|
|
763
|
+
action: "comment",
|
|
764
|
+
prompt,
|
|
765
|
+
workspaceRoot: session.workspaceRoot,
|
|
766
|
+
file: session.file,
|
|
767
|
+
requestId,
|
|
768
|
+
}).then((result) => {
|
|
769
|
+
if (result.ok) {
|
|
770
|
+
completeCommentRequest(requestId)
|
|
771
|
+
} else {
|
|
772
|
+
failCommentRequest(requestId, result.error)
|
|
773
|
+
}
|
|
774
|
+
}).catch((error: unknown) => {
|
|
775
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
776
|
+
failCommentRequest(requestId, message)
|
|
738
777
|
})
|
|
739
778
|
|
|
740
779
|
session.lastActiveAt = Date.now()
|
|
741
780
|
scheduleIdleStop()
|
|
742
|
-
return jsonResponse({ ok: true, deckVersion })
|
|
781
|
+
return jsonResponse({ ok: true, requestId, commentRequestId: requestId, deckVersion, status: "pending" })
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function handleCommentResult(requestId: string | null, session: EditSession): Response {
|
|
785
|
+
if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400)
|
|
786
|
+
const request = getCommentRequest(requestId)
|
|
787
|
+
if (!request) return jsonResponse({ ok: false, requestId, error: "Comment request not found" }, 404)
|
|
788
|
+
session.lastActiveAt = Date.now()
|
|
789
|
+
scheduleIdleStop()
|
|
790
|
+
if (request.status === "failed" || request.status === "expired") {
|
|
791
|
+
return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion, error: request.error || "Review agent failed" })
|
|
792
|
+
}
|
|
793
|
+
return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion })
|
|
743
794
|
}
|
|
744
795
|
|
|
745
796
|
async function handleInspect(req: Request, session: EditSession): Promise<Response> {
|
|
@@ -766,22 +817,29 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
|
|
|
766
817
|
session.lastActiveAt = Date.now()
|
|
767
818
|
scheduleIdleStop()
|
|
768
819
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
820
|
+
const prompt = buildInspectionPrompt({
|
|
821
|
+
requestId,
|
|
822
|
+
file: session.file,
|
|
823
|
+
language,
|
|
824
|
+
comment,
|
|
825
|
+
delivery: session.promptBridge.kind === "codex-exec" ? "json" : "tool",
|
|
826
|
+
projection: staleReason
|
|
827
|
+
? { ...projection, stale: { stale: true, reason: staleReason } } as any
|
|
828
|
+
: projection,
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
void session.promptBridge.send({
|
|
832
|
+
action: "inspect",
|
|
833
|
+
prompt,
|
|
834
|
+
workspaceRoot: session.workspaceRoot,
|
|
835
|
+
file: session.file,
|
|
836
|
+
requestId,
|
|
837
|
+
}).then((result) => {
|
|
838
|
+
if (result.ok && result.result) {
|
|
839
|
+
completeInspectRequest(requestId, result.result)
|
|
840
|
+
} else if (!result.ok) {
|
|
841
|
+
failInspectRequest(requestId, result.error)
|
|
842
|
+
}
|
|
785
843
|
}).catch((error: unknown) => {
|
|
786
844
|
const message = error instanceof Error ? error.message : String(error)
|
|
787
845
|
failInspectRequest(requestId, message)
|
|
@@ -2038,14 +2096,14 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2038
2096
|
});
|
|
2039
2097
|
const body = await res.json().catch(() => ({}));
|
|
2040
2098
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send comment');
|
|
2041
|
-
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion });
|
|
2099
|
+
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion, requestId: body.commentRequestId || body.requestId || '' });
|
|
2042
2100
|
if (pendingCommentStatus(commentId) !== 'updated') setStatus('Comment sent. Waiting for deck update...');
|
|
2043
|
-
|
|
2044
|
-
updateSendState();
|
|
2101
|
+
if (body.commentRequestId || body.requestId) pollCommentResult(commentId, body.commentRequestId || body.requestId);
|
|
2045
2102
|
} catch (error) {
|
|
2046
2103
|
updatePendingCommentStatus(commentId, 'failed');
|
|
2047
|
-
state.sendingEdit = false;
|
|
2048
2104
|
reportError(error);
|
|
2105
|
+
} finally {
|
|
2106
|
+
state.sendingEdit = false;
|
|
2049
2107
|
updateSendState();
|
|
2050
2108
|
}
|
|
2051
2109
|
}
|
|
@@ -2398,8 +2456,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2398
2456
|
});
|
|
2399
2457
|
const body = await res.json().catch(() => ({}));
|
|
2400
2458
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send asset placement');
|
|
2401
|
-
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion });
|
|
2459
|
+
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion, requestId: body.commentRequestId || body.requestId || '' });
|
|
2402
2460
|
if (pendingCommentStatus(commentId) !== 'updated') setStatus('Asset placement sent. Waiting for deck update...');
|
|
2461
|
+
if (body.commentRequestId || body.requestId) pollCommentResult(commentId, body.commentRequestId || body.requestId);
|
|
2403
2462
|
} catch (error) {
|
|
2404
2463
|
updatePendingCommentStatus(commentId, 'failed');
|
|
2405
2464
|
reportError(error);
|
|
@@ -2483,6 +2542,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2483
2542
|
createdAt: Date.now(),
|
|
2484
2543
|
baseDeckVersion: state.deckVersion,
|
|
2485
2544
|
updatedVersion: null,
|
|
2545
|
+
requestId: '',
|
|
2486
2546
|
});
|
|
2487
2547
|
renderCommentThread();
|
|
2488
2548
|
return id;
|
|
@@ -2531,6 +2591,34 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2531
2591
|
return state.pendingComments.find((comment) => comment.id === id)?.status || '';
|
|
2532
2592
|
}
|
|
2533
2593
|
|
|
2594
|
+
async function pollCommentResult(commentId, requestId) {
|
|
2595
|
+
if (!requestId) return;
|
|
2596
|
+
for (let attempt = 0; attempt < 140; attempt++) {
|
|
2597
|
+
await delay(1000);
|
|
2598
|
+
if (pendingCommentStatus(commentId) === 'updated') return;
|
|
2599
|
+
try {
|
|
2600
|
+
const res = await fetch('/api/comment-result?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId), { cache: 'no-store' });
|
|
2601
|
+
const body = await res.json().catch(() => ({}));
|
|
2602
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Comment result failed');
|
|
2603
|
+
if (body.status === 'failed' || body.status === 'expired') {
|
|
2604
|
+
updatePendingCommentStatus(commentId, 'failed');
|
|
2605
|
+
setStatus(body.error || 'Review agent failed to apply the comment.');
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
if (body.status === 'completed') {
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2611
|
+
} catch (error) {
|
|
2612
|
+
reportError(error);
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (pendingCommentStatus(commentId) !== 'updated') {
|
|
2617
|
+
updatePendingCommentStatus(commentId, 'failed');
|
|
2618
|
+
setStatus('Review agent timed out before applying the comment.');
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2534
2622
|
function renderCommentThread() {
|
|
2535
2623
|
els.commentThread.textContent = '';
|
|
2536
2624
|
state.pendingComments.forEach((comment) => {
|
|
@@ -2555,8 +2643,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2555
2643
|
if (status === 'updated') return 'Deck file updated';
|
|
2556
2644
|
if (status === 'stale') return 'Still waiting for deck file update';
|
|
2557
2645
|
if (status === 'failed') return 'Failed to send';
|
|
2558
|
-
if (status === 'sending') return 'Sending to
|
|
2559
|
-
return '
|
|
2646
|
+
if (status === 'sending') return 'Sending to Review agent...';
|
|
2647
|
+
return 'Sent to Review agent';
|
|
2560
2648
|
}
|
|
2561
2649
|
|
|
2562
2650
|
function targetFromPointer(event) {
|
|
@@ -2735,7 +2823,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2735
2823
|
}
|
|
2736
2824
|
if (body.status === 'failed' || body.status === 'expired') throw new Error(body.error || 'Insight failed');
|
|
2737
2825
|
}
|
|
2738
|
-
throw new Error('Insight timed out while waiting for
|
|
2826
|
+
throw new Error('Insight timed out while waiting for Review agent result');
|
|
2739
2827
|
}
|
|
2740
2828
|
|
|
2741
2829
|
function collectReferenceSnapshot() {
|