@cyber-dash-tech/revela 0.17.12 → 0.17.13
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 +9 -0
- package/README.zh-CN.md +9 -0
- package/lib/inspect/requests.ts +81 -1
- package/lib/refine/comment-requests.ts +82 -1
- package/lib/refine/open.ts +4 -2
- package/lib/refine/prompt-bridge.ts +83 -7
- package/lib/refine/server.ts +423 -17
- package/lib/runtime/review.ts +1 -0
- package/package.json +4 -2
- package/plugins/revela/.mcp.json +1 -1
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:
|
package/lib/inspect/requests.ts
CHANGED
|
@@ -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
|
}
|
package/lib/refine/open.ts
CHANGED
|
@@ -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
|
|
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")
|
|
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
|
-
|
|
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:
|
|
162
|
+
stderr: nextStderr,
|
|
135
163
|
})
|
|
136
164
|
}, input.timeoutMs)
|
|
137
165
|
child.stdout?.on("data", (chunk) => {
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
package/lib/refine/server.ts
CHANGED
|
@@ -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
|
|
@@ -765,11 +784,12 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
|
|
|
765
784
|
workspaceRoot: session.workspaceRoot,
|
|
766
785
|
file: session.file,
|
|
767
786
|
requestId,
|
|
787
|
+
onEvent: (event) => addCommentRequestEvent(requestId, event),
|
|
768
788
|
}).then((result) => {
|
|
769
789
|
if (result.ok) {
|
|
770
790
|
completeCommentRequest(requestId)
|
|
771
791
|
} else {
|
|
772
|
-
failCommentRequest(requestId, result.error)
|
|
792
|
+
failCommentRequest(requestId, result.error, result.raw)
|
|
773
793
|
}
|
|
774
794
|
}).catch((error: unknown) => {
|
|
775
795
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -781,6 +801,88 @@ async function handleComment(req: Request, session: EditSession): Promise<Respon
|
|
|
781
801
|
return jsonResponse({ ok: true, requestId, commentRequestId: requestId, deckVersion, status: "pending" })
|
|
782
802
|
}
|
|
783
803
|
|
|
804
|
+
function handleCommentEvents(requestId: string | null, session: EditSession): Response {
|
|
805
|
+
if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400)
|
|
806
|
+
const request = getCommentRequest(requestId)
|
|
807
|
+
if (!request) return jsonResponse({ ok: false, requestId, error: "Comment request not found" }, 404)
|
|
808
|
+
session.lastActiveAt = Date.now()
|
|
809
|
+
scheduleIdleStop()
|
|
810
|
+
|
|
811
|
+
const encoder = new TextEncoder()
|
|
812
|
+
let unsubscribe = () => {}
|
|
813
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
814
|
+
start(controller) {
|
|
815
|
+
const send = (event: unknown) => {
|
|
816
|
+
controller.enqueue(encoder.encode(`event: progress\ndata: ${JSON.stringify(event)}\n\n`))
|
|
817
|
+
}
|
|
818
|
+
for (const event of request.events) send(event)
|
|
819
|
+
if (request.status !== "pending") {
|
|
820
|
+
controller.close()
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
unsubscribe = subscribeCommentRequestEvents(requestId, (event) => {
|
|
824
|
+
send(event)
|
|
825
|
+
if (event.type === "completed" || event.type === "failed" || event.type === "timeout") {
|
|
826
|
+
unsubscribe()
|
|
827
|
+
controller.close()
|
|
828
|
+
}
|
|
829
|
+
})
|
|
830
|
+
},
|
|
831
|
+
cancel() {
|
|
832
|
+
unsubscribe()
|
|
833
|
+
},
|
|
834
|
+
})
|
|
835
|
+
|
|
836
|
+
return new Response(stream, {
|
|
837
|
+
headers: {
|
|
838
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
839
|
+
"cache-control": "no-store",
|
|
840
|
+
connection: "keep-alive",
|
|
841
|
+
},
|
|
842
|
+
})
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function handleInspectEvents(requestId: string | null, session: EditSession): Response {
|
|
846
|
+
if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400)
|
|
847
|
+
const request = getInspectRequest(requestId)
|
|
848
|
+
if (!request) return jsonResponse({ ok: false, requestId, error: "Inspection request not found" }, 404)
|
|
849
|
+
session.lastActiveAt = Date.now()
|
|
850
|
+
scheduleIdleStop()
|
|
851
|
+
|
|
852
|
+
const encoder = new TextEncoder()
|
|
853
|
+
let unsubscribe = () => {}
|
|
854
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
855
|
+
start(controller) {
|
|
856
|
+
const send = (event: unknown) => {
|
|
857
|
+
controller.enqueue(encoder.encode(`event: progress\ndata: ${JSON.stringify(event)}\n\n`))
|
|
858
|
+
}
|
|
859
|
+
for (const event of request.events) send(event)
|
|
860
|
+
if (request.status !== "pending") {
|
|
861
|
+
controller.close()
|
|
862
|
+
return
|
|
863
|
+
}
|
|
864
|
+
unsubscribe = subscribeInspectRequestEvents(requestId, (event) => {
|
|
865
|
+
send(event)
|
|
866
|
+
if (event.type === "completed" || event.type === "failed" || event.type === "timeout") {
|
|
867
|
+
unsubscribe()
|
|
868
|
+
controller.close()
|
|
869
|
+
}
|
|
870
|
+
})
|
|
871
|
+
},
|
|
872
|
+
cancel() {
|
|
873
|
+
unsubscribe()
|
|
874
|
+
},
|
|
875
|
+
})
|
|
876
|
+
|
|
877
|
+
return new Response(stream, {
|
|
878
|
+
headers: {
|
|
879
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
880
|
+
"cache-control": "no-store",
|
|
881
|
+
connection: "keep-alive",
|
|
882
|
+
},
|
|
883
|
+
})
|
|
884
|
+
}
|
|
885
|
+
|
|
784
886
|
function handleCommentResult(requestId: string | null, session: EditSession): Response {
|
|
785
887
|
if (!requestId) return jsonResponse({ ok: false, error: "Missing requestId" }, 400)
|
|
786
888
|
const request = getCommentRequest(requestId)
|
|
@@ -788,7 +890,14 @@ function handleCommentResult(requestId: string | null, session: EditSession): Re
|
|
|
788
890
|
session.lastActiveAt = Date.now()
|
|
789
891
|
scheduleIdleStop()
|
|
790
892
|
if (request.status === "failed" || request.status === "expired") {
|
|
791
|
-
return jsonResponse({
|
|
893
|
+
return jsonResponse({
|
|
894
|
+
ok: true,
|
|
895
|
+
requestId,
|
|
896
|
+
status: request.status,
|
|
897
|
+
deckVersion: request.deckVersion,
|
|
898
|
+
error: request.error || "Review agent failed",
|
|
899
|
+
raw: request.raw,
|
|
900
|
+
})
|
|
792
901
|
}
|
|
793
902
|
return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion })
|
|
794
903
|
}
|
|
@@ -834,11 +943,12 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
|
|
|
834
943
|
workspaceRoot: session.workspaceRoot,
|
|
835
944
|
file: session.file,
|
|
836
945
|
requestId,
|
|
946
|
+
onEvent: (event) => addInspectRequestEvent(requestId, event),
|
|
837
947
|
}).then((result) => {
|
|
838
948
|
if (result.ok && result.result) {
|
|
839
949
|
completeInspectRequest(requestId, result.result)
|
|
840
950
|
} else if (!result.ok) {
|
|
841
|
-
failInspectRequest(requestId, result.error)
|
|
951
|
+
failInspectRequest(requestId, result.error, result.raw)
|
|
842
952
|
}
|
|
843
953
|
}).catch((error: unknown) => {
|
|
844
954
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -860,7 +970,7 @@ function handleInspectResult(requestId: string | null, session: EditSession): Re
|
|
|
860
970
|
session.lastActiveAt = Date.now()
|
|
861
971
|
scheduleIdleStop()
|
|
862
972
|
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" })
|
|
973
|
+
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
974
|
return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion })
|
|
865
975
|
}
|
|
866
976
|
|
|
@@ -964,9 +1074,16 @@ function jsonResponse(body: unknown, status = 200): Response {
|
|
|
964
1074
|
})
|
|
965
1075
|
}
|
|
966
1076
|
|
|
967
|
-
export function
|
|
1077
|
+
export function renderCodexReviewShell(token: string, defaultMode: RefineMode = "edit"): string {
|
|
1078
|
+
return renderRefineShell(token, defaultMode, "codex")
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
export function renderRefineShell(token: string, defaultMode: RefineMode = "edit", surface: ReviewShellSurface = "legacy"): string {
|
|
968
1082
|
const encodedToken = JSON.stringify(token)
|
|
969
1083
|
const encodedDefaultMode = JSON.stringify(defaultMode)
|
|
1084
|
+
const encodedSurface = JSON.stringify(surface)
|
|
1085
|
+
const activityLabel = surface === "codex" ? "Codex Activity" : "Activity"
|
|
1086
|
+
const bodyClass = surface === "codex" ? "codex-review" : "legacy-review"
|
|
970
1087
|
return `<!doctype html>
|
|
971
1088
|
<html lang="en">
|
|
972
1089
|
<head>
|
|
@@ -1029,6 +1146,19 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1029
1146
|
.comment-bubble.failed { border-color: #c58f82; background: #f7eae5; }
|
|
1030
1147
|
.comment-bubble-text { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1031
1148
|
.comment-bubble-state { margin-top: 8px; color: #8a6231; font-size: 12px; font-weight: 800; }
|
|
1149
|
+
.comment-progress { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; color: #5f574d; font-size: 12px; }
|
|
1150
|
+
.comment-progress-line { display: flex; gap: 6px; align-items: flex-start; }
|
|
1151
|
+
.comment-progress-line::before { content: ""; width: 6px; height: 6px; margin-top: 6px; border-radius: 999px; background: #b48b52; flex: 0 0 auto; }
|
|
1152
|
+
.comment-raw { margin-top: 8px; color: #6f473c; font-size: 12px; }
|
|
1153
|
+
.comment-raw summary { cursor: pointer; font-weight: 800; }
|
|
1154
|
+
.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; }
|
|
1155
|
+
.codex-log { margin-top: 8px; color: #4b5563; font-size: 12px; }
|
|
1156
|
+
.codex-log summary { cursor: pointer; font-weight: 900; }
|
|
1157
|
+
.codex-log-list { margin-top: 7px; display: flex; flex-direction: column; gap: 6px; max-height: 240px; overflow: auto; }
|
|
1158
|
+
.codex-log-entry { padding: 7px 8px; border: 1px solid rgba(148,163,184,.34); border-radius: 8px; background: rgba(255,255,255,.58); }
|
|
1159
|
+
.codex-log-meta { display: flex; justify-content: space-between; gap: 8px; color: #6b7280; font-size: 11px; font-weight: 800; text-transform: uppercase; }
|
|
1160
|
+
.codex-log-message { margin-top: 4px; color: #374151; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1161
|
+
.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
1162
|
.comment-bubble.updated .comment-bubble-state { color: #556b3f; }
|
|
1033
1163
|
.comment-bubble.stale .comment-bubble-state { color: #8a6231; }
|
|
1034
1164
|
.comment-bubble.failed .comment-bubble-state { color: #8f4638; }
|
|
@@ -1098,7 +1228,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1098
1228
|
@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
1229
|
</style>
|
|
1100
1230
|
</head>
|
|
1101
|
-
<body>
|
|
1231
|
+
<body class="${bodyClass}">
|
|
1102
1232
|
<main class="app">
|
|
1103
1233
|
<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
1234
|
<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 +1253,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1123
1253
|
</div>
|
|
1124
1254
|
</div>
|
|
1125
1255
|
<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"
|
|
1256
|
+
<div class="activity-panel"><div class="label">${activityLabel}</div><div id="commentThread" class="comment-thread" aria-live="polite"></div></div>
|
|
1127
1257
|
</div>
|
|
1128
1258
|
<div id="inspectPanel" class="tab-panel">
|
|
1129
1259
|
<div class="panel">
|
|
@@ -1155,6 +1285,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1155
1285
|
(() => {
|
|
1156
1286
|
const token = ${encodedToken};
|
|
1157
1287
|
const defaultMode = ${encodedDefaultMode};
|
|
1288
|
+
const reviewSurface = ${encodedSurface};
|
|
1289
|
+
const codexReview = reviewSurface === 'codex';
|
|
1158
1290
|
const COMMENT_STALE_MS = 60000;
|
|
1159
1291
|
const EDITOR_WIDTH_KEY = 'revela-edit-editor-width';
|
|
1160
1292
|
const DEFAULT_EDITOR_WIDTH = 376;
|
|
@@ -1192,6 +1324,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1192
1324
|
mode: defaultMode === 'inspect' ? 'inspect' : 'edit',
|
|
1193
1325
|
inspecting: false,
|
|
1194
1326
|
activeInspectRequestId: '',
|
|
1327
|
+
inspectEventLog: [],
|
|
1195
1328
|
inspectLanguage: 'Auto',
|
|
1196
1329
|
inspectFallback: null,
|
|
1197
1330
|
sendingEdit: false,
|
|
@@ -1975,12 +2108,19 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1975
2108
|
window.setInterval(pollDeckVersion, 2000);
|
|
1976
2109
|
}
|
|
1977
2110
|
|
|
2111
|
+
async function fetchDeckVersion() {
|
|
2112
|
+
const res = await fetch('/api/deck-version?token=' + encodeURIComponent(token), { cache: 'no-store' });
|
|
2113
|
+
const body = await res.json().catch(() => ({}));
|
|
2114
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to check deck version');
|
|
2115
|
+
return {
|
|
2116
|
+
body,
|
|
2117
|
+
version: body.version || (String(body.mtimeMs) + ':' + String(body.size)),
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
|
|
1978
2121
|
async function pollDeckVersion() {
|
|
1979
2122
|
try {
|
|
1980
|
-
const
|
|
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));
|
|
2123
|
+
const { body, version: nextVersion } = await fetchDeckVersion();
|
|
1984
2124
|
if (!state.deckVersion) {
|
|
1985
2125
|
state.deckVersion = nextVersion;
|
|
1986
2126
|
markCommentsUpdatedForVersion(nextVersion);
|
|
@@ -1999,6 +2139,28 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1999
2139
|
}
|
|
2000
2140
|
}
|
|
2001
2141
|
|
|
2142
|
+
async function watchDeckVersionAfterComment(commentId) {
|
|
2143
|
+
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2144
|
+
const baseDeckVersion = comment?.baseDeckVersion || state.deckVersion;
|
|
2145
|
+
const started = Date.now();
|
|
2146
|
+
while (Date.now() - started < 15000) {
|
|
2147
|
+
if (pendingCommentStatus(commentId) === 'updated' || pendingCommentStatus(commentId) === 'failed') return;
|
|
2148
|
+
await delay(250);
|
|
2149
|
+
try {
|
|
2150
|
+
const { body, version: nextVersion } = await fetchDeckVersion();
|
|
2151
|
+
if (nextVersion && nextVersion !== baseDeckVersion) {
|
|
2152
|
+
state.deckVersion = nextVersion;
|
|
2153
|
+
markCommentsUpdatedForVersion(nextVersion);
|
|
2154
|
+
refreshDeckPreview(body.mtimeMs);
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
} catch (error) {
|
|
2158
|
+
reportError(error);
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2002
2164
|
function refreshDeckPreview(version) {
|
|
2003
2165
|
state.pendingRefreshMessage = true;
|
|
2004
2166
|
state.pendingDeckSlideRestore = state.deckSlideIndex;
|
|
@@ -2098,7 +2260,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2098
2260
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send comment');
|
|
2099
2261
|
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion, requestId: body.commentRequestId || body.requestId || '' });
|
|
2100
2262
|
if (pendingCommentStatus(commentId) !== 'updated') setStatus('Comment sent. Waiting for deck update...');
|
|
2101
|
-
if (body.commentRequestId || body.requestId)
|
|
2263
|
+
if (body.commentRequestId || body.requestId) watchCommentProgress(commentId, body.commentRequestId || body.requestId);
|
|
2102
2264
|
} catch (error) {
|
|
2103
2265
|
updatePendingCommentStatus(commentId, 'failed');
|
|
2104
2266
|
reportError(error);
|
|
@@ -2458,7 +2620,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2458
2620
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Failed to send asset placement');
|
|
2459
2621
|
updatePendingCommentStatus(commentId, 'sent', { baseDeckVersion: body.deckVersion || state.deckVersion, requestId: body.commentRequestId || body.requestId || '' });
|
|
2460
2622
|
if (pendingCommentStatus(commentId) !== 'updated') setStatus('Asset placement sent. Waiting for deck update...');
|
|
2461
|
-
if (body.commentRequestId || body.requestId)
|
|
2623
|
+
if (body.commentRequestId || body.requestId) watchCommentProgress(commentId, body.commentRequestId || body.requestId);
|
|
2462
2624
|
} catch (error) {
|
|
2463
2625
|
updatePendingCommentStatus(commentId, 'failed');
|
|
2464
2626
|
reportError(error);
|
|
@@ -2543,6 +2705,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2543
2705
|
baseDeckVersion: state.deckVersion,
|
|
2544
2706
|
updatedVersion: null,
|
|
2545
2707
|
requestId: '',
|
|
2708
|
+
progressEvent: null,
|
|
2709
|
+
eventLog: [],
|
|
2710
|
+
failureRaw: '',
|
|
2546
2711
|
});
|
|
2547
2712
|
renderCommentThread();
|
|
2548
2713
|
return id;
|
|
@@ -2554,6 +2719,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2554
2719
|
if (comment.status === 'updated' && status !== 'failed') return;
|
|
2555
2720
|
comment.status = status;
|
|
2556
2721
|
if (updates) Object.assign(comment, updates);
|
|
2722
|
+
if (status === 'updated' || status === 'failed') comment.progressEvent = null;
|
|
2557
2723
|
renderCommentThread();
|
|
2558
2724
|
}
|
|
2559
2725
|
|
|
@@ -2563,6 +2729,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2563
2729
|
if ((comment.status === 'sent' || comment.status === 'sending' || comment.status === 'stale') && comment.baseDeckVersion !== version) {
|
|
2564
2730
|
comment.status = 'updated';
|
|
2565
2731
|
comment.updatedVersion = version;
|
|
2732
|
+
comment.progressEvent = null;
|
|
2566
2733
|
changed = true;
|
|
2567
2734
|
}
|
|
2568
2735
|
});
|
|
@@ -2601,7 +2768,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2601
2768
|
const body = await res.json().catch(() => ({}));
|
|
2602
2769
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Comment result failed');
|
|
2603
2770
|
if (body.status === 'failed' || body.status === 'expired') {
|
|
2604
|
-
updatePendingCommentStatus(commentId, 'failed');
|
|
2771
|
+
updatePendingCommentStatus(commentId, 'failed', { failureRaw: body.raw || '' });
|
|
2605
2772
|
setStatus(body.error || 'Review agent failed to apply the comment.');
|
|
2606
2773
|
return;
|
|
2607
2774
|
}
|
|
@@ -2619,6 +2786,135 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2619
2786
|
}
|
|
2620
2787
|
}
|
|
2621
2788
|
|
|
2789
|
+
function watchCommentProgress(commentId, requestId) {
|
|
2790
|
+
if (!requestId) return;
|
|
2791
|
+
if (!('EventSource' in window)) {
|
|
2792
|
+
pollCommentResult(commentId, requestId);
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
let closed = false;
|
|
2796
|
+
let fallbackStarted = false;
|
|
2797
|
+
const startFallback = () => {
|
|
2798
|
+
if (fallbackStarted) return;
|
|
2799
|
+
fallbackStarted = true;
|
|
2800
|
+
pollCommentResult(commentId, requestId);
|
|
2801
|
+
};
|
|
2802
|
+
let source;
|
|
2803
|
+
try {
|
|
2804
|
+
source = new EventSource('/api/comment-events?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId));
|
|
2805
|
+
} catch {
|
|
2806
|
+
startFallback();
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
source.addEventListener('progress', (event) => {
|
|
2810
|
+
let payload;
|
|
2811
|
+
try {
|
|
2812
|
+
payload = JSON.parse(event.data || '{}');
|
|
2813
|
+
} catch {
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
recordCommentProgress(commentId, payload);
|
|
2817
|
+
if (payload.type === 'failed' || payload.type === 'timeout') {
|
|
2818
|
+
closed = true;
|
|
2819
|
+
source.close();
|
|
2820
|
+
updatePendingCommentStatus(commentId, 'failed', { failureRaw: payload.detail || '' });
|
|
2821
|
+
setStatus(payload.message || 'Review agent failed to apply the comment.');
|
|
2822
|
+
} else if (payload.type === 'completed') {
|
|
2823
|
+
closed = true;
|
|
2824
|
+
source.close();
|
|
2825
|
+
if (pendingCommentStatus(commentId) !== 'updated') setStatus(payload.message || 'Waiting for deck file update...');
|
|
2826
|
+
watchDeckVersionAfterComment(commentId);
|
|
2827
|
+
} else if (payload.message) {
|
|
2828
|
+
setStatus(payload.message);
|
|
2829
|
+
}
|
|
2830
|
+
});
|
|
2831
|
+
source.onerror = () => {
|
|
2832
|
+
source.close();
|
|
2833
|
+
if (!closed && pendingCommentStatus(commentId) !== 'updated' && pendingCommentStatus(commentId) !== 'failed') {
|
|
2834
|
+
startFallback();
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
function recordCommentProgress(commentId, event) {
|
|
2840
|
+
const comment = state.pendingComments.find((item) => item.id === commentId);
|
|
2841
|
+
if (!comment || !event || !event.message) return;
|
|
2842
|
+
if (codexReview) {
|
|
2843
|
+
appendCodexEventLog(comment, event);
|
|
2844
|
+
}
|
|
2845
|
+
if (event.type === 'completed') {
|
|
2846
|
+
comment.progressEvent = null;
|
|
2847
|
+
if (codexReview) renderCommentThread();
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
const nextEvent = {
|
|
2851
|
+
type: event.type || 'codex_event',
|
|
2852
|
+
message: String(event.message).slice(0, 240),
|
|
2853
|
+
detail: typeof event.detail === 'string' ? event.detail.slice(-4096) : '',
|
|
2854
|
+
};
|
|
2855
|
+
const duplicate = comment.progressEvent;
|
|
2856
|
+
if (duplicate && duplicate.type === nextEvent.type && duplicate.message === nextEvent.message && duplicate.detail === nextEvent.detail) return;
|
|
2857
|
+
comment.progressEvent = nextEvent;
|
|
2858
|
+
if (event.type === 'failed' || event.type === 'timeout') comment.failureRaw = typeof event.detail === 'string' ? event.detail.slice(-4096) : '';
|
|
2859
|
+
renderCommentThread();
|
|
2860
|
+
}
|
|
2861
|
+
|
|
2862
|
+
function appendCodexEventLog(target, event) {
|
|
2863
|
+
if (!target.eventLog) target.eventLog = [];
|
|
2864
|
+
const next = {
|
|
2865
|
+
type: event.type || 'codex_event',
|
|
2866
|
+
message: String(event.message || '').slice(0, 500),
|
|
2867
|
+
detail: typeof event.detail === 'string' ? event.detail.slice(-12000) : '',
|
|
2868
|
+
timestamp: typeof event.timestamp === 'number' ? event.timestamp : Date.now(),
|
|
2869
|
+
};
|
|
2870
|
+
const previous = target.eventLog[target.eventLog.length - 1];
|
|
2871
|
+
if (previous && previous.type === next.type && previous.message === next.message && previous.detail === next.detail) return;
|
|
2872
|
+
target.eventLog.push(next);
|
|
2873
|
+
if (target.eventLog.length > 250) target.eventLog.splice(0, target.eventLog.length - 250);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
function codexLogSummary(log) {
|
|
2877
|
+
const count = Array.isArray(log) ? log.length : 0;
|
|
2878
|
+
return count === 1 ? 'Codex execution log (1 event)' : 'Codex execution log (' + count + ' events)';
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
function renderCodexLog(log) {
|
|
2882
|
+
if (!codexReview || !Array.isArray(log) || !log.length) return null;
|
|
2883
|
+
const details = document.createElement('details');
|
|
2884
|
+
details.className = 'codex-log';
|
|
2885
|
+
const summary = document.createElement('summary');
|
|
2886
|
+
summary.textContent = codexLogSummary(log);
|
|
2887
|
+
const list = document.createElement('div');
|
|
2888
|
+
list.className = 'codex-log-list';
|
|
2889
|
+
log.forEach((item) => {
|
|
2890
|
+
const row = document.createElement('div');
|
|
2891
|
+
row.className = 'codex-log-entry';
|
|
2892
|
+
const meta = document.createElement('div');
|
|
2893
|
+
meta.className = 'codex-log-meta';
|
|
2894
|
+
const type = document.createElement('span');
|
|
2895
|
+
type.textContent = item.type || 'event';
|
|
2896
|
+
const time = document.createElement('span');
|
|
2897
|
+
time.textContent = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : '';
|
|
2898
|
+
meta.appendChild(type);
|
|
2899
|
+
meta.appendChild(time);
|
|
2900
|
+
const message = document.createElement('div');
|
|
2901
|
+
message.className = 'codex-log-message';
|
|
2902
|
+
message.textContent = item.message || '';
|
|
2903
|
+
row.appendChild(meta);
|
|
2904
|
+
row.appendChild(message);
|
|
2905
|
+
if (item.detail) {
|
|
2906
|
+
const detail = document.createElement('pre');
|
|
2907
|
+
detail.className = 'codex-log-detail';
|
|
2908
|
+
detail.textContent = item.detail;
|
|
2909
|
+
row.appendChild(detail);
|
|
2910
|
+
}
|
|
2911
|
+
list.appendChild(row);
|
|
2912
|
+
});
|
|
2913
|
+
details.appendChild(summary);
|
|
2914
|
+
details.appendChild(list);
|
|
2915
|
+
return details;
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2622
2918
|
function renderCommentThread() {
|
|
2623
2919
|
els.commentThread.textContent = '';
|
|
2624
2920
|
state.pendingComments.forEach((comment) => {
|
|
@@ -2635,6 +2931,28 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2635
2931
|
|
|
2636
2932
|
bubble.appendChild(text);
|
|
2637
2933
|
bubble.appendChild(status);
|
|
2934
|
+
if (comment.progressEvent) {
|
|
2935
|
+
const progress = document.createElement('div');
|
|
2936
|
+
progress.className = 'comment-progress';
|
|
2937
|
+
const line = document.createElement('div');
|
|
2938
|
+
line.className = 'comment-progress-line';
|
|
2939
|
+
line.textContent = comment.progressEvent.message;
|
|
2940
|
+
progress.appendChild(line);
|
|
2941
|
+
bubble.appendChild(progress);
|
|
2942
|
+
}
|
|
2943
|
+
if (comment.status === 'failed' && comment.failureRaw) {
|
|
2944
|
+
const details = document.createElement('details');
|
|
2945
|
+
details.className = 'comment-raw';
|
|
2946
|
+
const summary = document.createElement('summary');
|
|
2947
|
+
summary.textContent = 'Details';
|
|
2948
|
+
const pre = document.createElement('pre');
|
|
2949
|
+
pre.textContent = comment.failureRaw;
|
|
2950
|
+
details.appendChild(summary);
|
|
2951
|
+
details.appendChild(pre);
|
|
2952
|
+
bubble.appendChild(details);
|
|
2953
|
+
}
|
|
2954
|
+
const codexLog = renderCodexLog(comment.eventLog);
|
|
2955
|
+
if (codexLog) bubble.appendChild(codexLog);
|
|
2638
2956
|
els.commentThread.appendChild(bubble);
|
|
2639
2957
|
});
|
|
2640
2958
|
}
|
|
@@ -2767,6 +3085,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2767
3085
|
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
3086
|
+ '<div class="skeleton-card"><div class="skeleton-line short"></div><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div>'
|
|
2769
3087
|
+ '<div class="skeleton-card"><div class="skeleton-line short"></div><div class="skeleton-line long"></div><div class="skeleton-line medium"></div></div>';
|
|
3088
|
+
renderInspectCodexLog();
|
|
2770
3089
|
}
|
|
2771
3090
|
|
|
2772
3091
|
function getInspectComment() {
|
|
@@ -2783,6 +3102,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2783
3102
|
setMode('inspect');
|
|
2784
3103
|
els.inspectStale.innerHTML = '';
|
|
2785
3104
|
state.inspectFallback = null;
|
|
3105
|
+
state.inspectEventLog = [];
|
|
2786
3106
|
renderInspectLoading('Reading selection...');
|
|
2787
3107
|
try {
|
|
2788
3108
|
const res = await fetch('/api/inspect?token=' + encodeURIComponent(token), {
|
|
@@ -2796,7 +3116,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2796
3116
|
state.activeInspectRequestId = body.requestId;
|
|
2797
3117
|
state.inspectFallback = body.preprocess || null;
|
|
2798
3118
|
renderInspectLoading('Waiting for Purpose and Source...');
|
|
2799
|
-
await
|
|
3119
|
+
await watchInspectProgress(body.requestId);
|
|
2800
3120
|
} catch (error) {
|
|
2801
3121
|
if (state.inspectFallback) {
|
|
2802
3122
|
renderInspectResult(state.inspectFallback, 'Deterministic fallback');
|
|
@@ -2826,6 +3146,92 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
2826
3146
|
throw new Error('Insight timed out while waiting for Review agent result');
|
|
2827
3147
|
}
|
|
2828
3148
|
|
|
3149
|
+
async function fetchInspectResultOnce(requestId) {
|
|
3150
|
+
const res = await fetch('/api/inspect-result?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId), { cache: 'no-store' });
|
|
3151
|
+
const body = await res.json().catch(() => ({}));
|
|
3152
|
+
if (!res.ok || !body.ok) throw new Error(body.error || 'Insight result failed');
|
|
3153
|
+
if (body.status === 'completed') {
|
|
3154
|
+
state.deckVersion = body.deckVersion || state.deckVersion;
|
|
3155
|
+
renderInspectResult(body.result, 'Generated');
|
|
3156
|
+
renderInspectCodexLog();
|
|
3157
|
+
return true;
|
|
3158
|
+
}
|
|
3159
|
+
if (body.status === 'failed' || body.status === 'expired') {
|
|
3160
|
+
const error = new Error(body.error || 'Insight failed');
|
|
3161
|
+
error.raw = body.raw || '';
|
|
3162
|
+
throw error;
|
|
3163
|
+
}
|
|
3164
|
+
return false;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
async function watchInspectProgress(requestId) {
|
|
3168
|
+
if (!requestId) return;
|
|
3169
|
+
if (!codexReview || !('EventSource' in window)) {
|
|
3170
|
+
await pollInspectResult(requestId);
|
|
3171
|
+
renderInspectCodexLog();
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
await new Promise((resolve, reject) => {
|
|
3175
|
+
let settled = false;
|
|
3176
|
+
let source;
|
|
3177
|
+
const finish = (ok, error) => {
|
|
3178
|
+
if (settled) return;
|
|
3179
|
+
settled = true;
|
|
3180
|
+
if (source) source.close();
|
|
3181
|
+
if (ok) resolve();
|
|
3182
|
+
else reject(error);
|
|
3183
|
+
};
|
|
3184
|
+
try {
|
|
3185
|
+
source = new EventSource('/api/inspect-events?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId));
|
|
3186
|
+
} catch (error) {
|
|
3187
|
+
pollInspectResult(requestId).then(resolve, reject);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
source.addEventListener('progress', (event) => {
|
|
3191
|
+
let payload;
|
|
3192
|
+
try {
|
|
3193
|
+
payload = JSON.parse(event.data || '{}');
|
|
3194
|
+
} catch {
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
recordInspectProgress(payload);
|
|
3198
|
+
if (payload.type === 'failed' || payload.type === 'timeout') {
|
|
3199
|
+
const error = new Error(payload.message || 'Insight failed');
|
|
3200
|
+
error.raw = payload.detail || '';
|
|
3201
|
+
finish(false, error);
|
|
3202
|
+
} else if (payload.type === 'completed') {
|
|
3203
|
+
fetchInspectResultOnce(requestId).then((ready) => {
|
|
3204
|
+
if (ready) finish(true);
|
|
3205
|
+
else pollInspectResult(requestId).then(() => finish(true), (error) => finish(false, error));
|
|
3206
|
+
}, (error) => finish(false, error));
|
|
3207
|
+
}
|
|
3208
|
+
});
|
|
3209
|
+
source.onerror = () => {
|
|
3210
|
+
if (!settled) {
|
|
3211
|
+
source.close();
|
|
3212
|
+
pollInspectResult(requestId).then(() => finish(true), (error) => finish(false, error));
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
function recordInspectProgress(event) {
|
|
3219
|
+
if (!event || !event.message) return;
|
|
3220
|
+
const inspectLog = { eventLog: state.inspectEventLog };
|
|
3221
|
+
appendCodexEventLog(inspectLog, event);
|
|
3222
|
+
state.inspectEventLog = inspectLog.eventLog;
|
|
3223
|
+
if (event.type !== 'completed') {
|
|
3224
|
+
renderInspectLoading(event.message);
|
|
3225
|
+
} else {
|
|
3226
|
+
renderInspectCodexLog();
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
function renderInspectCodexLog() {
|
|
3231
|
+
const codexLog = renderCodexLog(state.inspectEventLog);
|
|
3232
|
+
if (codexLog) els.inspectCards.appendChild(codexLog);
|
|
3233
|
+
}
|
|
3234
|
+
|
|
2829
3235
|
function collectReferenceSnapshot() {
|
|
2830
3236
|
const elements = state.references.map((reference) => reference.payload);
|
|
2831
3237
|
const first = elements[0] || {};
|
package/lib/runtime/review.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.17.13",
|
|
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": "*"
|