@cyber-dash-tech/revela 0.18.10 → 0.18.12
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 +4 -4
- package/README.zh-CN.md +4 -4
- package/lib/design/designs.ts +2 -2
- package/lib/refine/comment-requests.ts +20 -2
- package/lib/refine/prompt-bridge.ts +40 -2
- package/lib/refine/review-comments.ts +7 -2
- package/lib/refine/server.ts +106 -39
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/mcp/revela-server.ts +2 -2
- package/plugins/revela/skills/revela-design/SKILL.md +6 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,7 +34,7 @@ To install globally, add the same entry to `~/.config/opencode/opencode.json`.
|
|
|
34
34
|
Requirements:
|
|
35
35
|
|
|
36
36
|
- The Codex CLI must be installed and the `codex` command must be available in your shell.
|
|
37
|
-
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.
|
|
37
|
+
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.12 mcp` to start the MCP server.
|
|
38
38
|
- For interactive Review Apply actions, `codex exec` must also work because the Review UI uses it after saved comments are applied.
|
|
39
39
|
|
|
40
40
|
Optional preflight:
|
|
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
Install Revela through the Codex Git marketplace:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.12
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.12 mcp` so npm can fetch the published package and its dependencies.
|
|
63
63
|
|
|
64
64
|
You do not need to run `bun install` inside the Codex marketplace clone.
|
|
65
65
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | **中文**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,7 +34,7 @@ Revela 可在 [OpenCode](https://opencode.ai) 和 Codex 中使用,把来源材
|
|
|
34
34
|
环境要求:
|
|
35
35
|
|
|
36
36
|
- 需要已安装 Codex CLI,并且 shell 中可以执行 `codex`。
|
|
37
|
-
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.
|
|
37
|
+
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.12 mcp` 启动 MCP server。
|
|
38
38
|
- 如果使用 Review UI 的 Apply,需要 `codex exec` 可用;评论会先保存,点击 Apply 后才执行修复。
|
|
39
39
|
|
|
40
40
|
可选的安装前检查:
|
|
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
通过 Codex Git marketplace 安装 Revela:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.12
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.12 mcp`,由 npm 获取已发布 package 及其 dependencies。
|
|
63
63
|
|
|
64
64
|
不需要在 Codex marketplace clone 里运行 `bun install`。
|
|
65
65
|
|
package/lib/design/designs.ts
CHANGED
|
@@ -656,9 +656,9 @@ function writeDesignAssets(targetDir: string, assets?: DesignPackageAssetInput[]
|
|
|
656
656
|
|
|
657
657
|
function normalizeAssetPath(pathInput: string): string {
|
|
658
658
|
const normalized = pathInput.replace(/\\/g, "/").replace(/^\.\/+/, "")
|
|
659
|
-
if (!normalized.startsWith("assets/")) throw new Error(`Design asset
|
|
659
|
+
if (!normalized.startsWith("assets/")) throw new Error(`Design asset path must be located under assets/: ${pathInput}`)
|
|
660
660
|
if (normalized.includes("\0") || normalized.startsWith("/") || normalized.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
661
|
-
throw new Error(`
|
|
661
|
+
throw new Error(`Design asset path must be located under assets/ and must not contain absolute paths, empty segments, '.', '..', or NUL characters: ${pathInput}`)
|
|
662
662
|
}
|
|
663
663
|
return normalized
|
|
664
664
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReviewBridgeEvent } from "./prompt-bridge"
|
|
2
2
|
|
|
3
|
-
export type CommentRequestStatus = "pending" | "completed" | "failed" | "expired"
|
|
3
|
+
export type CommentRequestStatus = "pending" | "completed" | "failed" | "expired" | "stopped"
|
|
4
4
|
|
|
5
5
|
export interface PendingCommentRequest {
|
|
6
6
|
requestId: string
|
|
@@ -85,6 +85,24 @@ export function failCommentRequest(requestId: string, error: string, raw?: strin
|
|
|
85
85
|
return request
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
export function stopCommentRequest(requestId: string): PendingCommentRequest | undefined {
|
|
89
|
+
const request = getCommentRequest(requestId)
|
|
90
|
+
if (!request || request.status !== "pending") return request
|
|
91
|
+
request.status = "stopped"
|
|
92
|
+
request.error = "Stopped by user."
|
|
93
|
+
request.raw = "Stopped by user."
|
|
94
|
+
request.updatedAt = Date.now()
|
|
95
|
+
if (!hasTerminalEvent(request)) {
|
|
96
|
+
appendCommentRequestEvent(request, {
|
|
97
|
+
type: "stopped",
|
|
98
|
+
message: "Stopped by user.",
|
|
99
|
+
timestamp: request.updatedAt,
|
|
100
|
+
detail: "Stopped by user.",
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
return request
|
|
104
|
+
}
|
|
105
|
+
|
|
88
106
|
function boundedTail(text: string, limit = 4096): string {
|
|
89
107
|
if (text.length <= limit) return text
|
|
90
108
|
return text.slice(text.length - limit)
|
|
@@ -154,5 +172,5 @@ function appendCommentRequestEvent(request: PendingCommentRequest, event: Review
|
|
|
154
172
|
}
|
|
155
173
|
|
|
156
174
|
function hasTerminalEvent(request: PendingCommentRequest): boolean {
|
|
157
|
-
return request.events.some((event) => event.type === "completed" || event.type === "failed" || event.type === "timeout")
|
|
175
|
+
return request.events.some((event) => event.type === "completed" || event.type === "failed" || event.type === "timeout" || event.type === "stopped")
|
|
158
176
|
}
|
|
@@ -5,7 +5,7 @@ export type ReviewPromptAction = "comment" | "inspect"
|
|
|
5
5
|
export type ReviewPromptBridgeKind = "opencode" | "codex-exec"
|
|
6
6
|
|
|
7
7
|
export type ReviewBridgeEvent = {
|
|
8
|
-
type: "started" | "codex_event" | "stdout" | "stderr" | "completed" | "failed" | "timeout"
|
|
8
|
+
type: "started" | "codex_event" | "stdout" | "stderr" | "completed" | "failed" | "timeout" | "stopped"
|
|
9
9
|
message: string
|
|
10
10
|
timestamp: number
|
|
11
11
|
detail?: string
|
|
@@ -18,12 +18,13 @@ export interface ReviewPromptInput {
|
|
|
18
18
|
file: string
|
|
19
19
|
requestId?: string
|
|
20
20
|
timeoutMs?: number
|
|
21
|
+
signal?: AbortSignal
|
|
21
22
|
onEvent?: (event: ReviewBridgeEvent) => void
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export type ReviewPromptResult =
|
|
25
26
|
| { ok: true; status: "sent" | "completed"; result?: InspectionResult; raw?: string }
|
|
26
|
-
| { ok: false; status: "failed" | "unsupported"; error: string; raw?: string }
|
|
27
|
+
| { ok: false; status: "failed" | "unsupported" | "stopped"; error: string; raw?: string }
|
|
27
28
|
|
|
28
29
|
export interface ReviewPromptBridge {
|
|
29
30
|
kind: ReviewPromptBridgeKind
|
|
@@ -46,6 +47,7 @@ export type CodexExecRunner = (input: {
|
|
|
46
47
|
timeoutMs: number
|
|
47
48
|
sandboxMode: "read-only" | "workspace-write"
|
|
48
49
|
skipGitRepoCheck: boolean
|
|
50
|
+
signal?: AbortSignal
|
|
49
51
|
onEvent?: (event: ReviewBridgeEvent) => void
|
|
50
52
|
}) => Promise<CodexExecRunResult>
|
|
51
53
|
|
|
@@ -84,6 +86,10 @@ export function createCodexExecReviewPromptBridge(options: {
|
|
|
84
86
|
const sandboxMode = input.action === "comment" ? "workspace-write" : "read-only"
|
|
85
87
|
const timeoutMs = input.timeoutMs ?? options.timeoutMs ?? (input.action === "comment" ? DEFAULT_COMMENT_TIMEOUT_MS : DEFAULT_INSPECT_TIMEOUT_MS)
|
|
86
88
|
input.onEvent?.(bridgeEvent("started", "Starting Codex..."))
|
|
89
|
+
if (input.signal?.aborted) {
|
|
90
|
+
input.onEvent?.(bridgeEvent("stopped", "Stopped by user."))
|
|
91
|
+
return { ok: false, status: "stopped", error: "Stopped by user.", raw: "Stopped by user." }
|
|
92
|
+
}
|
|
87
93
|
const startedAt = Date.now()
|
|
88
94
|
const heartbeat = input.onEvent
|
|
89
95
|
? setInterval(() => {
|
|
@@ -100,12 +106,22 @@ export function createCodexExecReviewPromptBridge(options: {
|
|
|
100
106
|
timeoutMs,
|
|
101
107
|
sandboxMode,
|
|
102
108
|
skipGitRepoCheck: true,
|
|
109
|
+
signal: input.signal,
|
|
103
110
|
onEvent: input.onEvent,
|
|
104
111
|
})
|
|
105
112
|
} finally {
|
|
106
113
|
if (heartbeat) clearInterval(heartbeat)
|
|
107
114
|
}
|
|
108
115
|
const raw = [output.stdout, output.stderr].filter(Boolean).join("\n")
|
|
116
|
+
if (input.signal?.aborted || raw.includes("Stopped by user.")) {
|
|
117
|
+
input.onEvent?.(bridgeEvent("stopped", "Stopped by user.", boundedTail(raw || "Stopped by user.")))
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
status: "stopped",
|
|
121
|
+
error: "Stopped by user.",
|
|
122
|
+
raw,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
109
125
|
if (input.action === "comment" && isCodexWriteBlocked(raw)) {
|
|
110
126
|
input.onEvent?.(bridgeEvent("failed", "codex exec could not write the deck because its sandbox blocked file changes.", boundedTail(raw)))
|
|
111
127
|
return {
|
|
@@ -155,6 +171,7 @@ async function runCodexExec(input: {
|
|
|
155
171
|
timeoutMs: number
|
|
156
172
|
sandboxMode: "read-only" | "workspace-write"
|
|
157
173
|
skipGitRepoCheck: boolean
|
|
174
|
+
signal?: AbortSignal
|
|
158
175
|
onEvent?: (event: ReviewBridgeEvent) => void
|
|
159
176
|
}): Promise<CodexExecRunResult> {
|
|
160
177
|
return new Promise((resolve) => {
|
|
@@ -169,12 +186,31 @@ async function runCodexExec(input: {
|
|
|
169
186
|
let stdoutLineBuffer = ""
|
|
170
187
|
let sawTrustedCompletion = false
|
|
171
188
|
let resolved = false
|
|
189
|
+
let stopped = false
|
|
172
190
|
const resolveOnce = (output: CodexExecRunResult) => {
|
|
173
191
|
if (resolved) return
|
|
174
192
|
resolved = true
|
|
193
|
+
if (input.signal) input.signal.removeEventListener("abort", abort)
|
|
175
194
|
resolve(output)
|
|
176
195
|
}
|
|
196
|
+
const abort = () => {
|
|
197
|
+
if (stopped) return
|
|
198
|
+
stopped = true
|
|
199
|
+
child.kill()
|
|
200
|
+
input.onEvent?.(bridgeEvent("stopped", "Stopped by user."))
|
|
201
|
+
resolveOnce({
|
|
202
|
+
exitCode: null,
|
|
203
|
+
stdout,
|
|
204
|
+
stderr: `${stderr}${stderr ? "\n" : ""}Stopped by user.`,
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
if (input.signal?.aborted) {
|
|
208
|
+
abort()
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
input.signal?.addEventListener("abort", abort, { once: true })
|
|
177
212
|
const timer = setTimeout(() => {
|
|
213
|
+
if (stopped) return
|
|
178
214
|
child.kill()
|
|
179
215
|
const nextStderr = `${stderr}${stderr ? "\n" : ""}codex exec timed out after ${input.timeoutMs}ms.`
|
|
180
216
|
if (input.action === "comment" && sawTrustedCompletion) {
|
|
@@ -202,11 +238,13 @@ async function runCodexExec(input: {
|
|
|
202
238
|
})
|
|
203
239
|
child.on("error", (error) => {
|
|
204
240
|
clearTimeout(timer)
|
|
241
|
+
if (stopped) return
|
|
205
242
|
input.onEvent?.(bridgeEvent("failed", "Failed to start codex exec.", boundedTail(error.message)))
|
|
206
243
|
resolveOnce({ exitCode: 127, stdout, stderr: error.message })
|
|
207
244
|
})
|
|
208
245
|
child.on("close", (code) => {
|
|
209
246
|
clearTimeout(timer)
|
|
247
|
+
if (stopped) return
|
|
210
248
|
const progress = emitCodexJsonProgress(`${stdoutLineBuffer}\n`, input.action, input.onEvent)
|
|
211
249
|
sawTrustedCompletion = sawTrustedCompletion || progress.sawTrustedCompletion
|
|
212
250
|
resolveOnce({ exitCode: code, stdout, stderr })
|
|
@@ -3,7 +3,7 @@ import { join } from "path"
|
|
|
3
3
|
import { randomBytes } from "crypto"
|
|
4
4
|
import { workspaceMetaPath } from "../workspace-meta"
|
|
5
5
|
|
|
6
|
-
export type ReviewCommentStatus = "open" | "queued" | "applying" | "applied" | "failed"
|
|
6
|
+
export type ReviewCommentStatus = "open" | "queued" | "applying" | "applied" | "failed" | "stopped"
|
|
7
7
|
|
|
8
8
|
export interface ReviewCommentRecord {
|
|
9
9
|
id: string
|
|
@@ -113,7 +113,12 @@ export function markReviewCommentFailed(workspaceRoot: string, id: string, error
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export function markReviewCommentStopped(workspaceRoot: string, id: string): ReviewCommentRecord | undefined {
|
|
116
|
-
return
|
|
116
|
+
return updateReviewComment(workspaceRoot, id, (record) => ({
|
|
117
|
+
...record,
|
|
118
|
+
status: "stopped",
|
|
119
|
+
lastApplyError: "Stopped by user.",
|
|
120
|
+
lastApplyRaw: "Stopped by user.",
|
|
121
|
+
}))
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
export function deleteReviewComment(workspaceRoot: string, id: string): boolean {
|
package/lib/refine/server.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { addInspectRequestEvent, completeInspectRequest, createInspectRequest, f
|
|
|
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 { addCommentRequestEvent, completeCommentRequest, createCommentRequest, failCommentRequest, getCommentRequest, subscribeCommentRequestEvents } from "./comment-requests"
|
|
16
|
+
import { addCommentRequestEvent, completeCommentRequest, createCommentRequest, failCommentRequest, getCommentRequest, stopCommentRequest, subscribeCommentRequestEvents } from "./comment-requests"
|
|
17
17
|
import { createOpenCodeReviewPromptBridge, type ReviewPromptBridge } from "./prompt-bridge"
|
|
18
18
|
import { suppressReviewApplyFixArtifactQa } from "./qa-suppression"
|
|
19
19
|
import { createReviewComment, deleteReviewComment, listReviewComments, markReviewCommentApplied, markReviewCommentApplying, markReviewCommentFailed, markReviewCommentQueued, markReviewCommentStopped, readReviewComment, type ReviewCommentRecord } from "./review-comments"
|
|
@@ -53,6 +53,8 @@ interface EditSession {
|
|
|
53
53
|
visualTargets: Map<string, VisualEditTarget>
|
|
54
54
|
visualTargetDeckVersion?: string
|
|
55
55
|
activeApplyCommentId?: string
|
|
56
|
+
activeApplyRequestId?: string
|
|
57
|
+
activeApplyController?: AbortController
|
|
56
58
|
applyQueue: string[]
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -237,7 +239,6 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
237
239
|
if (url.pathname === "/api/comments" && req.method === "POST") {
|
|
238
240
|
const session = validateSession(url.searchParams.get("token"))
|
|
239
241
|
if (!session.ok) return session.response
|
|
240
|
-
if (session.value.activeApplyCommentId) return applyLockedResponse()
|
|
241
242
|
return handleReviewCommentCreate(req, session.value)
|
|
242
243
|
}
|
|
243
244
|
|
|
@@ -245,7 +246,6 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
245
246
|
if (applyMatch && req.method === "POST") {
|
|
246
247
|
const session = validateSession(url.searchParams.get("token"))
|
|
247
248
|
if (!session.ok) return session.response
|
|
248
|
-
if (session.value.activeApplyCommentId) return applyLockedResponse()
|
|
249
249
|
return handleReviewCommentApply(decodeURIComponent(applyMatch[1]), req, session.value)
|
|
250
250
|
}
|
|
251
251
|
|
|
@@ -253,7 +253,6 @@ async function handleRequest(req: Request): Promise<Response> {
|
|
|
253
253
|
if (stopMatch && req.method === "POST") {
|
|
254
254
|
const session = validateSession(url.searchParams.get("token"))
|
|
255
255
|
if (!session.ok) return session.response
|
|
256
|
-
if (session.value.activeApplyCommentId) return applyLockedResponse()
|
|
257
256
|
return handleReviewCommentStop(decodeURIComponent(stopMatch[1]), session.value)
|
|
258
257
|
}
|
|
259
258
|
|
|
@@ -856,6 +855,9 @@ async function handleReviewCommentCreate(req: Request, session: EditSession): Pr
|
|
|
856
855
|
})
|
|
857
856
|
session.lastActiveAt = Date.now()
|
|
858
857
|
scheduleIdleStop()
|
|
858
|
+
if ((body as any).applyNow === true) {
|
|
859
|
+
return enqueueOrStartPersistedReviewCommentApply(session, saved, {})
|
|
860
|
+
}
|
|
859
861
|
return jsonResponse({ ok: true, comment: saved, deckVersion })
|
|
860
862
|
} catch (error) {
|
|
861
863
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -881,11 +883,20 @@ function handleReviewCommentStop(commentId: string, session: EditSession): Respo
|
|
|
881
883
|
const comment = readReviewComment(session.workspaceRoot, commentId)
|
|
882
884
|
if (!comment || comment.deckFile !== session.file) return jsonResponse({ ok: false, error: "Review comment not found" }, 404)
|
|
883
885
|
session.applyQueue = (session.applyQueue ?? []).filter((id) => id !== comment.id)
|
|
884
|
-
|
|
886
|
+
const wasActive = session.activeApplyCommentId === comment.id
|
|
887
|
+
if (wasActive) {
|
|
888
|
+
session.activeApplyController?.abort()
|
|
889
|
+
if (session.activeApplyRequestId) stopCommentRequest(session.activeApplyRequestId)
|
|
890
|
+
session.activeApplyCommentId = undefined
|
|
891
|
+
session.activeApplyRequestId = undefined
|
|
892
|
+
session.activeApplyController = undefined
|
|
893
|
+
} else if (comment.lastApplyRequestId) {
|
|
894
|
+
stopCommentRequest(comment.lastApplyRequestId)
|
|
895
|
+
}
|
|
885
896
|
const stopped = markReviewCommentStopped(session.workspaceRoot, comment.id) ?? comment
|
|
886
897
|
session.lastActiveAt = Date.now()
|
|
887
898
|
scheduleIdleStop()
|
|
888
|
-
if (comment.status === "queued") drainPersistedReviewCommentApplyQueue(session)
|
|
899
|
+
if (comment.status === "queued" || wasActive) drainPersistedReviewCommentApplyQueue(session)
|
|
889
900
|
return jsonResponse({ ok: true, status: "stopped", comment: stopped, deckVersion: readDeckVersion(session).version })
|
|
890
901
|
}
|
|
891
902
|
|
|
@@ -903,7 +914,17 @@ function handleReviewCommentDelete(commentId: string, session: EditSession): Res
|
|
|
903
914
|
|
|
904
915
|
async function enqueueOrStartPersistedReviewCommentApply(session: EditSession, comment: ReviewCommentRecord, body: any = {}): Promise<Response> {
|
|
905
916
|
session.applyQueue = session.applyQueue ?? []
|
|
906
|
-
if (session.activeApplyCommentId)
|
|
917
|
+
if (session.activeApplyCommentId) {
|
|
918
|
+
if (!session.applyQueue.includes(comment.id)) session.applyQueue.push(comment.id)
|
|
919
|
+
const queued = markReviewCommentQueued(session.workspaceRoot, comment.id) ?? comment
|
|
920
|
+
return jsonResponse({
|
|
921
|
+
ok: true,
|
|
922
|
+
deckVersion: readDeckVersion(session).version,
|
|
923
|
+
status: "queued",
|
|
924
|
+
queuePosition: session.applyQueue.indexOf(comment.id) + 1,
|
|
925
|
+
comment: queued,
|
|
926
|
+
})
|
|
927
|
+
}
|
|
907
928
|
|
|
908
929
|
const queuedIndex = session.applyQueue.indexOf(comment.id)
|
|
909
930
|
if (queuedIndex >= 0) {
|
|
@@ -922,6 +943,10 @@ async function enqueueOrStartPersistedReviewCommentApply(session: EditSession, c
|
|
|
922
943
|
|
|
923
944
|
async function startPersistedReviewCommentApply(session: EditSession, comment: ReviewCommentRecord, body: any = {}): Promise<Response> {
|
|
924
945
|
session.activeApplyCommentId = comment.id
|
|
946
|
+
const requestId = typeof body.requestId === "string" && body.requestId.trim() ? body.requestId.trim() : randomBytes(10).toString("base64url")
|
|
947
|
+
const controller = new AbortController()
|
|
948
|
+
session.activeApplyRequestId = requestId
|
|
949
|
+
session.activeApplyController = controller
|
|
925
950
|
try {
|
|
926
951
|
const response = await applyCommentPayload({
|
|
927
952
|
...body,
|
|
@@ -929,9 +954,10 @@ async function startPersistedReviewCommentApply(session: EditSession, comment: R
|
|
|
929
954
|
elements: comment.elements,
|
|
930
955
|
asset: comment.asset,
|
|
931
956
|
drop: comment.drop,
|
|
932
|
-
requestId
|
|
957
|
+
requestId,
|
|
933
958
|
}, session, {
|
|
934
959
|
persistedCommentId: comment.id,
|
|
960
|
+
signal: controller.signal,
|
|
935
961
|
onSettled: () => settlePersistedReviewCommentApply(session, comment.id),
|
|
936
962
|
})
|
|
937
963
|
if (!response.ok) settlePersistedReviewCommentApply(session, comment.id)
|
|
@@ -944,6 +970,10 @@ async function startPersistedReviewCommentApply(session: EditSession, comment: R
|
|
|
944
970
|
|
|
945
971
|
function settlePersistedReviewCommentApply(session: EditSession, commentId: string): void {
|
|
946
972
|
if (session.activeApplyCommentId === commentId) session.activeApplyCommentId = undefined
|
|
973
|
+
if (session.activeApplyCommentId === undefined) {
|
|
974
|
+
session.activeApplyRequestId = undefined
|
|
975
|
+
session.activeApplyController = undefined
|
|
976
|
+
}
|
|
947
977
|
drainPersistedReviewCommentApplyQueue(session)
|
|
948
978
|
}
|
|
949
979
|
|
|
@@ -963,9 +993,9 @@ function drainPersistedReviewCommentApplyQueue(session: EditSession): void {
|
|
|
963
993
|
async function applyCommentPayload(
|
|
964
994
|
body: Partial<EditCommentPayload>,
|
|
965
995
|
session: EditSession,
|
|
966
|
-
options: { persistedCommentId?: string; onSettled?: () => void } = {},
|
|
996
|
+
options: { persistedCommentId?: string; signal?: AbortSignal; onSettled?: () => void } = {},
|
|
967
997
|
): Promise<Response> {
|
|
968
|
-
const { persistedCommentId, onSettled } = options
|
|
998
|
+
const { persistedCommentId, signal, onSettled } = options
|
|
969
999
|
const comments = Array.isArray(body.comments)
|
|
970
1000
|
? body.comments
|
|
971
1001
|
.map((draft: any) => ({
|
|
@@ -1008,11 +1038,15 @@ async function applyCommentPayload(
|
|
|
1008
1038
|
workspaceRoot: session.workspaceRoot,
|
|
1009
1039
|
file: session.file,
|
|
1010
1040
|
requestId,
|
|
1041
|
+
signal,
|
|
1011
1042
|
onEvent: (event) => addCommentRequestEvent(requestId, event),
|
|
1012
1043
|
}).then((result) => {
|
|
1013
1044
|
if (result.ok) {
|
|
1014
1045
|
completeCommentRequest(requestId)
|
|
1015
1046
|
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentApplied(session.workspaceRoot, persistedCommentId)
|
|
1047
|
+
} else if (result.status === "stopped") {
|
|
1048
|
+
stopCommentRequest(requestId)
|
|
1049
|
+
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentStopped(session.workspaceRoot, persistedCommentId)
|
|
1016
1050
|
} else {
|
|
1017
1051
|
failCommentRequest(requestId, result.error, result.raw)
|
|
1018
1052
|
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentFailed(session.workspaceRoot, persistedCommentId, result.error, result.raw)
|
|
@@ -1020,8 +1054,13 @@ async function applyCommentPayload(
|
|
|
1020
1054
|
onSettled?.()
|
|
1021
1055
|
}).catch((error: unknown) => {
|
|
1022
1056
|
const message = error instanceof Error ? error.message : String(error)
|
|
1023
|
-
|
|
1024
|
-
|
|
1057
|
+
if (signal?.aborted) {
|
|
1058
|
+
stopCommentRequest(requestId)
|
|
1059
|
+
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentStopped(session.workspaceRoot, persistedCommentId)
|
|
1060
|
+
} else {
|
|
1061
|
+
failCommentRequest(requestId, message)
|
|
1062
|
+
if (persistedCommentId && readReviewComment(session.workspaceRoot, persistedCommentId)?.status === "applying") markReviewCommentFailed(session.workspaceRoot, persistedCommentId, message)
|
|
1063
|
+
}
|
|
1025
1064
|
onSettled?.()
|
|
1026
1065
|
})
|
|
1027
1066
|
|
|
@@ -1052,7 +1091,7 @@ function handleCommentEvents(requestId: string | null, session: EditSession): Re
|
|
|
1052
1091
|
}
|
|
1053
1092
|
unsubscribe = subscribeCommentRequestEvents(requestId, (event) => {
|
|
1054
1093
|
send(event)
|
|
1055
|
-
if (event.type === "completed" || event.type === "failed" || event.type === "timeout") {
|
|
1094
|
+
if (event.type === "completed" || event.type === "failed" || event.type === "timeout" || event.type === "stopped") {
|
|
1056
1095
|
unsubscribe()
|
|
1057
1096
|
controller.close()
|
|
1058
1097
|
}
|
|
@@ -1119,7 +1158,7 @@ function handleCommentResult(requestId: string | null, session: EditSession): Re
|
|
|
1119
1158
|
if (!request) return jsonResponse({ ok: false, requestId, error: "Comment request not found" }, 404)
|
|
1120
1159
|
session.lastActiveAt = Date.now()
|
|
1121
1160
|
scheduleIdleStop()
|
|
1122
|
-
if (request.status === "failed" || request.status === "expired") {
|
|
1161
|
+
if (request.status === "failed" || request.status === "expired" || request.status === "stopped") {
|
|
1123
1162
|
return jsonResponse({
|
|
1124
1163
|
ok: true,
|
|
1125
1164
|
requestId,
|
|
@@ -1312,7 +1351,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1312
1351
|
const encodedToken = JSON.stringify(token)
|
|
1313
1352
|
const encodedDefaultMode = JSON.stringify(defaultMode)
|
|
1314
1353
|
const encodedSurface = JSON.stringify(surface)
|
|
1315
|
-
const sendButtonHtml = `${lucideIcon("send")}<span class="sr-only">
|
|
1354
|
+
const sendButtonHtml = `${lucideIcon("send")}<span class="sr-only">Apply</span>`
|
|
1316
1355
|
const encodedSendButtonHtml = JSON.stringify(sendButtonHtml)
|
|
1317
1356
|
const activityLabel = "Comments"
|
|
1318
1357
|
const bodyClass = surface === "codex" ? "codex-review" : "legacy-review"
|
|
@@ -1379,7 +1418,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1379
1418
|
.ref-chip { display: inline-flex; align-items: center; max-width: 32ch; overflow: hidden; text-overflow: ellipsis; margin: 0 2px; padding: 1px 7px; border-radius: 999px; background: var(--ref-bg, #eff6ff); color: var(--ref-text, #1d4ed8); border: 1px solid var(--ref-border, #bfdbfe); font-weight: 800; white-space: nowrap; }
|
|
1380
1419
|
.activity-panel { display: flex; flex: 1 1 auto; flex-direction: column; gap: 8px; min-height: 0; padding-top: 2px; }
|
|
1381
1420
|
.comment-thread { display: flex; flex: 1 1 auto; flex-direction: column; gap: 16px; min-height: 160px; overflow-y: auto; overflow-x: hidden; padding: 12px 14px; }
|
|
1382
|
-
.comment-thread:empty::before { content: "No activity yet.
|
|
1421
|
+
.comment-thread:empty::before { content: "No activity yet. Apply a change to start."; display: block; padding: 14px; border: 1px dashed #cbd5e1; border-radius: 16px; color: #64748b; font-size: 12px; line-height: 1.45; background: #f8fafc; box-shadow: none; }
|
|
1383
1422
|
.comment-bubble { position: relative; display: flex; flex: 0 0 150px; flex-direction: column; min-height: 150px; max-height: 150px; overflow: hidden; border: 1px solid #e2e8f0; border-radius: 15px; padding: 14px 15px 13px 16px; background: #ffffff; color: #334155; font-size: 13px; line-height: 1.45; box-shadow: 0 1px 2px rgba(15,23,42,.04); transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease; cursor: pointer; }
|
|
1384
1423
|
.comment-bubble:hover { transform: translateY(-1px); border-color: #cbd5e1; box-shadow: 0 8px 20px rgba(15,23,42,.07); }
|
|
1385
1424
|
.comment-bubble.active { border-color: #93c5fd; box-shadow: 0 0 0 3px rgba(59,130,246,.12), 0 8px 20px rgba(15,23,42,.08); }
|
|
@@ -1392,6 +1431,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1392
1431
|
.comment-bubble.updated { border-color: #bbf7d0; background: #f7fef9; }
|
|
1393
1432
|
.comment-bubble.stale { border-color: #fed7aa; background: #fffaf0; }
|
|
1394
1433
|
.comment-bubble.failed { border-color: #fecaca; background: #fffafa; }
|
|
1434
|
+
.comment-bubble.stopped { border-color: #fed7aa; background: #fffaf0; }
|
|
1395
1435
|
.comment-bubble-text { flex: 1 1 auto; min-height: 0; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1396
1436
|
.comment-bubble-state { margin-top: 8px; align-self: flex-start; padding: 0; background: transparent; color: #475569; font-size: 11px; font-weight: 800; }
|
|
1397
1437
|
.comment-bubble-meta { margin-bottom: 6px; color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
@@ -1430,6 +1470,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1430
1470
|
.comment-bubble.queued .comment-bubble-state { color: #92400e; }
|
|
1431
1471
|
.comment-bubble.stale .comment-bubble-state { color: #9a3412; }
|
|
1432
1472
|
.comment-bubble.failed .comment-bubble-state { color: #991b1b; }
|
|
1473
|
+
.comment-bubble.stopped .comment-bubble-state { color: #92400e; }
|
|
1433
1474
|
.inspect-actions { display: flex; flex-direction: column; gap: 8px; }
|
|
1434
1475
|
.inspect-options { display: flex; flex-direction: column; gap: 5px; }
|
|
1435
1476
|
.inspect-options label { color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
@@ -1539,7 +1580,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1539
1580
|
</div>
|
|
1540
1581
|
</div>
|
|
1541
1582
|
</div>
|
|
1542
|
-
<button id="send" class="primary-action composer-send" type="button" aria-label="
|
|
1583
|
+
<button id="send" class="primary-action composer-send" type="button" aria-label="Apply" title="Apply" disabled>${sendButtonHtml}</button>
|
|
1543
1584
|
</div>
|
|
1544
1585
|
</div>
|
|
1545
1586
|
</div>
|
|
@@ -2644,21 +2685,29 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2644
2685
|
const asset = state.selectedAsset || undefined;
|
|
2645
2686
|
state.sendingEdit = true;
|
|
2646
2687
|
updateSendState();
|
|
2647
|
-
setStatus('
|
|
2688
|
+
setStatus('Applying comment...');
|
|
2648
2689
|
try {
|
|
2649
2690
|
const res = await fetch('/api/comments?token=' + encodeURIComponent(token), {
|
|
2650
2691
|
method: 'POST',
|
|
2651
2692
|
headers: { 'content-type': 'application/json' },
|
|
2652
|
-
body: JSON.stringify({ comment: text, elements, asset }),
|
|
2693
|
+
body: JSON.stringify({ comment: text, elements, asset, applyNow: true }),
|
|
2653
2694
|
});
|
|
2654
2695
|
const body = await res.json().catch(() => ({}));
|
|
2655
|
-
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to
|
|
2696
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to apply comment');
|
|
2656
2697
|
upsertPersistedComment(body.comment);
|
|
2657
2698
|
clearReferences(false);
|
|
2658
2699
|
state.selectedAsset = null;
|
|
2659
2700
|
els.comment.textContent = '';
|
|
2660
2701
|
renderReferenceOutlines();
|
|
2661
|
-
|
|
2702
|
+
if (body.status === 'queued') {
|
|
2703
|
+
updatePendingCommentStatus(body.comment.id, 'queued', { requestId: '', progressEvent: null, failureRaw: '', failureMessage: '' });
|
|
2704
|
+
setStatus('Comment queued. It will apply after the current edit finishes.');
|
|
2705
|
+
pollQueuedComment(body.comment.id);
|
|
2706
|
+
} else {
|
|
2707
|
+
updatePendingCommentStatus(body.comment.id, 'applying', { requestId: body.commentRequestId || body.requestId || '', baseDeckVersion: body.deckVersion || state.deckVersion });
|
|
2708
|
+
setStatus('Applying comment with Codex...');
|
|
2709
|
+
if (body.commentRequestId || body.requestId) watchCommentProgress(body.comment.id, body.commentRequestId || body.requestId);
|
|
2710
|
+
}
|
|
2662
2711
|
} catch (error) {
|
|
2663
2712
|
reportError(error);
|
|
2664
2713
|
} finally {
|
|
@@ -2711,7 +2760,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2711
2760
|
eventLog: [],
|
|
2712
2761
|
failureRaw: record.lastApplyRaw || '',
|
|
2713
2762
|
failureMessage: record.lastApplyError || '',
|
|
2714
|
-
stopped: record.status === '
|
|
2763
|
+
stopped: record.status === 'stopped',
|
|
2715
2764
|
};
|
|
2716
2765
|
}
|
|
2717
2766
|
|
|
@@ -2719,7 +2768,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2719
2768
|
if (isApplyLocked()) return setStatus(applyLockStatus());
|
|
2720
2769
|
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2721
2770
|
if (!comment || comment.status === 'applying' || comment.status === 'queued') return;
|
|
2722
|
-
updatePendingCommentStatus(commentId, 'applying', { baseDeckVersion: state.deckVersion || comment.baseDeckVersion, progressEvent: null, eventLog: [], failureRaw: '', failureMessage: '' });
|
|
2771
|
+
updatePendingCommentStatus(commentId, 'applying', { stopped: false, baseDeckVersion: state.deckVersion || comment.baseDeckVersion, progressEvent: null, eventLog: [], failureRaw: '', failureMessage: '' });
|
|
2723
2772
|
setStatus(comment.status === 'applied' || comment.status === 'updated' || comment.status === 'stale' ? 'Re-applying saved comment...' : 'Applying saved comment...');
|
|
2724
2773
|
try {
|
|
2725
2774
|
const res = await fetch('/api/comments/' + encodeURIComponent(commentId) + '/apply?token=' + encodeURIComponent(token), {
|
|
@@ -2745,7 +2794,6 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2745
2794
|
}
|
|
2746
2795
|
|
|
2747
2796
|
async function stopPersistedComment(commentId) {
|
|
2748
|
-
if (isApplyLocked()) return setStatus(applyLockStatus());
|
|
2749
2797
|
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2750
2798
|
if (!comment || !canStopPersistedComment(comment.status)) return;
|
|
2751
2799
|
try {
|
|
@@ -2753,7 +2801,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2753
2801
|
const body = await res.json().catch(() => ({}));
|
|
2754
2802
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to stop comment');
|
|
2755
2803
|
if (body.comment) upsertPersistedComment(body.comment);
|
|
2756
|
-
updatePendingCommentStatus(commentId, '
|
|
2804
|
+
updatePendingCommentStatus(commentId, 'stopped', { stopped: true, requestId: '', progressEvent: null, failureRaw: 'Stopped by user.', failureMessage: 'Stopped by user.' });
|
|
2757
2805
|
setStatus('Stopped comment apply.');
|
|
2758
2806
|
} catch (error) {
|
|
2759
2807
|
reportError(error);
|
|
@@ -3044,7 +3092,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3044
3092
|
setMode('edit');
|
|
3045
3093
|
updateSendState();
|
|
3046
3094
|
closeLocalAssetMenu();
|
|
3047
|
-
setStatus('Asset added to the Edit comment. Describe where or how to use it, then
|
|
3095
|
+
setStatus('Asset added to the Edit comment. Describe where or how to use it, then Apply.');
|
|
3048
3096
|
}
|
|
3049
3097
|
|
|
3050
3098
|
function onAssetDragOver(event) {
|
|
@@ -3121,7 +3169,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3121
3169
|
const target = placement?.target ? elementFromPayload(placement.target) : null;
|
|
3122
3170
|
state.assetDropTarget = target;
|
|
3123
3171
|
renderBox(state.assetDropOutline, target);
|
|
3124
|
-
if (placement?.targetLabel) setStatus(placement.targetLabel + '. Drop to
|
|
3172
|
+
if (placement?.targetLabel) setStatus(placement.targetLabel + '. Drop to apply an asset placement edit.');
|
|
3125
3173
|
}
|
|
3126
3174
|
|
|
3127
3175
|
function elementFromPayload(payload) {
|
|
@@ -3252,17 +3300,25 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3252
3300
|
: 'add it near the drop point';
|
|
3253
3301
|
const comment = 'Place workspace asset ' + asset.path + ' on slide ' + placement.slideIndex + ' as a ' + (asset.purpose || 'visual asset') + '; ' + modeText + '. Preserve the current layout and do not cover existing text, charts, tables, or evidence.';
|
|
3254
3302
|
const elements = placement.target ? [placement.target] : [];
|
|
3255
|
-
setStatus('
|
|
3303
|
+
setStatus('Applying asset placement edit...');
|
|
3256
3304
|
try {
|
|
3257
3305
|
const res = await fetch('/api/comments?token=' + encodeURIComponent(token), {
|
|
3258
3306
|
method: 'POST',
|
|
3259
3307
|
headers: { 'content-type': 'application/json' },
|
|
3260
|
-
body: JSON.stringify({ comment, elements, asset, drop: placement }),
|
|
3308
|
+
body: JSON.stringify({ comment, elements, asset, drop: placement, applyNow: true }),
|
|
3261
3309
|
});
|
|
3262
3310
|
const body = await res.json().catch(() => ({}));
|
|
3263
|
-
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to
|
|
3311
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to apply asset placement');
|
|
3264
3312
|
upsertPersistedComment(body.comment);
|
|
3265
|
-
|
|
3313
|
+
if (body.status === 'queued') {
|
|
3314
|
+
updatePendingCommentStatus(body.comment.id, 'queued', { requestId: '', progressEvent: null, failureRaw: '', failureMessage: '' });
|
|
3315
|
+
setStatus('Asset placement queued. It will apply after the current edit finishes.');
|
|
3316
|
+
pollQueuedComment(body.comment.id);
|
|
3317
|
+
} else {
|
|
3318
|
+
updatePendingCommentStatus(body.comment.id, 'applying', { requestId: body.commentRequestId || body.requestId || '', baseDeckVersion: body.deckVersion || state.deckVersion });
|
|
3319
|
+
setStatus('Applying asset placement with Codex...');
|
|
3320
|
+
if (body.commentRequestId || body.requestId) watchCommentProgress(body.comment.id, body.commentRequestId || body.requestId);
|
|
3321
|
+
}
|
|
3266
3322
|
} catch (error) {
|
|
3267
3323
|
reportError(error);
|
|
3268
3324
|
}
|
|
@@ -3358,10 +3414,10 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3358
3414
|
function updatePendingCommentStatus(id, status, updates) {
|
|
3359
3415
|
const comment = state.pendingComments.find((item) => item.id === id);
|
|
3360
3416
|
if (!comment) return;
|
|
3361
|
-
if (comment.status === 'updated' && status !== 'failed') return;
|
|
3417
|
+
if (comment.status === 'updated' && status !== 'failed' && status !== 'stopped') return;
|
|
3362
3418
|
comment.status = status;
|
|
3363
3419
|
if (updates) Object.assign(comment, updates);
|
|
3364
|
-
if (status === 'updated' || status === 'failed' || status === 'applied') comment.progressEvent = null;
|
|
3420
|
+
if (status === 'updated' || status === 'failed' || status === 'applied' || status === 'stopped') comment.progressEvent = null;
|
|
3365
3421
|
renderCommentThread();
|
|
3366
3422
|
}
|
|
3367
3423
|
|
|
@@ -3373,7 +3429,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3373
3429
|
if (!requestId) return;
|
|
3374
3430
|
for (let attempt = 0; attempt < 140; attempt++) {
|
|
3375
3431
|
await delay(1000);
|
|
3376
|
-
if (pendingCommentStatus(commentId) === 'updated') return;
|
|
3432
|
+
if (pendingCommentStatus(commentId) === 'updated' || pendingCommentStatus(commentId) === 'stopped') return;
|
|
3377
3433
|
try {
|
|
3378
3434
|
const res = await fetch('/api/comment-result?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId), { cache: 'no-store' });
|
|
3379
3435
|
const body = await res.json().catch(() => ({}));
|
|
@@ -3383,6 +3439,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3383
3439
|
setStatus(body.error || 'Review agent failed to apply the comment.');
|
|
3384
3440
|
return;
|
|
3385
3441
|
}
|
|
3442
|
+
if (body.status === 'stopped') {
|
|
3443
|
+
updatePendingCommentStatus(commentId, 'stopped', { stopped: true, failureRaw: body.raw || 'Stopped by user.', failureMessage: body.error || 'Stopped by user.' });
|
|
3444
|
+
setStatus(body.error || 'Stopped comment apply.');
|
|
3445
|
+
return;
|
|
3446
|
+
}
|
|
3386
3447
|
if (body.status === 'completed') {
|
|
3387
3448
|
if (pendingCommentStatus(commentId) === 'applying') {
|
|
3388
3449
|
updatePendingCommentStatus(commentId, 'applied', { progressEvent: null });
|
|
@@ -3395,7 +3456,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3395
3456
|
return;
|
|
3396
3457
|
}
|
|
3397
3458
|
}
|
|
3398
|
-
if (pendingCommentStatus(commentId) !== 'updated') {
|
|
3459
|
+
if (pendingCommentStatus(commentId) !== 'updated' && pendingCommentStatus(commentId) !== 'stopped') {
|
|
3399
3460
|
updatePendingCommentStatus(commentId, 'failed');
|
|
3400
3461
|
setStatus('Review agent timed out before applying the comment.');
|
|
3401
3462
|
}
|
|
@@ -3434,6 +3495,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3434
3495
|
source.close();
|
|
3435
3496
|
updatePendingCommentStatus(commentId, 'failed', { failureRaw: payload.detail || '' });
|
|
3436
3497
|
setStatus(payload.message || 'Review agent failed to apply the comment.');
|
|
3498
|
+
} else if (payload.type === 'stopped') {
|
|
3499
|
+
closed = true;
|
|
3500
|
+
source.close();
|
|
3501
|
+
updatePendingCommentStatus(commentId, 'stopped', { stopped: true, failureRaw: payload.detail || 'Stopped by user.', failureMessage: payload.message || 'Stopped by user.' });
|
|
3502
|
+
setStatus(payload.message || 'Stopped comment apply.');
|
|
3437
3503
|
} else if (payload.type === 'completed') {
|
|
3438
3504
|
closed = true;
|
|
3439
3505
|
source.close();
|
|
@@ -3444,7 +3510,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3444
3510
|
});
|
|
3445
3511
|
source.onerror = () => {
|
|
3446
3512
|
source.close();
|
|
3447
|
-
if (!closed && pendingCommentStatus(commentId) !== 'updated' && pendingCommentStatus(commentId) !== 'failed') {
|
|
3513
|
+
if (!closed && pendingCommentStatus(commentId) !== 'updated' && pendingCommentStatus(commentId) !== 'failed' && pendingCommentStatus(commentId) !== 'stopped') {
|
|
3448
3514
|
startFallback();
|
|
3449
3515
|
}
|
|
3450
3516
|
};
|
|
@@ -3453,7 +3519,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3453
3519
|
function recordCommentProgress(commentId, event) {
|
|
3454
3520
|
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
3455
3521
|
if (!comment || !event || !event.message) return;
|
|
3456
|
-
if (comment.stopped || comment.status === '
|
|
3522
|
+
if (comment.stopped || comment.status === 'stopped') return;
|
|
3457
3523
|
if (codexReview) {
|
|
3458
3524
|
appendCodexEventLog(comment, event);
|
|
3459
3525
|
}
|
|
@@ -3594,7 +3660,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3594
3660
|
actions.appendChild(apply);
|
|
3595
3661
|
bubble.appendChild(actions);
|
|
3596
3662
|
}
|
|
3597
|
-
if (
|
|
3663
|
+
if (comment.persisted && canStopPersistedComment(comment.status)) {
|
|
3598
3664
|
const actions = bubble.querySelector('.comment-actions') || document.createElement('div');
|
|
3599
3665
|
actions.className = 'comment-actions';
|
|
3600
3666
|
const stop = commentActionButton('Stop', '${lucideIcon("square", "comment-action-icon")}', 'stop');
|
|
@@ -3703,12 +3769,13 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
3703
3769
|
if (status === 'applying') return 'Applying with Codex...';
|
|
3704
3770
|
if (status === 'applied' || status === 'updated' || status === 'stale') return 'Codex completed';
|
|
3705
3771
|
if (status === 'failed') return 'Failed to apply';
|
|
3772
|
+
if (status === 'stopped') return 'Stopped';
|
|
3706
3773
|
if (status === 'sending') return 'Sending to Review agent...';
|
|
3707
3774
|
return 'Sent to Review agent';
|
|
3708
3775
|
}
|
|
3709
3776
|
|
|
3710
3777
|
function canApplyPersistedComment(status) {
|
|
3711
|
-
return status === 'open' || status === 'failed' || isReapplyStatus(status);
|
|
3778
|
+
return status === 'open' || status === 'failed' || status === 'stopped' || isReapplyStatus(status);
|
|
3712
3779
|
}
|
|
3713
3780
|
|
|
3714
3781
|
function canStopPersistedComment(status) {
|
package/package.json
CHANGED
package/plugins/revela/.mcp.json
CHANGED
|
@@ -526,11 +526,11 @@ function arrayObjectProp(description: string) {
|
|
|
526
526
|
function designAssetsProp() {
|
|
527
527
|
return {
|
|
528
528
|
type: "array",
|
|
529
|
-
description: "Optional design-owned assets to
|
|
529
|
+
description: "Optional design-owned assets for user-uploaded or local materials to archive under assets/**. Each item must use path plus content, contentBase64, or sourcePath.",
|
|
530
530
|
items: {
|
|
531
531
|
type: "object",
|
|
532
532
|
properties: {
|
|
533
|
-
path: { type: "string", description: "Package-relative asset path. Must start with assets/." },
|
|
533
|
+
path: { type: "string", description: "Package-relative asset path for uploaded or local design material. Must start with assets/." },
|
|
534
534
|
content: { type: "string", description: "UTF-8 text asset content." },
|
|
535
535
|
contentBase64: { type: "string", description: "Base64-encoded binary asset content." },
|
|
536
536
|
sourcePath: { type: "string", description: "Local file path to copy into the design asset." },
|
|
@@ -11,6 +11,8 @@ Use this skill when the user asks to create, customize, edit, validate, package,
|
|
|
11
11
|
|
|
12
12
|
- Designs define deck visual systems: rules, foundation, layouts, components, chart rules, and preview coverage.
|
|
13
13
|
- Designs may include package-owned `assets/**` such as cover or closing backgrounds; design tools surface these as design elements, not source evidence.
|
|
14
|
+
- When the user uploads or provides logo, cover, closing, background, texture, brand image, or similar design material, store it inside the design package with `revela_design_draft_create.assets`; use paths under `assets/**` only.
|
|
15
|
+
- Generated `preview.html` must actually reference uploaded design assets with package-relative `assets/...` paths rather than describing them only in text.
|
|
14
16
|
- Default authoring is workspace draft first, then validate, then install only when appropriate.
|
|
15
17
|
- Direct user-level creation is reserved for explicit create/install-now requests.
|
|
16
18
|
- Shareable design archives are `.tar` or `.tar.gz`; install archives only from trusted local paths.
|
|
@@ -30,7 +32,7 @@ For new or edited designs:
|
|
|
30
32
|
1. Call `revela_design_list`.
|
|
31
33
|
2. Read the requested base design or active design with `revela_design_read`.
|
|
32
34
|
3. Draft complete `DESIGN.md` and complete `preview.html` content.
|
|
33
|
-
4. Call `revela_design_draft_create
|
|
35
|
+
4. Call `revela_design_draft_create`; when uploaded or local design material exists, pass `assets: [{ path: "assets/...", contentBase64|content|sourcePath }]` so the files are written into the draft package.
|
|
34
36
|
5. Call `revela_design_draft_validate`.
|
|
35
37
|
6. If validation fails, revise the draft content and repeat draft create/validate.
|
|
36
38
|
7. Call `revela_design_draft_install` only after the draft validates and the user intent is to install it.
|
|
@@ -52,7 +54,9 @@ Use `revela_design_create` only when the user explicitly requests direct local c
|
|
|
52
54
|
- `DESIGN.md` must include valid frontmatter and complete design marker sections.
|
|
53
55
|
- Include design rules, foundation guidance, at least one layout, and at least one component.
|
|
54
56
|
- Optional assets must live under `assets/**`; reference them as package-relative paths like `assets/cover-background.png`.
|
|
57
|
+
- `DESIGN.md` may reference package assets in rules, layouts, or components with `assets/...`; do not reference workspace `assets/` media manifest entries for design-owned visuals.
|
|
55
58
|
- `preview.html` must use the fixed Revela preview canvas contract and visibly preview the design.
|
|
59
|
+
- If design assets are present, `preview.html` must visibly use the saved `assets/...` files, for example a cover hero background or logo image.
|
|
56
60
|
- Preview must include cover and closing examples and showcase every component.
|
|
57
61
|
- Preserve source inspiration and limitations explicitly; do not copy copyrighted design text or assets into the package.
|
|
58
62
|
|
|
@@ -61,6 +65,7 @@ Use `revela_design_create` only when the user explicitly requests direct local c
|
|
|
61
65
|
- Design draft path/status or installed design name.
|
|
62
66
|
- Archive path/status when packaging or installing a shareable design.
|
|
63
67
|
- Asset metadata surfaced by read/inventory tools when `assets/**` exists.
|
|
68
|
+
- Saved asset paths and intended uses, for example `assets/cover-background.png -> cover hero background`.
|
|
64
69
|
- Validation result and any remaining diagnostics.
|
|
65
70
|
- Whether the design was activated.
|
|
66
71
|
- Next step, usually `revela-research` for planning with the design or `revela-make-deck` when a valid `deck-plan.md` already exists.
|