@cyber-dash-tech/revela 0.17.12 → 0.17.14

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 CHANGED
@@ -45,6 +45,13 @@ codex exec --help
45
45
  npx --version
46
46
  ```
47
47
 
48
+ If `npx` fails with an npm cache permission error, repair the cache ownership or use a writable cache for local checks:
49
+
50
+ ```bash
51
+ sudo chown -R "$(id -u):$(id -g)" ~/.npm
52
+ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
53
+ ```
54
+
48
55
  Install Revela through the Codex Git marketplace:
49
56
 
50
57
  ```bash
@@ -58,6 +65,8 @@ You do not need to run `bun install` inside the Codex marketplace clone.
58
65
 
59
66
  Start a new Codex thread after installing so Codex loads the Revela skills, MCP tools, and hooks.
60
67
 
68
+ For release-aligned local validation, run `bun run smoke:mcp-pack`. It packs the current checkout to a temporary npm tarball and starts the MCP server through `npx`, matching the published Codex launcher path without requiring a registry publish.
69
+
61
70
  ## Built-In Designs
62
71
 
63
72
  Revela includes built-in deck designs:
package/README.zh-CN.md CHANGED
@@ -45,6 +45,13 @@ codex exec --help
45
45
  npx --version
46
46
  ```
47
47
 
48
+ 如果 `npx` 报 npm cache 权限错误,可以修复 cache owner,或在本地检查时使用可写 cache:
49
+
50
+ ```bash
51
+ sudo chown -R "$(id -u):$(id -g)" ~/.npm
52
+ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
53
+ ```
54
+
48
55
  通过 Codex Git marketplace 安装 Revela:
49
56
 
50
57
  ```bash
@@ -58,6 +65,8 @@ Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。
58
65
 
59
66
  安装后开启一个新的 Codex thread,让 Codex 加载 Revela 的 skills、MCP tools 和 hooks。
60
67
 
68
+ 如果要按发布路径做本地验证,运行 `bun run smoke:mcp-pack`。它会把当前 checkout 打成临时 npm tarball,再通过 `npx` 启动 MCP server,不需要先发布到 registry。
69
+
61
70
  ## 内置设计
62
71
 
63
72
  Revela 内置多个 deck design:
@@ -1,5 +1,6 @@
1
1
  import type { InspectionPromptProjection } from "../inspection-context/project"
2
2
  import { buildDeterministicInspectionResult, type InspectionResult } from "../inspection-context/result"
3
+ import type { ReviewBridgeEvent } from "../refine/prompt-bridge"
3
4
 
4
5
  export type InspectRequestStatus = "pending" | "completed" | "failed" | "expired"
5
6
 
@@ -10,12 +11,15 @@ export interface PendingInspectRequest {
10
11
  deckVersion: string
11
12
  createdAt: number
12
13
  updatedAt: number
14
+ events: ReviewBridgeEvent[]
13
15
  result?: InspectionResult
14
16
  error?: string
17
+ raw?: string
15
18
  }
16
19
 
17
20
  const REQUEST_TTL_MS = 90 * 1000
18
21
  const requests = new Map<string, PendingInspectRequest>()
22
+ const subscribers = new Map<string, Set<(event: ReviewBridgeEvent) => void>>()
19
23
 
