@jawkx1999/opencr 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
Files changed (34) hide show
  1. package/dist/client/assets/index-C9jeaZcr.css +1 -0
  2. package/dist/client/assets/index-DF6LDO7P.js +1978 -0
  3. package/dist/client/index.html +3 -3
  4. package/dist/server/cli/index.js +32 -1
  5. package/dist/server/cli/index.js.map +1 -1
  6. package/dist/server/core/agents/opencode.js +685 -0
  7. package/dist/server/core/agents/opencode.js.map +1 -0
  8. package/dist/server/core/reviewDiagnostics.js +116 -0
  9. package/dist/server/core/reviewDiagnostics.js.map +1 -0
  10. package/dist/server/core/reviewPayload.js +7 -0
  11. package/dist/server/core/reviewPayload.js.map +1 -1
  12. package/dist/server/core/reviewRequest.js +4 -1
  13. package/dist/server/core/reviewRequest.js.map +1 -1
  14. package/dist/server/core/reviewSource/commitish.js +1 -1
  15. package/dist/server/core/reviewSource/commitish.js.map +1 -1
  16. package/dist/server/core/reviewSource/githubPr.js +31 -0
  17. package/dist/server/core/reviewSource/githubPr.js.map +1 -1
  18. package/dist/server/core/reviewSource/index.js +2 -2
  19. package/dist/server/core/reviewSource/index.js.map +1 -1
  20. package/dist/server/core/server.js +27 -6
  21. package/dist/server/core/server.js.map +1 -1
  22. package/dist/server/core/workspace/identity.js +47 -0
  23. package/dist/server/core/workspace/identity.js.map +1 -0
  24. package/dist/server/core/workspace/repository.js +399 -0
  25. package/dist/server/core/workspace/repository.js.map +1 -0
  26. package/dist/server/server/app.js +204 -2
  27. package/dist/server/server/app.js.map +1 -1
  28. package/dist/server/shared/types.js +1 -1
  29. package/dist/server/shared/types.js.map +1 -1
  30. package/package.json +5 -4
  31. package/dist/client/assets/index-CMvi92Od.js +0 -1974
  32. package/dist/client/assets/index-dwKNSbA7.css +0 -1
  33. package/dist/server/cli/utils.js +0 -2
  34. package/dist/server/cli/utils.js.map +0 -1
