@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 +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 +435 -23
- 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
|
|
@@ -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
|
|
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:
|
|
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({
|
|
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
|
|
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"
|
|
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
|
|
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)
|
|
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)
|
|
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
|
|
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] || {};
|
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.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": "*"
|