20
24
  export function createInspectRequest(input: {
21
25
  requestId: string
@@ -31,6 +35,7 @@ export function createInspectRequest(input: {
31
35
  deckVersion: input.deckVersion,
32
36
  createdAt: now,
33
37
  updatedAt: now,
38
+ events: [],
34
39
  }
35
40
  requests.set(input.requestId, request)
36
41
  return request
@@ -43,6 +48,11 @@ export function getInspectRequest(requestId: string): PendingInspectRequest | un
43
48
  if (request.status === "pending" && Date.now() - request.createdAt > REQUEST_TTL_MS) {
44
49
  request.status = "expired"
45
50
  request.error = "Inspection timed out before the LLM submitted a result."
51
+ appendInspectRequestEvent(request, {
52
+ type: "timeout",
53
+ message: request.error,
54
+ timestamp: Date.now(),
55
+ })
46
56
  request.updatedAt = Date.now()
47
57
  }
48
58
  return request
@@ -55,6 +65,13 @@ export function completeInspectRequest(requestId: string, result: InspectionResu
55
65
  request.status = "completed"
56
66
  request.result = normalizeInspectionResult(request.projection, result, requestId)
57
67
  request.updatedAt = Date.now()
68
+ if (!hasTerminalEvent(request)) {
69
+ appendInspectRequestEvent(request, {
70
+ type: "completed",
71
+ message: "Codex completed the inspection.",
72
+ timestamp: request.updatedAt,
73
+ })
74
+ }
58
75
  return request
59
76
  }
60
77
 
@@ -77,29 +94,92 @@ function normalizeInspectionResult(
77
94
  }
78
95
  }
79
96
 
80
- export function failInspectRequest(requestId: string, error: string): PendingInspectRequest | undefined {
97
+ export function failInspectRequest(requestId: string, error: string, raw?: string): PendingInspectRequest | undefined {
81
98
  const request = getInspectRequest(requestId)
82
99
  if (!request || request.status !== "pending") return request
83
100
  request.status = "failed"
84
101
  request.error = error
102
+ if (raw) request.raw = boundedTail(raw)
103
+ request.updatedAt = Date.now()
104
+ if (!hasTerminalEvent(request)) {
105
+ appendInspectRequestEvent(request, {
106
+ type: "failed",
107
+ message: error,
108
+ timestamp: request.updatedAt,
109
+ ...(raw ? { detail: boundedTail(raw) } : {}),
110
+ })
111
+ }
112
+ return request
113
+ }
114
+
115
+ export function addInspectRequestEvent(requestId: string, event: ReviewBridgeEvent): PendingInspectRequest | undefined {
116
+ const request = getInspectRequest(requestId)
117
+ if (!request) return undefined
118
+ appendInspectRequestEvent(request, event)
85
119
  request.updatedAt = Date.now()
86
120
  return request
87
121
  }
88
122
 
123
+ export function subscribeInspectRequestEvents(
124
+ requestId: string,
125
+ listener: (event: ReviewBridgeEvent) => void,
126
+ ): () => void {
127
+ const set = subscribers.get(requestId) ?? new Set<(event: ReviewBridgeEvent) => void>()
128
+ set.add(listener)
129
+ subscribers.set(requestId, set)
130
+ return () => {
131
+ set.delete(listener)
132
+ if (set.size === 0) subscribers.delete(requestId)
133
+ }
134
+ }
135
+
89
136
  export function cleanupInspectRequests(now = Date.now()): void {
90
137
  for (const [requestId, request] of requests) {
91
138
  if (request.status === "pending" && now - request.createdAt > REQUEST_TTL_MS) {
92
139
  request.status = "expired"
93
140
  request.error = "Inspection timed out before the LLM submitted a result."
94
141
  request.updatedAt = now
142
+ appendInspectRequestEvent(request, {
143
+ type: "timeout",
144
+ message: request.error,
145
+ timestamp: now,
146
+ })
95
147
  continue
96
148
  }
97
149
  if (request.status !== "pending" && now - request.updatedAt > REQUEST_TTL_MS) {
98
150
  requests.delete(requestId)
151
+ subscribers.delete(requestId)
99
152
  }
100
153
  }
101
154
  }
102
155
 
103
156
  export function clearInspectRequestsForTests(): void {
104
157
  requests.clear()
158
+ subscribers.clear()
159
+ }
160
+
161
+ function appendInspectRequestEvent(request: PendingInspectRequest, event: ReviewBridgeEvent): void {
162
+ const previous = request.events.at(-1)
163
+ if (
164
+ previous
165
+ && previous.type === event.type
166
+ && previous.message === event.message
167
+ && previous.detail === event.detail
168
+ && Math.abs(previous.timestamp - event.timestamp) < 100
169
+ ) {
170
+ return
171
+ }
172
+ request.events.push(event)
173
+ const set = subscribers.get(request.requestId)
174
+ if (!set) return
175
+ for (const listener of set) listener(event)
176
+ }
177
+
178
+ function hasTerminalEvent(request: PendingInspectRequest): boolean {
179
+ return request.events.some((event) => event.type === "completed" || event.type === "failed" || event.type === "timeout")
180
+ }
181
+
182
+ function boundedTail(text: string, limit = 4096): string {
183
+ if (text.length <= limit) return text
184
+ return text.slice(text.length - limit)
105
185
  }
@@ -1,3 +1,5 @@
1
+ import type { ReviewBridgeEvent } from "./prompt-bridge"
2
+
1
3
  export type CommentRequestStatus = "pending" | "completed" | "failed" | "expired"
2
4
 
3
5
  export interface PendingCommentRequest {
@@ -6,11 +8,14 @@ export interface PendingCommentRequest {
6
8
  deckVersion: string
7
9
  createdAt: number
8
10
  updatedAt: number
11
+ events: ReviewBridgeEvent[]
9
12
  error?: string
13
+ raw?: string
10
14
  }
11
15
 
12
16
  const REQUEST_TTL_MS = 120 * 1000
13
17
  const requests = new Map<string, PendingCommentRequest>()
18
+ const subscribers = new Map<string, Set<(event: ReviewBridgeEvent) => void>>()
14
19
 
15
20
  export function createCommentRequest(input: {
16
21
  requestId: string
@@ -24,6 +29,7 @@ export function createCommentRequest(input: {
24
29
  deckVersion: input.deckVersion,
25
30
  createdAt: now,
26
31
  updatedAt: now,
32
+ events: [],
27
33
  }
28
34
  requests.set(input.requestId, request)
29
35
  return request
@@ -36,6 +42,11 @@ export function getCommentRequest(requestId: string): PendingCommentRequest | un
36
42
  if (request.status === "pending" && Date.now() - request.createdAt > REQUEST_TTL_MS) {
37
43
  request.status = "expired"
38
44
  request.error = "Review agent timed out before completing the comment request."
45
+ appendCommentRequestEvent(request, {
46
+ type: "timeout",
47
+ message: request.error,
48
+ timestamp: Date.now(),
49
+ })
39
50
  request.updatedAt = Date.now()
40
51
  }
41
52
  return request
@@ -46,32 +57,102 @@ export function completeCommentRequest(requestId: string): PendingCommentRequest
46
57
  if (!request || request.status !== "pending") return request
47
58
  request.status = "completed"
48
59
  request.updatedAt = Date.now()
60
+ if (!hasTerminalEvent(request)) {
61
+ appendCommentRequestEvent(request, {
62
+ type: "completed",
63
+ message: "Codex completed.",
64
+ timestamp: request.updatedAt,
65
+ })
66
+ }
49
67
  return request
50
68
  }
51
69
 
52
- export function failCommentRequest(requestId: string, error: string): PendingCommentRequest | undefined {
70
+ export function failCommentRequest(requestId: string, error: string, raw?: string): PendingCommentRequest | undefined {
53
71
  const request = getCommentRequest(requestId)
54
72
  if (!request || request.status !== "pending") return request
55
73
  request.status = "failed"
56
74
  request.error = error
75
+ if (raw) request.raw = boundedTail(raw)
57
76
  request.updatedAt = Date.now()
77
+ if (!hasTerminalEvent(request)) {
78
+ appendCommentRequestEvent(request, {
79
+ type: "failed",
80
+ message: error,
81
+ timestamp: request.updatedAt,
82
+ ...(raw ? { detail: boundedTail(raw) } : {}),
83
+ })
84
+ }
58
85
  return request
59
86
  }
60
87
 
88
+ function boundedTail(text: string, limit = 4096): string {
89
+ if (text.length <= limit) return text
90
+ return text.slice(text.length - limit)
91
+ }
92
+
93
+ export function addCommentRequestEvent(requestId: string, event: ReviewBridgeEvent): PendingCommentRequest | undefined {
94
+ const request = getCommentRequest(requestId)
95
+ if (!request) return undefined
96
+ appendCommentRequestEvent(request, event)
97
+ request.updatedAt = Date.now()
98
+ return request
99
+ }
100
+
101
+ export function subscribeCommentRequestEvents(
102
+ requestId: string,
103
+ listener: (event: ReviewBridgeEvent) => void,
104
+ ): () => void {
105
+ const set = subscribers.get(requestId) ?? new Set<(event: ReviewBridgeEvent) => void>()
106
+ set.add(listener)
107
+ subscribers.set(requestId, set)
108
+ return () => {
109
+ set.delete(listener)
110
+ if (set.size === 0) subscribers.delete(requestId)
111
+ }
112
+ }
113
+
61
114
  export function cleanupCommentRequests(now = Date.now()): void {
62
115
  for (const [requestId, request] of requests) {
63
116
  if (request.status === "pending" && now - request.createdAt > REQUEST_TTL_MS) {
64
117
  request.status = "expired"
65
118
  request.error = "Review agent timed out before completing the comment request."
66
119
  request.updatedAt = now
120
+ appendCommentRequestEvent(request, {
121
+ type: "timeout",
122
+ message: request.error,
123
+ timestamp: now,
124
+ })
67
125
  continue
68
126
  }
69
127
  if (request.status !== "pending" && now - request.updatedAt > REQUEST_TTL_MS) {
70
128
  requests.delete(requestId)
129
+ subscribers.delete(requestId)
71
130
  }
72
131
  }
73
132
  }
74
133
 
75
134
  export function clearCommentRequestsForTests(): void {
76
135
  requests.clear()
136
+ subscribers.clear()
137
+ }
138
+
139
+ function appendCommentRequestEvent(request: PendingCommentRequest, event: ReviewBridgeEvent): void {
140
+ const previous = request.events.at(-1)
141
+ if (
142
+ previous
143
+ && previous.type === event.type
144
+ && previous.message === event.message
145
+ && previous.detail === event.detail
146
+ && Math.abs(previous.timestamp - event.timestamp) < 100
147
+ ) {
148
+ return
149
+ }
150
+ request.events.push(event)
151
+ const set = subscribers.get(request.requestId)
152
+ if (!set) return
153
+ for (const listener of set) listener(event)
154
+ }
155
+
156
+ function hasTerminalEvent(request: PendingCommentRequest): boolean {
157
+ return request.events.some((event) => event.type === "completed" || event.type === "failed" || event.type === "timeout")
77
158
  }
@@ -9,7 +9,7 @@ import { openUrl } from "../edit/open"
9
9
  import { resolveEditableDeck, type EditableDeck } from "../edit/resolve-deck"
10
10
  import { buildPrompt } from "../prompt-builder"
11
11
  import type { ReviewPromptBridge } from "./prompt-bridge"
12
- import { startRefineServer, type RefineMode } from "./server"
12
+ import { startRefineServer, type RefineMode, type ReviewShellSurface } from "./server"
13
13
 
14
14
  export interface OpenRefineDeckResult {
15
15
  deck: EditableDeck
@@ -35,6 +35,7 @@ export interface OpenRefineDeckOptions {
35
35
  openBrowser?: boolean
36
36
  openUrl?: (url: string) => void
37
37
  promptBridge?: ReviewPromptBridge
38
+ surface?: ReviewShellSurface
38
39
  }
39
40
 
40
41
  export function openRefineDeck(target: string, options: OpenRefineDeckOptions): OpenRefineDeckResult {
@@ -74,7 +75,8 @@ function openRefineDeckInternal(
74
75
  mode,
75
76
  promptBridge: options.promptBridge,
76
77
  })
77
- const url = `${refineServer.baseUrl}/refine?token=${encodeURIComponent(session.token)}`
78
+ const route = options.surface === "codex" ? "/codex-review" : "/refine"
79
+ const url = `${refineServer.baseUrl}${route}?token=${encodeURIComponent(session.token)}`
78
80
  const shouldOpen = options.openBrowser !== false && !(behavior.skipLiveSession && session.live)
79
81
  if (shouldOpen) (options.openUrl ?? openUrl)(url)
80
82
 
@@ -4,6 +4,13 @@ import type { InspectionResult } from "../inspection-context/result"
4
4
  export type ReviewPromptAction = "comment" | "inspect"
5
5
  export type ReviewPromptBridgeKind = "opencode" | "codex-exec"
6
6
 
7
+ export type ReviewBridgeEvent = {
8
+ type: "started" | "codex_event" | "stdout" | "stderr" | "completed" | "failed" | "timeout"
9
+ message: string
10
+ timestamp: number
11
+ detail?: string
12
+ }
13
+
7
14
  export interface ReviewPromptInput {
8
15
  action: ReviewPromptAction
9
16
  prompt: string
@@ -11,6 +18,7 @@ export interface ReviewPromptInput {
11
18
  file: string
12
19
  requestId?: string
13
20
  timeoutMs?: number
21
+ onEvent?: (event: ReviewBridgeEvent) => void
14
22
  }
15
23
 
16
24
  export type ReviewPromptResult =
@@ -35,6 +43,7 @@ export type CodexExecRunner = (input: {
35
43
  timeoutMs: number
36
44
  sandboxMode: "read-only" | "workspace-write"
37
45
  skipGitRepoCheck: boolean
46
+ onEvent?: (event: ReviewBridgeEvent) => void
38
47
  }) => Promise<CodexExecRunResult>
39
48
 
40
49
  export function createOpenCodeReviewPromptBridge(client: any, sessionID: string): ReviewPromptBridge {
@@ -69,6 +78,7 @@ export function createCodexExecReviewPromptBridge(options: {
69
78
  kind: "codex-exec",
70
79
  async send(input) {
71
80
  const sandboxMode = input.action === "comment" ? "workspace-write" : "read-only"
81
+ input.onEvent?.(bridgeEvent("started", "Starting Codex..."))
72
82
  const output = await runner({
73
83
  action: input.action,
74
84
  prompt: input.prompt,
@@ -76,9 +86,11 @@ export function createCodexExecReviewPromptBridge(options: {
76
86
  timeoutMs: input.timeoutMs ?? timeoutMs,
77
87
  sandboxMode,
78
88
  skipGitRepoCheck: true,
89
+ onEvent: input.onEvent,
79
90
  })
80
91
  const raw = [output.stdout, output.stderr].filter(Boolean).join("\n")
81
92
  if (output.exitCode !== 0) {
93
+ input.onEvent?.(bridgeEvent("failed", `codex exec failed with exit code ${output.exitCode ?? "unknown"}.`, boundedTail(raw)))
82
94
  return {
83
95
  ok: false,
84
96
  status: "failed",
@@ -87,6 +99,7 @@ export function createCodexExecReviewPromptBridge(options: {
87
99
  }
88
100
  }
89
101
  if (input.action === "comment" && isCodexWriteBlocked(raw)) {
102
+ input.onEvent?.(bridgeEvent("failed", "codex exec could not write the deck because its sandbox blocked file changes.", boundedTail(raw)))
90
103
  return {
91
104
  ok: false,
92
105
  status: "failed",
@@ -94,9 +107,13 @@ export function createCodexExecReviewPromptBridge(options: {
94
107
  raw,
95
108
  }
96
109
  }
97
- if (input.action === "comment") return { ok: true, status: "completed", raw }
110
+ if (input.action === "comment") {
111
+ input.onEvent?.(bridgeEvent("completed", "Codex completed."))
112
+ return { ok: true, status: "completed", raw }
113
+ }
98
114
  const result = extractInspectionResult(output.stdout)
99
115
  if (!result) {
116
+ input.onEvent?.(bridgeEvent("failed", "codex exec did not return a valid inspection result JSON object.", boundedTail(raw)))
100
117
  return {
101
118
  ok: false,
102
119
  status: "failed",
@@ -104,6 +121,7 @@ export function createCodexExecReviewPromptBridge(options: {
104
121
  raw,
105
122
  }
106
123
  }
124
+ input.onEvent?.(bridgeEvent("completed", "Codex completed the inspection."))
107
125
  return { ok: true, status: "completed", result, raw }
108
126
  },
109
127
  }
@@ -116,6 +134,7 @@ async function runCodexExec(input: {
116
134
  timeoutMs: number
117
135
  sandboxMode: "read-only" | "workspace-write"
118
136
  skipGitRepoCheck: boolean
137
+ onEvent?: (event: ReviewBridgeEvent) => void
119
138
  }): Promise<CodexExecRunResult> {
120
139
  return new Promise((resolve) => {
121
140
  const args = ["exec", "--json", "--ephemeral"]
@@ -126,31 +145,88 @@ async function runCodexExec(input: {
126
145
  })
127
146
  let stdout = ""
128
147
  let stderr = ""
148
+ let stdoutLineBuffer = ""
149
+ let resolved = false
150
+ const resolveOnce = (output: CodexExecRunResult) => {
151
+ if (resolved) return
152
+ resolved = true
153
+ resolve(output)
154
+ }
129
155
  const timer = setTimeout(() => {
130
156
  child.kill()
131
- resolve({
157
+ const nextStderr = `${stderr}${stderr ? "\n" : ""}codex exec timed out after ${input.timeoutMs}ms.`
158
+ input.onEvent?.(bridgeEvent("timeout", "Codex timed out before completing.", boundedTail(nextStderr)))
159
+ resolveOnce({
132
160
  exitCode: 124,
133
161
  stdout,
134
- stderr: `${stderr}${stderr ? "\n" : ""}codex exec timed out after ${input.timeoutMs}ms.`,
162
+ stderr: nextStderr,
135
163
  })
136
164
  }, input.timeoutMs)
137
165
  child.stdout?.on("data", (chunk) => {
138
- stdout += chunk.toString()
166
+ const text = chunk.toString()
167
+ stdout += text
168
+ stdoutLineBuffer = emitCodexJsonProgress(stdoutLineBuffer + text, input.action, input.onEvent)
139
169
  })
140
170
  child.stderr?.on("data", (chunk) => {
141
- stderr += chunk.toString()
171
+ const text = chunk.toString()
172
+ stderr += text
173
+ input.onEvent?.(bridgeEvent("stderr", "Codex wrote diagnostic output.", boundedTail(text)))
142
174
  })
143
175
  child.on("error", (error) => {
144
176
  clearTimeout(timer)
145
- resolve({ exitCode: 127, stdout, stderr: error.message })
177
+ input.onEvent?.(bridgeEvent("failed", "Failed to start codex exec.", boundedTail(error.message)))
178
+ resolveOnce({ exitCode: 127, stdout, stderr: error.message })
146
179
  })
147
180
  child.on("close", (code) => {
148
181
  clearTimeout(timer)
149
- resolve({ exitCode: code, stdout, stderr })
182
+ emitCodexJsonProgress(`${stdoutLineBuffer}\n`, input.action, input.onEvent)
183
+ resolveOnce({ exitCode: code, stdout, stderr })
150
184
  })
151
185
  })
152
186
  }
153
187
 
188
+ function emitCodexJsonProgress(buffer: string, action: ReviewPromptAction, onEvent?: (event: ReviewBridgeEvent) => void): string {
189
+ const lines = buffer.split(/\r?\n/)
190
+ const remainder = lines.pop() ?? ""
191
+ for (const line of lines) {
192
+ const parsed = parseJson(line)
193
+ const message = codexProgressMessage(parsed, action)
194
+ if (message) {
195
+ onEvent?.(bridgeEvent("codex_event", message, boundedTail(line)))
196
+ } else if (parsed === undefined && line.trim()) {
197
+ onEvent?.(bridgeEvent("stdout", "Codex wrote output.", boundedTail(line)))
198
+ }
199
+ }
200
+ return remainder
201
+ }
202
+
203
+ function codexProgressMessage(value: unknown, action: ReviewPromptAction): string | undefined {
204
+ if (!value || typeof value !== "object") return undefined
205
+ const record = value as Record<string, unknown>
206
+ const type = typeof record.type === "string" ? record.type.toLowerCase() : ""
207
+ const event = typeof record.event === "string" ? record.event.toLowerCase() : ""
208
+ const name = `${type} ${event}`
209
+ if (!name.trim()) return undefined
210
+ if (name.includes("turn_completed") || name.includes("completed")) {
211
+ return action === "comment" ? undefined : "Codex completed the inspection."
212
+ }
213
+ if (name.includes("exec") || name.includes("patch") || name.includes("tool") || name.includes("apply")) {
214
+ return action === "comment" ? "Codex is applying the requested edit..." : "Codex is reading the deck..."
215
+ }
216
+ if (name.includes("session") || name.includes("turn") || name.includes("start")) return "Codex is reading the deck..."
217
+ if (name.includes("message") || name.includes("delta") || name.includes("agent")) return "Codex is working..."
218
+ return "Codex is working..."
219
+ }
220
+
221
+ function bridgeEvent(type: ReviewBridgeEvent["type"], message: string, detail?: string): ReviewBridgeEvent {
222
+ return { type, message, timestamp: Date.now(), ...(detail ? { detail } : {}) }
223
+ }
224
+
225
+ function boundedTail(text: string, limit = 4096): string {
226
+ if (text.length <= limit) return text
227
+ return text.slice(text.length - limit)
228
+ }
229
+
154
230
  function isCodexWriteBlocked(raw: string): boolean {
155
231
  const text = raw.toLowerCase()
156
232
  return (
@@ -9,11 +9,11 @@ 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 { completeInspectRequest, createInspectRequest, failInspectRequest, getInspectRequest } from "../inspect/requests"
12
+ import { addInspectRequestEvent, completeInspectRequest, createInspectRequest, failInspectRequest, getInspectRequest, subscribeInspectRequestEvents } 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"
16
+ import { addCommentRequestEvent, completeCommentRequest, createCommentRequest, failCommentRequest, getCommentRequest, subscribeCommentRequestEvents } from "./comment-requests"
17
17
  import { createOpenCodeReviewPromptBridge, type ReviewPromptBridge } from "./prompt-bridge"
18
18
  import { suppressReviewApplyFixArtifactQa } from "./qa-suppression"
19
19
  import { annotateVisualEditTargets, applyVisualTargetChanges, type VisualEditTarget } from "./visual-targets"
@@ -48,6 +48,7 @@ interface EditSession {
48
48
  }
49
49
 
50
50
  export type RefineMode = "edit" | "inspect"
51
+ export type ReviewShellSurface = "legacy" | "codex"
51
52
 
52
53
  export interface RefineServerHandle {
53
54
  baseUrl: string
@@ -177,6 +178,12 @@ async function handleRequest(req: Request): Promise<Response> {
177
178
  return htmlResponse(renderRefineShell(session.value.token, session.value.defaultMode))
178
179
  }
179
180
 
181
+ if (url.pathname === "/codex-review" && req.method === "GET") {
182
+ const session = validateSession(url.searchParams.get("token"))
183
+ if (!session.ok) return session.response
184
+ return htmlResponse(renderCodexReviewShell(session.value.token, session.value.defaultMode))
185
+ }
186
+
180
187
  if (url.pathname === "/deck" && req.method === "GET") {
181
188
  const session = validateSession(url.searchParams.get("token"))
182
189
  if (!session.ok) return session.response
@@ -201,6 +208,12 @@ async function handleRequest(req: Request): Promise<Response> {
201
208
  return handleCommentResult(url.searchParams.get("requestId"), session.value)
202
209
  }
203
210
 
211
+ if (url.pathname === "/api/comment-events" && req.method === "GET") {
212
+ const session = validateSession(url.searchParams.get("token"))
213
+ if (!session.ok) return session.response
214
+ return handleCommentEvents(url.searchParams.get("requestId"), session.value)
215
+ }
216
+
204
217
  if (url.pathname === "/api/inspect" && req.method === "POST") {
205
218
  const session = validateSession(url.searchParams.get("token"))
206
219
  if (!session.ok) return session.response
@@ -213,6 +226,12 @@ async function handleRequest(req: Request): Promise<Response> {
213
226
  return handleInspectResult(url.searchParams.get("requestId"), session.value)
214
227
  }
215
228
 
229
+ if (url.pathname === "/api/inspect-events" && req.method === "GET") {
230
+ const session = validateSession(url.searchParams.get("token"))
231
+ if (!session.ok) return session.response
232
+ return handleInspectEvents(url.searchParams.get("requestId"), session.value)
233
+ }
234
+
216
235
  if (url.pathname === "/api/deck-version" && req.method === "GET") {
217
236
  const session = validateSession(url.searchParams.get("token"))
218
237
  if (!session.ok) return session.response
@@ -474,11 +493,7 @@ function handleDeck(session: EditSession): Response {
474
493
  session.assets.clear()
475
494
  session.assetKeys.clear()
476
495
  session.nextAssetId = 1
477
- const sourceHtml = readFileSync(session.absoluteFile, "utf-8")
478
- const version = readDeckVersion(session).version
479
- const annotated = annotateVisualEditTargets(sourceHtml)
480
- session.visualTargets = annotated.targets
481
- session.visualTargetDeckVersion = version
496
+ const annotated = refreshVisualTargets(session)
482
497
  return htmlResponse(rewriteLocalAssetRefs(annotated.html, {
483
498
  session,
484
499
  sourceFile: session.absoluteFile,
@@ -486,6 +501,15 @@ function handleDeck(session: EditSession): Response {
486
501
  }))
487
502
  }
488
503
 
504
+ function refreshVisualTargets(session: EditSession): { html: string; version: string } {
505
+ const sourceHtml = readFileSync(session.absoluteFile, "utf-8")
506
+ const version = readDeckVersion(session).version
507
+ const annotated = annotateVisualEditTargets(sourceHtml)
508
+ session.visualTargets = annotated.targets
509
+ session.visualTargetDeckVersion = version
510
+ return { html: annotated.html, version }
511
+ }
512
+
489
513
  function handleAsset(session: EditSession, id: string | null, method: string): Response {
490
514
  if (!id) return textResponse("Missing asset id", 400)
491
515
  const asset = session.assets.get(id)
@@ -707,9 +731,10 @@ async function handleVisualChanges(req: Request, session: EditSession): Promise<
707
731
  targets: session.visualTargets,
708
732
  changes,
709
733
  })
734
+ const refreshed = refreshVisualTargets(session)
710
735
  session.lastActiveAt = Date.now()
711
736
  scheduleIdleStop()
712
- return jsonResponse({ ok: true, deckVersion: result.deckVersion, changeCount: result.changeCount })
737
+ return jsonResponse({ ok: true, deckVersion: refreshed.version, changeCount: result.changeCount })
713
738
  } catch (error) {
714
739
  const message = error instanceof Error ? error.message : String(error)
715
740
  return jsonResponse({ ok: false, error: message }, 400)
@@ -765,11 +790,12 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
765
790
  workspaceRoot: session.workspaceRoot,
766
791
  file: session.file,
767
792
  requestId,
793
+ onEvent: (event) => addCommentRequestEvent(requestId, event),
768
794
  }).then((result) => {
769
795
  if (result.ok) {
770
796
  completeCommentRequest(requestId)
771
797
  } else {
772
- failCommentRequest(requestId, result.error)
798
+ failCommentRequest(requestId, result.error, result.raw)
773
799
  }
774
800
  }).catch((error: unknown) => {
775
801
  const message = error instanceof Error ? error.message : String(error)
@@ -781,6 +807,88 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
781
807
  return jsonResponse({ ok: true, requestId, commentRequestId: requestId, deckVersion, status: "pending" })
782
808
  }
783
809
 
810
+ function handleCommentEvents(requestId: string | null, session: EditSession): Response {
811
+ if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400)
812
+ const request = getCommentRequest(requestId)
813
+ if (!request) return jsonResponse({ ok: false, requestId, error: "Comment request not found" }, 404)
814
+ session.lastActiveAt = Date.now()
815
+ scheduleIdleStop()
816
+
817
+ const encoder = new TextEncoder()
818
+ let unsubscribe = () => {}
819
+ const stream = new ReadableStream<Uint8Array>({
820
+ start(controller) {
821
+ const send = (event: unknown) => {
822
+ controller.enqueue(encoder.encode(`event: progress\ndata: ${JSON.stringify(event)}\n\n`))
823
+ }
824
+ for (const event of request.events) send(event)
825
+ if (request.status !== "pending") {
826
+ controller.close()
827
+ return
828
+ }
829
+ unsubscribe = subscribeCommentRequestEvents(requestId, (event) => {
830
+ send(event)
831
+ if (event.type === "completed" || event.type === "failed" || event.type === "timeout") {
832
+ unsubscribe()
833
+ controller.close()
834
+ }
835
+ })
836
+ },
837
+ cancel() {
838
+ unsubscribe()
839
+ },
840
+ })
841
+
842
+ return new Response(stream, {
843
+ headers: {
844
+ "content-type": "text/event-stream; charset=utf-8",
845
+ "cache-control": "no-store",
846
+ connection: "keep-alive",
847
+ },
848
+ })
849
+ }
850
+
851
+ function handleInspectEvents(requestId: string | null, session: EditSession): Response {
852
+ if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400)
853
+ const request = getInspectRequest(requestId)
854
+ if (!request) return jsonResponse({ ok: false, requestId, error: "Inspection request not found" }, 404)
855
+ session.lastActiveAt = Date.now()
856
+ scheduleIdleStop()
857
+
858
+ const encoder = new TextEncoder()
859
+ let unsubscribe = () => {}
860
+ const stream = new ReadableStream<Uint8Array>({
861
+ start(controller) {
862
+ const send = (event: unknown) => {
863
+ controller.enqueue(encoder.encode(`event: progress\ndata: ${JSON.stringify(event)}\n\n`))
864
+ }
865
+ for (const event of request.events) send(event)
866
+ if (request.status !== "pending") {
867
+ controller.close()
868
+ return
869
+ }
870
+ unsubscribe = subscribeInspectRequestEvents(requestId, (event) => {
871
+ send(event)
872
+ if (event.type === "completed" || event.type === "failed" || event.type === "timeout") {
873
+ unsubscribe()
874
+ controller.close()
875
+ }
876
+ })
877
+ },
878
+ cancel() {
879
+ unsubscribe()
880
+ },
881
+ })
882
+
883
+ return new Response(stream, {
884
+ headers: {
885
+ "content-type": "text/event-stream; charset=utf-8",
886
+ "cache-control": "no-store",
887
+ connection: "keep-alive",
888
+ },
889
+ })
890
+ }
891
+
784
892
  function handleCommentResult(requestId: string | null, session: EditSession): Response {
785
893
  if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400)
786
894
  const request = getCommentRequest(requestId)
@@ -788,7 +896,14 @@ function handleCommentResult(requestId: string | null, session: EditSession): Re
788
896
  session.lastActiveAt = Date.now()
789
897
  scheduleIdleStop()
790
898
  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" })
899
+ return jsonResponse({
900
+ ok: true,
901
+ requestId,
902
+ status: request.status,
903
+ deckVersion: request.deckVersion,
904
+ error: request.error || "Review agent failed",
905
+ raw: request.raw,
906
+ })
792
907
  }
793
908
  return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion })
794
909
  }
@@ -834,11 +949,12 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
834
949
  workspaceRoot: session.workspaceRoot,
835
950
  file: session.file,
836
951
  requestId,
952
+ onEvent: (event) => addInspectRequestEvent(requestId, event),
837
953
  }).then((result) => {
838
954
  if (result.ok && result.result) {
839
955
  completeInspectRequest(requestId, result.result)
840
956
  } else if (!result.ok) {
841
- failInspectRequest(requestId, result.error)
957
+ failInspectRequest(requestId, result.error, result.raw)
842
958
  }
843
959
  }).catch((error: unknown) => {
844
960
  const message = error instanceof Error ? error.message : String(error)
@@ -860,7 +976,7 @@ function handleInspectResult(requestId: string | null, session: EditSession): Re
860
976
  session.lastActiveAt = Date.now()
861
977
  scheduleIdleStop()
862
978
  if (request.status === "completed") return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion, result: request.result })
863
- if (request.status === "failed" || request.status === "expired") return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion, error: request.error || "Insight failed" })
979
+ if (request.status === "failed" || request.status === "expired") return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion, error: request.error || "Insight failed", raw: request.raw })
864
980
  return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion })
865
981
  }
866
982
 
@@ -964,9 +1080,16 @@ function jsonResponse(body: unknown, status = 200): Response {
964
1080
  })
965
1081
  }
966
1082
 
967
- export function renderRefineShell(token: string, defaultMode: RefineMode = "edit"): string {
1083
+ export function renderCodexReviewShell(token: string, defaultMode: RefineMode = "edit"): string {
1084
+ return renderRefineShell(token, defaultMode, "codex")
1085
+ }
1086
+
1087
+ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit", surface: ReviewShellSurface = "legacy"): string {
968
1088
  const encodedToken = JSON.stringify(token)
969
1089
  const encodedDefaultMode = JSON.stringify(defaultMode)
1090
+ const encodedSurface = JSON.stringify(surface)
1091
+ const activityLabel = surface === "codex" ? "Codex Activity" : "Activity"
1092
+ const bodyClass = surface === "codex" ? "codex-review" : "legacy-review"
970
1093
  return `<!doctype html>
971
1094
  <html lang="en">
972
1095
  <head>
@@ -1029,6 +1152,19 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1029
1152
  .comment-bubble.failed { border-color: #c58f82; background: #f7eae5; }
1030
1153
  .comment-bubble-text { white-space: pre-wrap; overflow-wrap: anywhere; }
1031
1154
  .comment-bubble-state { margin-top: 8px; color: #8a6231; font-size: 12px; font-weight: 800; }
1155
+ .comment-progress { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; color: #5f574d; font-size: 12px; }
1156
+ .comment-progress-line { display: flex; gap: 6px; align-items: flex-start; }
1157
+ .comment-progress-line::before { content: ""; width: 6px; height: 6px; margin-top: 6px; border-radius: 999px; background: #b48b52; flex: 0 0 auto; }
1158
+ .comment-raw { margin-top: 8px; color: #6f473c; font-size: 12px; }
1159
+ .comment-raw summary { cursor: pointer; font-weight: 800; }
1160
+ .comment-raw pre { margin: 6px 0 0; max-height: 160px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; background: rgba(255,255,255,.55); border: 1px solid rgba(143,70,56,.22); border-radius: 8px; padding: 8px; }
1161
+ .codex-log { margin-top: 8px; color: #4b5563; font-size: 12px; }
1162
+ .codex-log summary { cursor: pointer; font-weight: 900; }
1163
+ .codex-log-list { margin-top: 7px; display: flex; flex-direction: column; gap: 6px; max-height: 240px; overflow: auto; }
1164
+ .codex-log-entry { padding: 7px 8px; border: 1px solid rgba(148,163,184,.34); border-radius: 8px; background: rgba(255,255,255,.58); }
1165
+ .codex-log-meta { display: flex; justify-content: space-between; gap: 8px; color: #6b7280; font-size: 11px; font-weight: 800; text-transform: uppercase; }
1166
+ .codex-log-message { margin-top: 4px; color: #374151; white-space: pre-wrap; overflow-wrap: anywhere; }
1167
+ .codex-log-detail { margin: 5px 0 0; max-height: 120px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; color: #111827; background: rgba(17,24,39,.05); border-radius: 6px; padding: 6px; }
1032
1168
  .comment-bubble.updated .comment-bubble-state { color: #556b3f; }
1033
1169
  .comment-bubble.stale .comment-bubble-state { color: #8a6231; }
1034
1170
  .comment-bubble.failed .comment-bubble-state { color: #8f4638; }
@@ -1098,7 +1234,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1098
1234
  @media (max-width: 900px) { .app { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; } .resize-handle { display: none; } aside { max-height: 48vh; } .deck-nav { bottom: 10px; } }
1099
1235
  </style>
1100
1236
  </head>
1101
- <body>
1237
+ <body class="${bodyClass}">
1102
1238
  <main class="app">
1103
1239
  <section class="preview"><iframe id="deck" src="/deck?token=${encodeURIComponent(token)}"></iframe><div id="hitbox" class="hitbox" aria-label="Deck element selection layer"></div><div id="visualMoveHandle" class="visual-move-handle" aria-hidden="true"></div><div id="visualResizeHandle" class="visual-resize-handle" aria-hidden="true"></div><div id="visualEditToolbar" class="visual-edit-toolbar" aria-live="polite"><span id="visualEditCount">No unsaved visual changes</span><button id="visualUndo" type="button">Undo</button><button id="visualReset" type="button">Reset</button><button id="visualSave" class="save-visual" type="button">Save Changes</button></div><nav class="deck-nav" aria-label="Deck navigation"><button id="deckPrev" type="button" title="Previous slide (ArrowLeft / ArrowUp / PageUp)">Previous</button><div id="deckCounter" class="deck-nav-status" aria-live="polite">-- / --</div><button id="deckNext" type="button" title="Next slide (ArrowRight / ArrowDown / Space / PageDown)">Next</button></nav></section>
1104
1240
  <div id="resizeHandle" class="resize-handle" role="separator" aria-label="Resize editor panel" aria-orientation="vertical" title="Drag to resize editor. Double-click to reset."></div>
@@ -1123,7 +1259,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1123
1259
  </div>
1124
1260
  </div>
1125
1261
  <button id="send" class="primary-action" disabled><svg class="send-icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94L14.7 6.3z"/></svg><span>Apply Fix</span></button>
1126
- <div class="activity-panel"><div class="label">Activity</div><div id="commentThread" class="comment-thread" aria-live="polite"></div></div>
1262
+ <div class="activity-panel"><div class="label">${activityLabel}</div><div id="commentThread" class="comment-thread" aria-live="polite"></div></div>
1127
1263
  </div>
1128
1264
  <div id="inspectPanel" class="tab-panel">
1129
1265
  <div class="panel">
@@ -1155,6 +1291,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1155
1291
  (() => {
1156
1292
  const token = ${encodedToken};
1157
1293
  const defaultMode = ${encodedDefaultMode};
1294
+ const reviewSurface = ${encodedSurface};
1295
+ const codexReview = reviewSurface === 'codex';
1158
1296
  const COMMENT_STALE_MS = 60000;
1159
1297
  const EDITOR_WIDTH_KEY = 'revela-edit-editor-width';
1160
1298
  const DEFAULT_EDITOR_WIDTH = 376;
@@ -1192,6 +1330,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1192
1330
  mode: defaultMode === 'inspect' ? 'inspect' : 'edit',
1193
1331
  inspecting: false,
1194
1332
  activeInspectRequestId: '',
1333
+ inspectEventLog: [],
1195
1334
  inspectLanguage: 'Auto',
1196
1335
  inspectFallback: null,
1197
1336
  sendingEdit: false,
@@ -1975,12 +2114,19 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1975
2114
  window.setInterval(pollDeckVersion, 2000);
1976
2115
  }
1977
2116
 
2117
+ async function fetchDeckVersion() {
2118
+ const res = await fetch('/api/deck-version?token=' + encodeURIComponent(token), { cache: 'no-store' });
2119
+ const body = await res.json().catch(() => ({}));
2120
+ if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to check deck version');
2121
+ return {
2122
+ body,
2123
+ version: body.version || (String(body.mtimeMs) + ':' + String(body.size)),
2124
+ };
2125
+ }
2126
+
1978
2127
  async function pollDeckVersion() {
1979
2128
  try {
1980
- const res = await fetch('/api/deck-version?token=' + encodeURIComponent(token), { cache: 'no-store' });
1981
- const body = await res.json().catch(() => ({}));
1982
- if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to check deck version');
1983
- const nextVersion = body.version || (String(body.mtimeMs) + ':' + String(body.size));
2129
+ const { body, version: nextVersion } = await fetchDeckVersion();
1984
2130
  if (!state.deckVersion) {
1985
2131
  state.deckVersion = nextVersion;
1986
2132
  markCommentsUpdatedForVersion(nextVersion);
@@ -1999,6 +2145,28 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
1999
2145
  }
2000
2146
  }
2001
2147
 
2148
+ async function watchDeckVersionAfterComment(commentId) {
2149
+ const comment = state.pendingComments.find((item) => item.id === commentId);
2150
+ const baseDeckVersion = comment?.baseDeckVersion || state.deckVersion;
2151
+ const started = Date.now();
2152
+ while (Date.now() - started < 15000) {
2153
+ if (pendingCommentStatus(commentId) === 'updated' || pendingCommentStatus(commentId) === 'failed') return;
2154
+ await delay(250);
2155
+ try {
2156
+ const { body, version: nextVersion } = await fetchDeckVersion();
2157
+ if (nextVersion && nextVersion !== baseDeckVersion) {
2158
+ state.deckVersion = nextVersion;
2159
+ markCommentsUpdatedForVersion(nextVersion);
2160
+ refreshDeckPreview(body.mtimeMs);
2161
+ return;
2162
+ }
2163
+ } catch (error) {
2164
+ reportError(error);
2165
+ return;
2166
+ }
2167
+ }
2168
+ }
2169
+
2002
2170
  function refreshDeckPreview(version) {
2003
2171
  state.pendingRefreshMessage = true;
2004
2172
  state.pendingDeckSlideRestore = state.deckSlideIndex;
@@ -2098,7 +2266,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2098
2266
  if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send comment');
2099
2267
  updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion, requestId: body.commentRequestId || body.requestId || '' });
2100
2268
  if (pendingCommentStatus(commentId) !== 'updated') setStatus('Comment sent. Waiting for deck update...');
2101
- if (body.commentRequestId || body.requestId) pollCommentResult(commentId, body.commentRequestId || body.requestId);
2269
+ if (body.commentRequestId || body.requestId) watchCommentProgress(commentId, body.commentRequestId || body.requestId);
2102
2270
  } catch (error) {
2103
2271
  updatePendingCommentStatus(commentId, 'failed');
2104
2272
  reportError(error);
@@ -2458,7 +2626,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2458
2626
  if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send asset placement');
2459
2627
  updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion, requestId: body.commentRequestId || body.requestId || '' });
2460
2628
  if (pendingCommentStatus(commentId) !== 'updated') setStatus('Asset placement sent. Waiting for deck update...');
2461
- if (body.commentRequestId || body.requestId) pollCommentResult(commentId, body.commentRequestId || body.requestId);
2629
+ if (body.commentRequestId || body.requestId) watchCommentProgress(commentId, body.commentRequestId || body.requestId);
2462
2630
  } catch (error) {
2463
2631
  updatePendingCommentStatus(commentId, 'failed');
2464
2632
  reportError(error);
@@ -2543,6 +2711,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2543
2711
  baseDeckVersion: state.deckVersion,
2544
2712
  updatedVersion: null,
2545
2713
  requestId: '',
2714
+ progressEvent: null,
2715
+ eventLog: [],
2716
+ failureRaw: '',
2546
2717
  });
2547
2718
  renderCommentThread();
2548
2719
  return id;
@@ -2554,6 +2725,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2554
2725
  if (comment.status === 'updated' && status !== 'failed') return;
2555
2726
  comment.status = status;
2556
2727
  if (updates) Object.assign(comment, updates);
2728
+ if (status === 'updated' || status === 'failed') comment.progressEvent = null;
2557
2729
  renderCommentThread();
2558
2730
  }
2559
2731
 
@@ -2563,6 +2735,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2563
2735
  if ((comment.status === 'sent' || comment.status === 'sending' || comment.status === 'stale') && comment.baseDeckVersion !== version) {
2564
2736
  comment.status = 'updated';
2565
2737
  comment.updatedVersion = version;
2738
+ comment.progressEvent = null;
2566
2739
  changed = true;
2567
2740
  }
2568
2741
  });
@@ -2601,7 +2774,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2601
2774
  const body = await res.json().catch(() => ({}));
2602
2775
  if (!res.ok || !body.ok) throw new Error(body.error || 'Comment result failed');
2603
2776
  if (body.status === 'failed' || body.status === 'expired') {
2604
- updatePendingCommentStatus(commentId, 'failed');
2777
+ updatePendingCommentStatus(commentId, 'failed', { failureRaw: body.raw || '' });
2605
2778
  setStatus(body.error || 'Review agent failed to apply the comment.');
2606
2779
  return;
2607
2780
  }
@@ -2619,6 +2792,135 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2619
2792
  }
2620
2793
  }
2621
2794
 
2795
+ function watchCommentProgress(commentId, requestId) {
2796
+ if (!requestId) return;
2797
+ if (!('EventSource' in window)) {
2798
+ pollCommentResult(commentId, requestId);
2799
+ return;
2800
+ }
2801
+ let closed = false;
2802
+ let fallbackStarted = false;
2803
+ const startFallback = () => {
2804
+ if (fallbackStarted) return;
2805
+ fallbackStarted = true;
2806
+ pollCommentResult(commentId, requestId);
2807
+ };
2808
+ let source;
2809
+ try {
2810
+ source = new EventSource('/api/comment-events?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId));
2811
+ } catch {
2812
+ startFallback();
2813
+ return;
2814
+ }
2815
+ source.addEventListener('progress', (event) => {
2816
+ let payload;
2817
+ try {
2818
+ payload = JSON.parse(event.data || '{}');
2819
+ } catch {
2820
+ return;
2821
+ }
2822
+ recordCommentProgress(commentId, payload);
2823
+ if (payload.type === 'failed' || payload.type === 'timeout') {
2824
+ closed = true;
2825
+ source.close();
2826
+ updatePendingCommentStatus(commentId, 'failed', { failureRaw: payload.detail || '' });
2827
+ setStatus(payload.message || 'Review agent failed to apply the comment.');
2828
+ } else if (payload.type === 'completed') {
2829
+ closed = true;
2830
+ source.close();
2831
+ if (pendingCommentStatus(commentId) !== 'updated') setStatus(payload.message || 'Waiting for deck file update...');
2832
+ watchDeckVersionAfterComment(commentId);
2833
+ } else if (payload.message) {
2834
+ setStatus(payload.message);
2835
+ }
2836
+ });
2837
+ source.onerror = () => {
2838
+ source.close();
2839
+ if (!closed && pendingCommentStatus(commentId) !== 'updated' && pendingCommentStatus(commentId) !== 'failed') {
2840
+ startFallback();
2841
+ }
2842
+ };
2843
+ }
2844
+
2845
+ function recordCommentProgress(commentId, event) {
2846
+ const comment = state.pendingComments.find((item) => item.id === commentId);
2847
+ if (!comment || !event || !event.message) return;
2848
+ if (codexReview) {
2849
+ appendCodexEventLog(comment, event);
2850
+ }
2851
+ if (event.type === 'completed') {
2852
+ comment.progressEvent = null;
2853
+ if (codexReview) renderCommentThread();
2854
+ return;
2855
+ }
2856
+ const nextEvent = {
2857
+ type: event.type || 'codex_event',
2858
+ message: String(event.message).slice(0, 240),
2859
+ detail: typeof event.detail === 'string' ? event.detail.slice(-4096) : '',
2860
+ };
2861
+ const duplicate = comment.progressEvent;
2862
+ if (duplicate && duplicate.type === nextEvent.type && duplicate.message === nextEvent.message && duplicate.detail === nextEvent.detail) return;
2863
+ comment.progressEvent = nextEvent;
2864
+ if (event.type === 'failed' || event.type === 'timeout') comment.failureRaw = typeof event.detail === 'string' ? event.detail.slice(-4096) : '';
2865
+ renderCommentThread();
2866
+ }
2867
+
2868
+ function appendCodexEventLog(target, event) {
2869
+ if (!target.eventLog) target.eventLog = [];
2870
+ const next = {
2871
+ type: event.type || 'codex_event',
2872
+ message: String(event.message || '').slice(0, 500),
2873
+ detail: typeof event.detail === 'string' ? event.detail.slice(-12000) : '',
2874
+ timestamp: typeof event.timestamp === 'number' ? event.timestamp : Date.now(),
2875
+ };
2876
+ const previous = target.eventLog[target.eventLog.length - 1];
2877
+ if (previous && previous.type === next.type && previous.message === next.message && previous.detail === next.detail) return;
2878
+ target.eventLog.push(next);
2879
+ if (target.eventLog.length > 250) target.eventLog.splice(0, target.eventLog.length - 250);
2880
+ }
2881
+
2882
+ function codexLogSummary(log) {
2883
+ const count = Array.isArray(log) ? log.length : 0;
2884
+ return count === 1 ? 'Codex execution log (1 event)' : 'Codex execution log (' + count + ' events)';
2885
+ }
2886
+
2887
+ function renderCodexLog(log) {
2888
+ if (!codexReview || !Array.isArray(log) || !log.length) return null;
2889
+ const details = document.createElement('details');
2890
+ details.className = 'codex-log';
2891
+ const summary = document.createElement('summary');
2892
+ summary.textContent = codexLogSummary(log);
2893
+ const list = document.createElement('div');
2894
+ list.className = 'codex-log-list';
2895
+ log.forEach((item) => {
2896
+ const row = document.createElement('div');
2897
+ row.className = 'codex-log-entry';
2898
+ const meta = document.createElement('div');
2899
+ meta.className = 'codex-log-meta';
2900
+ const type = document.createElement('span');
2901
+ type.textContent = item.type || 'event';
2902
+ const time = document.createElement('span');
2903
+ time.textContent = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : '';
2904
+ meta.appendChild(type);
2905
+ meta.appendChild(time);
2906
+ const message = document.createElement('div');
2907
+ message.className = 'codex-log-message';
2908
+ message.textContent = item.message || '';
2909
+ row.appendChild(meta);
2910
+ row.appendChild(message);
2911
+ if (item.detail) {
2912
+ const detail = document.createElement('pre');
2913
+ detail.className = 'codex-log-detail';
2914
+ detail.textContent = item.detail;
2915
+ row.appendChild(detail);
2916
+ }
2917
+ list.appendChild(row);
2918
+ });
2919
+ details.appendChild(summary);
2920
+ details.appendChild(list);
2921
+ return details;
2922
+ }
2923
+
2622
2924
  function renderCommentThread() {
2623
2925
  els.commentThread.textContent = '';
2624
2926
  state.pendingComments.forEach((comment) => {
@@ -2635,6 +2937,28 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2635
2937
 
2636
2938
  bubble.appendChild(text);
2637
2939
  bubble.appendChild(status);
2940
+ if (comment.progressEvent) {
2941
+ const progress = document.createElement('div');
2942
+ progress.className = 'comment-progress';
2943
+ const line = document.createElement('div');
2944
+ line.className = 'comment-progress-line';
2945
+ line.textContent = comment.progressEvent.message;
2946
+ progress.appendChild(line);
2947
+ bubble.appendChild(progress);
2948
+ }
2949
+ if (comment.status === 'failed' && comment.failureRaw) {
2950
+ const details = document.createElement('details');
2951
+ details.className = 'comment-raw';
2952
+ const summary = document.createElement('summary');
2953
+ summary.textContent = 'Details';
2954
+ const pre = document.createElement('pre');
2955
+ pre.textContent = comment.failureRaw;
2956
+ details.appendChild(summary);
2957
+ details.appendChild(pre);
2958
+ bubble.appendChild(details);
2959
+ }
2960
+ const codexLog = renderCodexLog(comment.eventLog);
2961
+ if (codexLog) bubble.appendChild(codexLog);
2638
2962
  els.commentThread.appendChild(bubble);
2639
2963
  });
2640
2964
  }
@@ -2767,6 +3091,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2767
3091
  els.inspectCards.innerHTML = '<div class="inspect-loading"><span class="loading-row"><span class="spinner" aria-hidden="true"></span><b>' + escapeHtml(message) + '</b></span><br>Preparing concise Purpose and Source context.</div>'
2768
3092
  + '<div class="skeleton-card"><div class="skeleton-line short"></div><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div>'
2769
3093
  + '<div class="skeleton-card"><div class="skeleton-line short"></div><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div>';
3094
+ renderInspectCodexLog();
2770
3095
  }
2771
3096
 
2772
3097
  function getInspectComment() {
@@ -2783,6 +3108,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2783
3108
  setMode('inspect');
2784
3109
  els.inspectStale.innerHTML = '';
2785
3110
  state.inspectFallback = null;
3111
+ state.inspectEventLog = [];
2786
3112
  renderInspectLoading('Reading selection...');
2787
3113
  try {
2788
3114
  const res = await fetch('/api/inspect?token=' + encodeURIComponent(token), {
@@ -2796,7 +3122,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2796
3122
  state.activeInspectRequestId = body.requestId;
2797
3123
  state.inspectFallback = body.preprocess || null;
2798
3124
  renderInspectLoading('Waiting for Purpose and Source...');
2799
- await pollInspectResult(body.requestId);
3125
+ await watchInspectProgress(body.requestId);
2800
3126
  } catch (error) {
2801
3127
  if (state.inspectFallback) {
2802
3128
  renderInspectResult(state.inspectFallback, 'Deterministic fallback');
@@ -2826,6 +3152,92 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
2826
3152
  throw new Error('Insight timed out while waiting for Review agent result');
2827
3153
  }
2828
3154
 
3155
+ async function fetchInspectResultOnce(requestId) {
3156
+ const res = await fetch('/api/inspect-result?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId), { cache: 'no-store' });
3157
+ const body = await res.json().catch(() => ({}));
3158
+ if (!res.ok || !body.ok) throw new Error(body.error || 'Insight result failed');
3159
+ if (body.status === 'completed') {
3160
+ state.deckVersion = body.deckVersion || state.deckVersion;
3161
+ renderInspectResult(body.result, 'Generated');
3162
+ renderInspectCodexLog();
3163
+ return true;
3164
+ }
3165
+ if (body.status === 'failed' || body.status === 'expired') {
3166
+ const error = new Error(body.error || 'Insight failed');
3167
+ error.raw = body.raw || '';
3168
+ throw error;
3169
+ }
3170
+ return false;
3171
+ }
3172
+
3173
+ async function watchInspectProgress(requestId) {
3174
+ if (!requestId) return;
3175
+ if (!codexReview || !('EventSource' in window)) {
3176
+ await pollInspectResult(requestId);
3177
+ renderInspectCodexLog();
3178
+ return;
3179
+ }
3180
+ await new Promise((resolve, reject) => {
3181
+ let settled = false;
3182
+ let source;
3183
+ const finish = (ok, error) => {
3184
+ if (settled) return;
3185
+ settled = true;
3186
+ if (source) source.close();
3187
+ if (ok) resolve();
3188
+ else reject(error);
3189
+ };
3190
+ try {
3191
+ source = new EventSource('/api/inspect-events?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId));
3192
+ } catch (error) {
3193
+ pollInspectResult(requestId).then(resolve, reject);
3194
+ return;
3195
+ }
3196
+ source.addEventListener('progress', (event) => {
3197
+ let payload;
3198
+ try {
3199
+ payload = JSON.parse(event.data || '{}');
3200
+ } catch {
3201
+ return;
3202
+ }
3203
+ recordInspectProgress(payload);
3204
+ if (payload.type === 'failed' || payload.type === 'timeout') {
3205
+ const error = new Error(payload.message || 'Insight failed');
3206
+ error.raw = payload.detail || '';
3207
+ finish(false, error);
3208
+ } else if (payload.type === 'completed') {
3209
+ fetchInspectResultOnce(requestId).then((ready) => {
3210
+ if (ready) finish(true);
3211
+ else pollInspectResult(requestId).then(() => finish(true), (error) => finish(false, error));
3212
+ }, (error) => finish(false, error));
3213
+ }
3214
+ });
3215
+ source.onerror = () => {
3216
+ if (!settled) {
3217
+ source.close();
3218
+ pollInspectResult(requestId).then(() => finish(true), (error) => finish(false, error));
3219
+ }
3220
+ };
3221
+ });
3222
+ }
3223
+
3224
+ function recordInspectProgress(event) {
3225
+ if (!event || !event.message) return;
3226
+ const inspectLog = { eventLog: state.inspectEventLog };
3227
+ appendCodexEventLog(inspectLog, event);
3228
+ state.inspectEventLog = inspectLog.eventLog;
3229
+ if (event.type !== 'completed') {
3230
+ renderInspectLoading(event.message);
3231
+ } else {
3232
+ renderInspectCodexLog();
3233
+ }
3234
+ }
3235
+
3236
+ function renderInspectCodexLog() {
3237
+ const codexLog = renderCodexLog(state.inspectEventLog);
3238
+ if (codexLog) els.inspectCards.appendChild(codexLog);
3239
+ }
3240
+
2829
3241
  function collectReferenceSnapshot() {
2830
3242
  const elements = state.references.map((reference) => reference.payload);
2831
3243
  const first = elements[0] || {};
@@ -105,6 +105,7 @@ export async function reviewDeckOpen(input: ReviewDeckOpenInput): Promise<any> {
105
105
  openUrl: input.openUrl,
106
106
  sessionID: `codex-review:${requestedFile}`,
107
107
  promptBridge: createCodexExecReviewPromptBridge(),
108
+ surface: "codex",
108
109
  })
109
110
  return {
110
111
  ok: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.17.12",
3
+ "version": "0.17.14",
4
4
  "description": "OpenCode plugin for trusted narrative artifacts from local sources, research, and evidence",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -53,7 +53,9 @@
53
53
  },
54
54
  "scripts": {
55
55
  "test": "bun test",
56
- "typecheck": "tsc"
56
+ "typecheck": "tsc",
57
+ "smoke:mcp": "bun scripts/codex-mcp-smoke.ts",
58
+ "smoke:mcp-pack": "bun scripts/codex-mcp-pack-smoke.ts"
57
59
  },
58
60
  "peerDependencies": {
59
61
  "@opencode-ai/plugin": "*"
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "revela": {
4
4
  "command": "npx",
5
- "args": ["-y", "@cyber-dash-tech/revela@0.17.12", "mcp"]
5
+ "args": ["-y", "@cyber-dash-tech/revela@0.17.14", "mcp"]
6
6
  }
7
7
  }
8
8
  }