@@ -0,0 +1,399 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { DatabaseSync } from 'node:sqlite';
5
+ import { DEFAULT_REVIEW_PANEL_WIDTH_PX } from '../../shared/types.js';
6
+ const DEFAULT_DB_PATH = resolve(homedir(), '.opencr', 'state.db');
7
+ const isObject = (value) => {
8
+ return typeof value === 'object' && value !== null;
9
+ };
10
+ const isSelectionSide = (value) => {
11
+ return value === 'additions' || value === 'deletions';
12
+ };
13
+ const isAgentBackendId = (value) => {
14
+ return value === 'opencode' || value === 'codex';
15
+ };
16
+ const isPersistedRange = (value) => {
17
+ if (!isObject(value)) {
18
+ return false;
19
+ }
20
+ if (typeof value.start !== 'number' || typeof value.end !== 'number') {
21
+ return false;
22
+ }
23
+ if (value.side !== undefined && !isSelectionSide(value.side)) {
24
+ return false;
25
+ }
26
+ if (value.endSide !== undefined && !isSelectionSide(value.endSide)) {
27
+ return false;
28
+ }
29
+ return true;
30
+ };
31
+ const isPersistedSelection = (value) => {
32
+ if (!isObject(value)) {
33
+ return false;
34
+ }
35
+ return (typeof value.fileId === 'string' &&
36
+ typeof value.fileLabel === 'string' &&
37
+ isPersistedRange(value.range));
38
+ };
39
+ const isPersistedNote = (value) => {
40
+ if (!isObject(value)) {
41
+ return false;
42
+ }
43
+ return (typeof value.id === 'string' &&
44
+ typeof value.body === 'string' &&
45
+ typeof value.fileId === 'string' &&
46
+ typeof value.fileLabel === 'string' &&
47
+ isPersistedRange(value.range));
48
+ };
49
+ const parseAgentSessionTarget = (value) => {
50
+ if (!isObject(value)) {
51
+ return null;
52
+ }
53
+ if (!isAgentBackendId(value.backendId)) {
54
+ return null;
55
+ }
56
+ if (value.modelId !== undefined && value.modelId !== null && typeof value.modelId !== 'string') {
57
+ return null;
58
+ }
59
+ if (value.variant !== undefined && value.variant !== null && typeof value.variant !== 'string') {
60
+ return null;
61
+ }
62
+ const modelId = value.modelId === null || typeof value.modelId === 'string' ? (value.modelId ?? null) : null;
63
+ const variant = value.variant === null || typeof value.variant === 'string' ? (value.variant ?? null) : null;
64
+ return {
65
+ backendId: value.backendId,
66
+ modelId,
67
+ variant,
68
+ };
69
+ };
70
+ const parsePersistedAgentSession = (value) => {
71
+ if (!isObject(value)) {
72
+ return null;
73
+ }
74
+ if (typeof value.id !== 'string' ||
75
+ typeof value.title !== 'string' ||
76
+ typeof value.prompt !== 'string' ||
77
+ typeof value.createdAt !== 'string' ||
78
+ typeof value.updatedAt !== 'string' ||
79
+ typeof value.fileId !== 'string' ||
80
+ typeof value.fileLabel !== 'string' ||
81
+ !isPersistedRange(value.range)) {
82
+ return null;
83
+ }
84
+ const legacyOpencodeSessionId = typeof value.opencodeSessionId === 'string' ? value.opencodeSessionId : null;
85
+ const backendId = isAgentBackendId(value.backendId)
86
+ ? value.backendId
87
+ : legacyOpencodeSessionId != null
88
+ ? 'opencode'
89
+ : null;
90
+ const backendSessionId = typeof value.backendSessionId === 'string' ? value.backendSessionId : legacyOpencodeSessionId;
91
+ if (backendId == null || backendSessionId == null) {
92
+ return null;
93
+ }
94
+ if (value.modelId !== undefined && value.modelId !== null && typeof value.modelId !== 'string') {
95
+ return null;
96
+ }
97
+ if (value.variant !== undefined && value.variant !== null && typeof value.variant !== 'string') {
98
+ return null;
99
+ }
100
+ const modelId = value.modelId === null || typeof value.modelId === 'string' ? (value.modelId ?? null) : null;
101
+ const variant = value.variant === null || typeof value.variant === 'string' ? (value.variant ?? null) : null;
102
+ return {
103
+ id: value.id,
104
+ backendId,
105
+ backendSessionId,
106
+ modelId,
107
+ variant,
108
+ title: value.title,
109
+ prompt: value.prompt,
110
+ createdAt: value.createdAt,
111
+ updatedAt: value.updatedAt,
112
+ fileId: value.fileId,
113
+ fileLabel: value.fileLabel,
114
+ range: value.range,
115
+ };
116
+ };
117
+ const parseWorkspace = (json) => {
118
+ try {
119
+ const parsed = JSON.parse(json);
120
+ if (!isObject(parsed)) {
121
+ return null;
122
+ }
123
+ if (!Array.isArray(parsed.notes) || !parsed.notes.every(isPersistedNote)) {
124
+ return null;
125
+ }
126
+ if (parsed.agentSessions !== undefined && !Array.isArray(parsed.agentSessions)) {
127
+ return null;
128
+ }
129
+ const agentSessions = [];
130
+ for (const agentSession of parsed.agentSessions ?? []) {
131
+ const parsedSession = parsePersistedAgentSession(agentSession);
132
+ if (parsedSession == null) {
133
+ return null;
134
+ }
135
+ agentSessions.push(parsedSession);
136
+ }
137
+ let agentDraftTarget = null;
138
+ if (parsed.agentDraftTarget !== undefined && parsed.agentDraftTarget !== null) {
139
+ agentDraftTarget = parseAgentSessionTarget(parsed.agentDraftTarget);
140
+ if (agentDraftTarget == null) {
141
+ return null;
142
+ }
143
+ }
144
+ if (parsed.draftSelection !== null && !isPersistedSelection(parsed.draftSelection)) {
145
+ return null;
146
+ }
147
+ if (typeof parsed.draftNoteBody !== 'string') {
148
+ return null;
149
+ }
150
+ if (parsed.activeNoteId !== null && typeof parsed.activeNoteId !== 'string') {
151
+ return null;
152
+ }
153
+ if (parsed.activeAgentSessionId !== undefined &&
154
+ parsed.activeAgentSessionId !== null &&
155
+ typeof parsed.activeAgentSessionId !== 'string') {
156
+ return null;
157
+ }
158
+ if (parsed.activeSidebarPanel !== undefined &&
159
+ parsed.activeSidebarPanel !== 'notes' &&
160
+ parsed.activeSidebarPanel !== 'agents') {
161
+ return null;
162
+ }
163
+ if (typeof parsed.isPanelOpen !== 'boolean') {
164
+ return null;
165
+ }
166
+ if (parsed.panelWidthPx !== undefined &&
167
+ (typeof parsed.panelWidthPx !== 'number' ||
168
+ !Number.isFinite(parsed.panelWidthPx) ||
169
+ parsed.panelWidthPx <= 0)) {
170
+ return null;
171
+ }
172
+ if (parsed.diffViewMode !== 'split' && parsed.diffViewMode !== 'unified') {
173
+ return null;
174
+ }
175
+ return {
176
+ notes: parsed.notes,
177
+ agentSessions,
178
+ agentDraftTarget,
179
+ draftSelection: parsed.draftSelection,
180
+ draftNoteBody: parsed.draftNoteBody,
181
+ activeNoteId: parsed.activeNoteId,
182
+ activeAgentSessionId: parsed.activeAgentSessionId ?? null,
183
+ activeSidebarPanel: parsed.activeSidebarPanel ?? 'notes',
184
+ isPanelOpen: parsed.isPanelOpen,
185
+ panelWidthPx: parsed.panelWidthPx ?? DEFAULT_REVIEW_PANEL_WIDTH_PX,
186
+ diffViewMode: parsed.diffViewMode,
187
+ };
188
+ }
189
+ catch {
190
+ return null;
191
+ }
192
+ };
193
+ const WORKSPACES_TABLE_SQL = `
194
+ CREATE TABLE IF NOT EXISTS workspaces (
195
+ target_id TEXT PRIMARY KEY,
196
+ last_saved_snapshot_id TEXT NOT NULL,
197
+ revision INTEGER NOT NULL,
198
+ state_json TEXT NOT NULL,
199
+ created_at TEXT NOT NULL,
200
+ updated_at TEXT NOT NULL
201
+ ) STRICT;
202
+ `;
203
+ export class ReviewWorkspaceRepository {
204
+ db;
205
+ constructor(databasePath = DEFAULT_DB_PATH) {
206
+ mkdirSync(dirname(databasePath), { recursive: true });
207
+ this.db = new DatabaseSync(databasePath, {
208
+ timeout: 2000,
209
+ enableForeignKeyConstraints: true,
210
+ });
211
+ this.db.exec('PRAGMA journal_mode = WAL;');
212
+ this.db.exec('PRAGMA foreign_keys = ON;');
213
+ this.migrateSchema();
214
+ this.db.exec(WORKSPACES_TABLE_SQL);
215
+ }
216
+ close() {
217
+ this.db.close();
218
+ }
219
+ loadWorkspace(targetId) {
220
+ const row = this.getWorkspaceRow(targetId);
221
+ if (row == null) {
222
+ return {
223
+ revision: null,
224
+ savedSnapshotId: null,
225
+ workspace: null,
226
+ };
227
+ }
228
+ const workspace = parseWorkspace(row.state_json);
229
+ if (workspace == null) {
230
+ return {
231
+ revision: null,
232
+ savedSnapshotId: null,
233
+ workspace: null,
234
+ };
235
+ }
236
+ return {
237
+ revision: row.revision,
238
+ savedSnapshotId: row.last_saved_snapshot_id,
239
+ workspace,
240
+ };
241
+ }
242
+ clearWorkspace(targetId) {
243
+ this.db.prepare('DELETE FROM workspaces WHERE target_id = ?').run(targetId);
244
+ }
245
+ saveWorkspace(input) {
246
+ const existingWorkspace = this.getWorkspaceRow(input.targetId);
247
+ if (existingWorkspace == null) {
248
+ if (input.baseRevision !== 0) {
249
+ return {
250
+ status: 'conflict',
251
+ revision: 0,
252
+ workspace: input.workspace,
253
+ };
254
+ }
255
+ const now = new Date().toISOString();
256
+ this.db
257
+ .prepare(`
258
+ INSERT INTO workspaces (
259
+ target_id,
260
+ last_saved_snapshot_id,
261
+ revision,
262
+ state_json,
263
+ created_at,
264
+ updated_at
265
+ ) VALUES (?, ?, ?, ?, ?, ?)
266
+ `)
267
+ .run(input.targetId, input.snapshotId, 1, JSON.stringify(input.workspace), now, now);
268
+ return {
269
+ status: 'saved',
270
+ revision: 1,
271
+ };
272
+ }
273
+ if (existingWorkspace.revision !== input.baseRevision) {
274
+ const latestWorkspace = parseWorkspace(existingWorkspace.state_json) ?? input.workspace;
275
+ return {
276
+ status: 'conflict',
277
+ revision: existingWorkspace.revision,
278
+ workspace: latestWorkspace,
279
+ };
280
+ }
281
+ const nextRevision = existingWorkspace.revision + 1;
282
+ const now = new Date().toISOString();
283
+ this.db
284
+ .prepare(`
285
+ UPDATE workspaces
286
+ SET last_saved_snapshot_id = ?,
287
+ revision = ?,
288
+ state_json = ?,
289
+ updated_at = ?
290
+ WHERE target_id = ?
291
+ `)
292
+ .run(input.snapshotId, nextRevision, JSON.stringify(input.workspace), now, input.targetId);
293
+ return {
294
+ status: 'saved',
295
+ revision: nextRevision,
296
+ };
297
+ }
298
+ migrateSchema() {
299
+ const workspaceColumns = this.getTableColumns('workspaces');
300
+ if (workspaceColumns.length === 0) {
301
+ this.dropLegacyTables();
302
+ return;
303
+ }
304
+ const isLegacySchema = workspaceColumns.includes('snapshot_id') || !workspaceColumns.includes('last_saved_snapshot_id');
305
+ if (isLegacySchema) {
306
+ this.migrateLegacyWorkspaceTable();
307
+ return;
308
+ }
309
+ this.dropLegacyTables();
310
+ }
311
+ migrateLegacyWorkspaceTable() {
312
+ const legacyRows = this.db
313
+ .prepare(`
314
+ SELECT target_id, snapshot_id, revision, state_json, created_at, updated_at
315
+ FROM workspaces
316
+ ORDER BY updated_at DESC
317
+ `)
318
+ .all() ?? [];
319
+ this.db.exec('BEGIN TRANSACTION');
320
+ try {
321
+ this.db.exec('DROP TABLE IF EXISTS workspaces_v2');
322
+ this.db.exec(`
323
+ CREATE TABLE workspaces_v2 (
324
+ target_id TEXT PRIMARY KEY,
325
+ last_saved_snapshot_id TEXT NOT NULL,
326
+ revision INTEGER NOT NULL,
327
+ state_json TEXT NOT NULL,
328
+ created_at TEXT NOT NULL,
329
+ updated_at TEXT NOT NULL
330
+ ) STRICT;
331
+ `);
332
+ const insertWorkspace = this.db.prepare(`
333
+ INSERT INTO workspaces_v2 (
334
+ target_id,
335
+ last_saved_snapshot_id,
336
+ revision,
337
+ state_json,
338
+ created_at,
339
+ updated_at
340
+ ) VALUES (?, ?, ?, ?, ?, ?)
341
+ `);
342
+ const migratedTargetIds = new Set();
343
+ for (const row of legacyRows) {
344
+ if (migratedTargetIds.has(row.target_id)) {
345
+ continue;
346
+ }
347
+ if (parseWorkspace(row.state_json) == null) {
348
+ continue;
349
+ }
350
+ insertWorkspace.run(row.target_id, row.snapshot_id, row.revision, row.state_json, row.created_at, row.updated_at);
351
+ migratedTargetIds.add(row.target_id);
352
+ }
353
+ this.db.exec('DROP TABLE workspaces');
354
+ this.dropLegacyTables();
355
+ this.db.exec('ALTER TABLE workspaces_v2 RENAME TO workspaces');
356
+ this.db.exec('COMMIT');
357
+ }
358
+ catch (error) {
359
+ this.db.exec('ROLLBACK');
360
+ throw error;
361
+ }
362
+ }
363
+ dropLegacyTables() {
364
+ if (this.tableExists('review_snapshots')) {
365
+ this.db.exec('DROP TABLE review_snapshots');
366
+ }
367
+ if (this.tableExists('review_targets')) {
368
+ this.db.exec('DROP TABLE review_targets');
369
+ }
370
+ }
371
+ tableExists(tableName) {
372
+ const row = this.db
373
+ .prepare(`
374
+ SELECT name
375
+ FROM sqlite_master
376
+ WHERE type = 'table'
377
+ AND name = ?
378
+ `)
379
+ .get(tableName);
380
+ return row != null;
381
+ }
382
+ getTableColumns(tableName) {
383
+ if (!this.tableExists(tableName)) {
384
+ return [];
385
+ }
386
+ const rows = this.db.prepare(`PRAGMA table_info(${tableName})`).all();
387
+ return rows.map((row) => row.name);
388
+ }
389
+ getWorkspaceRow(targetId) {
390
+ return this.db
391
+ .prepare(`
392
+ SELECT revision, last_saved_snapshot_id, state_json
393
+ FROM workspaces
394
+ WHERE target_id = ?
395
+ `)
396
+ .get(targetId) ?? null;
397
+ }
398
+ }
399
+ //# sourceMappingURL=repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.js","sourceRoot":"","sources":["../../../../src/core/workspace/repository.ts"],"names":[],"mappings":"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"}
@@ -10,10 +10,58 @@ const clientRoot = resolve(__dirname, '../../client');
10
10
  const clientIndexPath = resolve(clientRoot, 'index.html');
11
11
  const clientAssetsPath = resolve(clientRoot, 'assets');
12
12
  const devUiUrl = process.env.OPENCR_DEV_UI_URL;
13
+ const DEV_API_BASE_QUERY_KEY = 'opencrApiBase';
14
+ const getRequestApiBase = (request) => {
15
+ const host = request.headers.host;
16
+ if (!host) {
17
+ return null;
18
+ }
19
+ return `${request.protocol}://${host}`;
20
+ };
21
+ const isObject = (value) => {
22
+ return typeof value === 'object' && value !== null;
23
+ };
24
+ const isSelectionSide = (value) => {
25
+ return value === 'additions' || value === 'deletions';
26
+ };
27
+ const isAgentBackendId = (value) => {
28
+ return value === 'opencode' || value === 'codex';
29
+ };
30
+ const isAgentSessionTarget = (value) => {
31
+ if (!isObject(value)) {
32
+ return false;
33
+ }
34
+ const hasValidModel = value.modelId === null || typeof value.modelId === 'string';
35
+ const hasValidVariant = value.variant === null || typeof value.variant === 'string';
36
+ return isAgentBackendId(value.backendId) && hasValidModel && hasValidVariant;
37
+ };
38
+ const isPersistedSelection = (value) => {
39
+ if (!isObject(value)) {
40
+ return false;
41
+ }
42
+ if (typeof value.fileId !== 'string' || typeof value.fileLabel !== 'string' || !isObject(value.range)) {
43
+ return false;
44
+ }
45
+ if (typeof value.range.start !== 'number' || typeof value.range.end !== 'number') {
46
+ return false;
47
+ }
48
+ if (value.range.side !== undefined && !isSelectionSide(value.range.side)) {
49
+ return false;
50
+ }
51
+ if (value.range.endSide !== undefined && !isSelectionSide(value.range.endSide)) {
52
+ return false;
53
+ }
54
+ return true;
55
+ };
13
56
  const sendClient = async (request, reply) => {
14
57
  if (!existsSync(clientIndexPath)) {
15
58
  if (devUiUrl) {
16
- return reply.redirect(new URL(request.url, devUiUrl).toString());
59
+ const redirectUrl = new URL(request.url, devUiUrl);
60
+ const apiBase = getRequestApiBase(request);
61
+ if (apiBase) {
62
+ redirectUrl.searchParams.set(DEV_API_BASE_QUERY_KEY, apiBase);
63
+ }
64
+ return reply.redirect(redirectUrl.toString());
17
65
  }
18
66
  return reply
19
67
  .code(503)
@@ -25,16 +73,170 @@ const sendClient = async (request, reply) => {
25
73
  };
26
74
  export async function createApp(options) {
27
75
  const app = Fastify({ logger: false });
76
+ const getAgentSessionParams = (request) => {
77
+ const params = request.params;
78
+ const backendId = params.backendId?.trim() ?? '';
79
+ const sessionId = params.sessionId?.trim() ?? '';
80
+ if (!isAgentBackendId(backendId) || sessionId.length === 0) {
81
+ return null;
82
+ }
83
+ return {
84
+ backendId,
85
+ sessionId,
86
+ };
87
+ };
88
+ app.addHook('onRequest', (request, reply, done) => {
89
+ if (request.url.startsWith('/api/')) {
90
+ const origin = request.headers.origin;
91
+ reply.header('Access-Control-Allow-Origin', origin ?? '*');
92
+ reply.header('Vary', 'Origin');
93
+ reply.header('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
94
+ reply.header('Access-Control-Allow-Headers', 'Content-Type');
95
+ }
96
+ done();
97
+ });
98
+ app.options('/api/*', async (_request, reply) => {
99
+ return reply.code(204).send();
100
+ });
28
101
  app.get('/api/debug-state', async () => options.debugState);
29
- app.get('/api/review', async () => options.reviewPayload);
102
+ app.get('/api/review', async () => options.getReviewPayload());
103
+ app.get('/api/agents/status', async () => options.agentService.getStatus());
104
+ app.get('/api/agents/catalog', async (_request, reply) => {
105
+ try {
106
+ return await options.agentService.getCatalog();
107
+ }
108
+ catch (error) {
109
+ const message = error instanceof Error ? error.message : String(error);
110
+ return reply.code(502).send({ error: message });
111
+ }
112
+ });
113
+ app.get('/api/agents/events', async (request, reply) => {
114
+ reply.hijack();
115
+ reply.raw.writeHead(200, {
116
+ 'Content-Type': 'text/event-stream',
117
+ 'Cache-Control': 'no-cache, no-transform',
118
+ Connection: 'keep-alive',
119
+ 'Access-Control-Allow-Origin': request.headers.origin ?? '*',
120
+ Vary: 'Origin',
121
+ });
122
+ const unsubscribe = options.agentService.subscribeEvents((event) => {
123
+ if (!reply.raw.writableEnded) {
124
+ reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
125
+ }
126
+ });
127
+ const heartbeat = setInterval(() => {
128
+ if (!reply.raw.writableEnded) {
129
+ reply.raw.write('data: {"kind":"connected"}\n\n');
130
+ }
131
+ }, 10_000);
132
+ request.raw.on('close', () => {
133
+ clearInterval(heartbeat);
134
+ unsubscribe();
135
+ });
136
+ });
137
+ app.get('/api/agents/session/:backendId/:sessionId/status', async (request, reply) => {
138
+ const session = getAgentSessionParams(request);
139
+ if (session == null) {
140
+ return reply.code(400).send({ error: 'Invalid session id.' });
141
+ }
142
+ try {
143
+ return {
144
+ status: await options.agentService.getSessionStatus({
145
+ backendId: session.backendId,
146
+ sessionId: session.sessionId,
147
+ }),
148
+ };
149
+ }
150
+ catch (error) {
151
+ const message = error instanceof Error ? error.message : String(error);
152
+ return reply.code(502).send({ error: message });
153
+ }
154
+ });
155
+ app.put('/api/workspace', async (request, reply) => {
156
+ const body = request.body;
157
+ const baseRevision = body?.baseRevision;
158
+ const workspace = body?.workspace;
159
+ if (body == null ||
160
+ typeof baseRevision !== 'number' ||
161
+ !Number.isInteger(baseRevision) ||
162
+ baseRevision < 0 ||
163
+ workspace == null) {
164
+ return reply.code(400).send({ error: 'Invalid workspace payload.' });
165
+ }
166
+ return options.saveWorkspace({
167
+ baseRevision,
168
+ workspace,
169
+ });
170
+ });
171
+ app.post('/api/agents/session', async (request, reply) => {
172
+ const body = request.body;
173
+ const prompt = typeof body?.prompt === 'string' ? body.prompt.trim() : '';
174
+ const selection = body?.selection;
175
+ const target = body?.target;
176
+ if (prompt.length === 0 || !isPersistedSelection(selection) || (target !== undefined && !isAgentSessionTarget(target))) {
177
+ return reply.code(400).send({ error: 'Invalid agent session payload.' });
178
+ }
179
+ try {
180
+ return await options.agentService.createSession({
181
+ prompt,
182
+ selection,
183
+ ...(target === undefined ? {} : { target }),
184
+ diffPatch: options.getReviewPayload().diff.patch,
185
+ });
186
+ }
187
+ catch (error) {
188
+ const message = error instanceof Error ? error.message : String(error);
189
+ return reply.code(502).send({ error: message });
190
+ }
191
+ });
192
+ app.get('/api/agents/session/:backendId/:sessionId/messages', async (request, reply) => {
193
+ const session = getAgentSessionParams(request);
194
+ if (session == null) {
195
+ return reply.code(400).send({ error: 'Invalid session id.' });
196
+ }
197
+ try {
198
+ return await options.agentService.listMessages({
199
+ backendId: session.backendId,
200
+ sessionId: session.sessionId,
201
+ });
202
+ }
203
+ catch (error) {
204
+ const message = error instanceof Error ? error.message : String(error);
205
+ return reply.code(502).send({ error: message });
206
+ }
207
+ });
208
+ app.post('/api/agents/session/:backendId/:sessionId/message', async (request, reply) => {
209
+ const body = request.body;
210
+ const prompt = typeof body?.prompt === 'string' ? body.prompt.trim() : '';
211
+ const session = getAgentSessionParams(request);
212
+ if (session == null || prompt.length === 0) {
213
+ return reply.code(400).send({ error: 'Invalid agent prompt payload.' });
214
+ }
215
+ try {
216
+ return await options.agentService.sendMessage({
217
+ backendId: session.backendId,
218
+ sessionId: session.sessionId,
219
+ request: {
220
+ prompt,
221
+ },
222
+ });
223
+ }
224
+ catch (error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ return reply.code(502).send({ error: message });
227
+ }
228
+ });
30
229
  app.get('/api/heartbeat', async (request, reply) => {
31
230
  reply.hijack();
32
231
  options.debugState.server.activeHeartbeatClients += 1;
33
232
  options.debugState.server.hasSeenHeartbeatClient = true;
233
+ options.onHeartbeatClientConnected();
34
234
  reply.raw.writeHead(200, {
35
235
  'Content-Type': 'text/event-stream',
36
236
  'Cache-Control': 'no-cache, no-transform',
37
237
  Connection: 'keep-alive',
238
+ 'Access-Control-Allow-Origin': request.headers.origin ?? '*',
239
+ Vary: 'Origin',
38
240
  });
39
241
  reply.raw.write('data: connected\n\n');
40
242
  const interval = setInterval(() => {