@tstax/coding-tab 0.1.1 → 0.2.0
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 +65 -8
- package/dist/browser.js +81 -39
- package/dist/browser.js.map +1 -1
- package/dist/server.cjs +661 -128
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +150 -22
- package/dist/server.d.ts +150 -22
- package/dist/server.js +660 -126
- package/dist/server.js.map +1 -1
- package/dist/style.css +310 -17
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -162,7 +162,11 @@ function makeAuthRouter(opts) {
|
|
|
162
162
|
|
|
163
163
|
// src/server/agentRoutes.ts
|
|
164
164
|
import { Router as Router2 } from "express";
|
|
165
|
-
import {
|
|
165
|
+
import {
|
|
166
|
+
Agent,
|
|
167
|
+
CursorAgentError
|
|
168
|
+
} from "@cursor/sdk";
|
|
169
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
166
170
|
|
|
167
171
|
// src/server/models.ts
|
|
168
172
|
import { Cursor } from "@cursor/sdk";
|
|
@@ -203,7 +207,6 @@ async function resolveModel(apiKey, choice) {
|
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
// src/server/sessions.ts
|
|
206
|
-
import { randomUUID } from "crypto";
|
|
207
210
|
var SESSION_IDLE_TTL_MS = 30 * 60 * 1e3;
|
|
208
211
|
var sessions = /* @__PURE__ */ new Map();
|
|
209
212
|
var sweeperStarted = false;
|
|
@@ -214,35 +217,41 @@ function startSweeper() {
|
|
|
214
217
|
const now = Date.now();
|
|
215
218
|
for (const [id, s] of sessions) {
|
|
216
219
|
if (now - s.lastUsedAt > SESSION_IDLE_TTL_MS) {
|
|
217
|
-
|
|
220
|
+
disposeSessionsForChat(id).catch(
|
|
221
|
+
(e) => console.error("[coding-tab] session sweep dispose failed", e)
|
|
222
|
+
);
|
|
218
223
|
}
|
|
219
224
|
}
|
|
220
225
|
}, 5 * 60 * 1e3).unref?.();
|
|
221
226
|
}
|
|
222
|
-
function
|
|
227
|
+
function registerLiveSession(opts) {
|
|
223
228
|
startSweeper();
|
|
224
|
-
const
|
|
229
|
+
const prev = sessions.get(opts.chatId);
|
|
230
|
+
if (prev) {
|
|
231
|
+
sessions.delete(opts.chatId);
|
|
232
|
+
prev.agent[Symbol.asyncDispose]().catch(() => {
|
|
233
|
+
});
|
|
234
|
+
}
|
|
225
235
|
const session = {
|
|
226
|
-
|
|
236
|
+
chatId: opts.chatId,
|
|
227
237
|
githubLogin: opts.githubLogin,
|
|
228
238
|
agent: opts.agent,
|
|
229
|
-
|
|
230
|
-
startingRef: opts.startingRef,
|
|
239
|
+
agentId: opts.agentId,
|
|
231
240
|
createdAt: Date.now(),
|
|
232
241
|
lastUsedAt: Date.now()
|
|
233
242
|
};
|
|
234
|
-
sessions.set(
|
|
243
|
+
sessions.set(opts.chatId, session);
|
|
235
244
|
return session;
|
|
236
245
|
}
|
|
237
|
-
function
|
|
238
|
-
const s = sessions.get(
|
|
246
|
+
function getLiveSession(chatId) {
|
|
247
|
+
const s = sessions.get(chatId);
|
|
239
248
|
if (s) s.lastUsedAt = Date.now();
|
|
240
249
|
return s;
|
|
241
250
|
}
|
|
242
|
-
async function
|
|
243
|
-
const s = sessions.get(
|
|
251
|
+
async function disposeSessionsForChat(chatId) {
|
|
252
|
+
const s = sessions.get(chatId);
|
|
244
253
|
if (!s) return;
|
|
245
|
-
sessions.delete(
|
|
254
|
+
sessions.delete(chatId);
|
|
246
255
|
try {
|
|
247
256
|
await s.agent[Symbol.asyncDispose]();
|
|
248
257
|
} catch (err) {
|
|
@@ -250,6 +259,235 @@ async function disposeSession(id) {
|
|
|
250
259
|
}
|
|
251
260
|
}
|
|
252
261
|
|
|
262
|
+
// src/server/storage.ts
|
|
263
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "fs/promises";
|
|
264
|
+
import { dirname, join, resolve } from "path";
|
|
265
|
+
import { randomUUID } from "crypto";
|
|
266
|
+
function safeId(s) {
|
|
267
|
+
return s.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
268
|
+
}
|
|
269
|
+
async function ensureDir(p) {
|
|
270
|
+
await mkdir(p, { recursive: true });
|
|
271
|
+
}
|
|
272
|
+
async function readJson(path) {
|
|
273
|
+
try {
|
|
274
|
+
const raw = await readFile(path, "utf8");
|
|
275
|
+
return JSON.parse(raw);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err.code === "ENOENT") return null;
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async function writeJsonAtomic(path, data) {
|
|
282
|
+
await ensureDir(dirname(path));
|
|
283
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
284
|
+
await writeFile(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
285
|
+
await rename(tmp, path);
|
|
286
|
+
}
|
|
287
|
+
var MAX_BLOB_BYTES = 32 * 1024;
|
|
288
|
+
function truncateBlob(value) {
|
|
289
|
+
if (value === void 0 || value === null) return value;
|
|
290
|
+
try {
|
|
291
|
+
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
292
|
+
if (str.length <= MAX_BLOB_BYTES) return value;
|
|
293
|
+
const head = str.slice(0, MAX_BLOB_BYTES);
|
|
294
|
+
return `${head}
|
|
295
|
+
|
|
296
|
+
\u2026[truncated ${str.length - MAX_BLOB_BYTES} chars]`;
|
|
297
|
+
} catch {
|
|
298
|
+
return "[unserializable]";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function sanitizeEvent(evt) {
|
|
302
|
+
if (evt.kind === "tool") {
|
|
303
|
+
return {
|
|
304
|
+
...evt,
|
|
305
|
+
args: truncateBlob(evt.args),
|
|
306
|
+
result: truncateBlob(evt.result)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return evt;
|
|
310
|
+
}
|
|
311
|
+
var FileChatStorage = class {
|
|
312
|
+
dataDir;
|
|
313
|
+
/** Per-chat write mutex chain to keep concurrent appends consistent. */
|
|
314
|
+
chains = /* @__PURE__ */ new Map();
|
|
315
|
+
constructor(opts) {
|
|
316
|
+
this.dataDir = resolve(opts.dataDir);
|
|
317
|
+
}
|
|
318
|
+
chatPath(id) {
|
|
319
|
+
return join(this.dataDir, "chats", `${safeId(id)}.json`);
|
|
320
|
+
}
|
|
321
|
+
indexPath(login) {
|
|
322
|
+
return join(this.dataDir, "index", `${safeId(login.toLowerCase())}.json`);
|
|
323
|
+
}
|
|
324
|
+
/** Serialize all reads/writes for a single chat through a chained promise. */
|
|
325
|
+
withChat(id, fn) {
|
|
326
|
+
const prev = this.chains.get(id) ?? Promise.resolve();
|
|
327
|
+
const next = prev.then(fn, fn);
|
|
328
|
+
this.chains.set(
|
|
329
|
+
id,
|
|
330
|
+
next.finally(() => {
|
|
331
|
+
if (this.chains.get(id) === next) this.chains.delete(id);
|
|
332
|
+
})
|
|
333
|
+
);
|
|
334
|
+
return next;
|
|
335
|
+
}
|
|
336
|
+
async readChat(id) {
|
|
337
|
+
return readJson(this.chatPath(id));
|
|
338
|
+
}
|
|
339
|
+
async writeChat(file) {
|
|
340
|
+
await writeJsonAtomic(this.chatPath(file.chat.id), file);
|
|
341
|
+
}
|
|
342
|
+
async readIndex(login) {
|
|
343
|
+
const list = await readJson(this.indexPath(login));
|
|
344
|
+
return list ?? [];
|
|
345
|
+
}
|
|
346
|
+
async writeIndex(login, items) {
|
|
347
|
+
items.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
348
|
+
await writeJsonAtomic(this.indexPath(login), items);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Rebuild the index for a user by scanning every chat file. Used as a
|
|
352
|
+
* self-heal path when the index gets out of sync (e.g. crash mid-write).
|
|
353
|
+
*/
|
|
354
|
+
async rebuildIndex(login) {
|
|
355
|
+
const dir = join(this.dataDir, "chats");
|
|
356
|
+
let entries = [];
|
|
357
|
+
try {
|
|
358
|
+
entries = await readdir(dir);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
if (err.code !== "ENOENT") throw err;
|
|
361
|
+
}
|
|
362
|
+
const items = [];
|
|
363
|
+
const target = login.toLowerCase();
|
|
364
|
+
for (const name of entries) {
|
|
365
|
+
if (!name.endsWith(".json")) continue;
|
|
366
|
+
const file = await readJson(join(dir, name));
|
|
367
|
+
if (!file?.chat) continue;
|
|
368
|
+
if (file.chat.githubLogin.toLowerCase() !== target) continue;
|
|
369
|
+
items.push(this.toListItem(file.chat));
|
|
370
|
+
}
|
|
371
|
+
await this.writeIndex(login, items);
|
|
372
|
+
return items;
|
|
373
|
+
}
|
|
374
|
+
toListItem(c) {
|
|
375
|
+
return {
|
|
376
|
+
id: c.id,
|
|
377
|
+
title: c.title,
|
|
378
|
+
mode: c.mode,
|
|
379
|
+
model: c.model,
|
|
380
|
+
createdAt: c.createdAt,
|
|
381
|
+
updatedAt: c.updatedAt
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async upsertIndex(chat) {
|
|
385
|
+
const items = await this.readIndex(chat.githubLogin);
|
|
386
|
+
const idx = items.findIndex((c) => c.id === chat.id);
|
|
387
|
+
const item = this.toListItem(chat);
|
|
388
|
+
if (idx >= 0) items[idx] = item;
|
|
389
|
+
else items.push(item);
|
|
390
|
+
await this.writeIndex(chat.githubLogin, items);
|
|
391
|
+
}
|
|
392
|
+
async removeFromIndex(login, chatId) {
|
|
393
|
+
const items = await this.readIndex(login);
|
|
394
|
+
const filtered = items.filter((c) => c.id !== chatId);
|
|
395
|
+
if (filtered.length !== items.length) {
|
|
396
|
+
await this.writeIndex(login, filtered);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
async listChats(login) {
|
|
400
|
+
const items = await this.readIndex(login);
|
|
401
|
+
if (items.length > 0) return items;
|
|
402
|
+
return this.rebuildIndex(login);
|
|
403
|
+
}
|
|
404
|
+
async loadChat(id, login) {
|
|
405
|
+
return this.withChat(id, async () => {
|
|
406
|
+
const file = await this.readChat(id);
|
|
407
|
+
if (!file) return null;
|
|
408
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
const turns = [...file.turns].sort((a, b) => a.seq - b.seq);
|
|
412
|
+
return { ...file.chat, turns };
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
async createChat(chat) {
|
|
416
|
+
await this.withChat(chat.id, async () => {
|
|
417
|
+
await this.writeChat({ chat, turns: [] });
|
|
418
|
+
});
|
|
419
|
+
await this.upsertIndex(chat);
|
|
420
|
+
}
|
|
421
|
+
async patchChat(id, login, patch) {
|
|
422
|
+
const updated = await this.withChat(id, async () => {
|
|
423
|
+
const file = await this.readChat(id);
|
|
424
|
+
if (!file) return null;
|
|
425
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const next = {
|
|
429
|
+
...file.chat,
|
|
430
|
+
...patch,
|
|
431
|
+
updatedAt: Date.now()
|
|
432
|
+
};
|
|
433
|
+
await this.writeChat({ chat: next, turns: file.turns });
|
|
434
|
+
return next;
|
|
435
|
+
});
|
|
436
|
+
if (updated) await this.upsertIndex(updated);
|
|
437
|
+
return updated;
|
|
438
|
+
}
|
|
439
|
+
async appendTurn(turn) {
|
|
440
|
+
const sanitized = {
|
|
441
|
+
...turn,
|
|
442
|
+
events: turn.events.map(sanitizeEvent)
|
|
443
|
+
};
|
|
444
|
+
let chat = null;
|
|
445
|
+
await this.withChat(turn.chatId, async () => {
|
|
446
|
+
const file = await this.readChat(turn.chatId);
|
|
447
|
+
if (!file) throw new Error(`chat_not_found:${turn.chatId}`);
|
|
448
|
+
file.turns = file.turns.filter((t) => t.id !== sanitized.id);
|
|
449
|
+
file.turns.push(sanitized);
|
|
450
|
+
file.chat = { ...file.chat, updatedAt: Date.now() };
|
|
451
|
+
chat = file.chat;
|
|
452
|
+
await this.writeChat(file);
|
|
453
|
+
});
|
|
454
|
+
if (chat) await this.upsertIndex(chat);
|
|
455
|
+
}
|
|
456
|
+
async patchTurn(chatId, turnId, patch) {
|
|
457
|
+
let chat = null;
|
|
458
|
+
await this.withChat(chatId, async () => {
|
|
459
|
+
const file = await this.readChat(chatId);
|
|
460
|
+
if (!file) return;
|
|
461
|
+
const idx = file.turns.findIndex((t) => t.id === turnId);
|
|
462
|
+
if (idx < 0) return;
|
|
463
|
+
const current = file.turns[idx];
|
|
464
|
+
const events = patch.events ? patch.events.map(sanitizeEvent) : current.events;
|
|
465
|
+
file.turns[idx] = { ...current, ...patch, events };
|
|
466
|
+
file.chat = { ...file.chat, updatedAt: Date.now() };
|
|
467
|
+
chat = file.chat;
|
|
468
|
+
await this.writeChat(file);
|
|
469
|
+
});
|
|
470
|
+
if (chat) await this.upsertIndex(chat);
|
|
471
|
+
}
|
|
472
|
+
async deleteChat(id, login) {
|
|
473
|
+
const removed = await this.withChat(id, async () => {
|
|
474
|
+
const file = await this.readChat(id);
|
|
475
|
+
if (!file) return false;
|
|
476
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
await rm(this.chatPath(id), { force: true });
|
|
480
|
+
return true;
|
|
481
|
+
});
|
|
482
|
+
if (removed) await this.removeFromIndex(login, id);
|
|
483
|
+
return removed;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
function createDefaultStorage(dataDir) {
|
|
487
|
+
const dir = dataDir ?? join(process.cwd(), ".coding-tab-data");
|
|
488
|
+
return new FileChatStorage({ dataDir: dir });
|
|
489
|
+
}
|
|
490
|
+
|
|
253
491
|
// src/server/agentRoutes.ts
|
|
254
492
|
var PLAN_INSTRUCTION = `You are operating in PLAN MODE.
|
|
255
493
|
|
|
@@ -291,39 +529,221 @@ function setupSseHeaders(res) {
|
|
|
291
529
|
});
|
|
292
530
|
res.flushHeaders?.();
|
|
293
531
|
}
|
|
294
|
-
|
|
532
|
+
function deriveTitle(prompt) {
|
|
533
|
+
const trimmed = prompt.trim().replace(/\s+/g, " ");
|
|
534
|
+
if (trimmed.length <= 60) return trimmed || "New chat";
|
|
535
|
+
return `${trimmed.slice(0, 57)}\u2026`;
|
|
536
|
+
}
|
|
537
|
+
var TurnBuffer = class {
|
|
538
|
+
events = [];
|
|
539
|
+
lastWasText = false;
|
|
540
|
+
pushText(text) {
|
|
541
|
+
if (this.lastWasText) {
|
|
542
|
+
const last = this.events[this.events.length - 1];
|
|
543
|
+
if (last.kind === "text") {
|
|
544
|
+
last.text += text;
|
|
545
|
+
return last;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const evt = { kind: "text", id: randomUUID2(), text };
|
|
549
|
+
this.events.push(evt);
|
|
550
|
+
this.lastWasText = true;
|
|
551
|
+
return evt;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Mark the next text event as starting a new block (called when the SDK
|
|
555
|
+
* emits a non-streaming-delta boundary, e.g. a tool call or a new assistant
|
|
556
|
+
* message). The next `pushText` will start a fresh paragraph.
|
|
557
|
+
*/
|
|
558
|
+
endTextBlock() {
|
|
559
|
+
this.lastWasText = false;
|
|
560
|
+
}
|
|
561
|
+
upsertTool(args) {
|
|
562
|
+
this.endTextBlock();
|
|
563
|
+
const existing = this.events.find(
|
|
564
|
+
(e) => e.kind === "tool" && e.callId === args.callId
|
|
565
|
+
);
|
|
566
|
+
if (existing) {
|
|
567
|
+
existing.status = args.status;
|
|
568
|
+
if (args.args !== void 0) existing.args = args.args;
|
|
569
|
+
if (args.result !== void 0) existing.result = args.result;
|
|
570
|
+
return existing;
|
|
571
|
+
}
|
|
572
|
+
const evt = {
|
|
573
|
+
kind: "tool",
|
|
574
|
+
id: randomUUID2(),
|
|
575
|
+
callId: args.callId,
|
|
576
|
+
name: args.name,
|
|
577
|
+
status: args.status,
|
|
578
|
+
args: args.args,
|
|
579
|
+
result: args.result
|
|
580
|
+
};
|
|
581
|
+
this.events.push(evt);
|
|
582
|
+
return evt;
|
|
583
|
+
}
|
|
584
|
+
snapshot() {
|
|
585
|
+
return this.events.map(sanitizeEvent);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
async function streamRun(res, run, ctx) {
|
|
589
|
+
const flushDebounceMs = 750;
|
|
590
|
+
let flushPending = false;
|
|
591
|
+
let lastFlush = 0;
|
|
592
|
+
const persist = async (status, pr) => {
|
|
593
|
+
try {
|
|
594
|
+
await ctx.storage.patchTurn(ctx.chat.id, ctx.turn.id, {
|
|
595
|
+
events: ctx.buffer.snapshot(),
|
|
596
|
+
...status ? { status } : {},
|
|
597
|
+
...pr ? { pr } : {}
|
|
598
|
+
});
|
|
599
|
+
lastFlush = Date.now();
|
|
600
|
+
} catch (err) {
|
|
601
|
+
console.error("[coding-tab] patchTurn failed", err);
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
const scheduleFlush = () => {
|
|
605
|
+
if (flushPending) return;
|
|
606
|
+
flushPending = true;
|
|
607
|
+
setTimeout(async () => {
|
|
608
|
+
flushPending = false;
|
|
609
|
+
const elapsed = Date.now() - lastFlush;
|
|
610
|
+
if (elapsed < flushDebounceMs) return;
|
|
611
|
+
await persist();
|
|
612
|
+
}, flushDebounceMs).unref?.();
|
|
613
|
+
};
|
|
614
|
+
let finalStatus = "finished";
|
|
615
|
+
let finalPr;
|
|
295
616
|
try {
|
|
296
617
|
for await (const message of run.stream()) {
|
|
297
618
|
if (res.writableEnded) break;
|
|
298
619
|
if (message.type === "assistant") {
|
|
620
|
+
ctx.buffer.endTextBlock();
|
|
299
621
|
for (const block of message.message.content) {
|
|
300
|
-
if (block.type === "text" && block.text)
|
|
301
|
-
|
|
622
|
+
if (block.type === "text" && block.text) {
|
|
623
|
+
ctx.buffer.pushText(block.text);
|
|
624
|
+
ctx.buffer.endTextBlock();
|
|
625
|
+
sse(res, { kind: "text", text: block.text });
|
|
626
|
+
} else if (block.type === "tool_use") {
|
|
627
|
+
ctx.buffer.upsertTool({
|
|
628
|
+
callId: block.id,
|
|
629
|
+
name: block.name,
|
|
630
|
+
status: "running",
|
|
631
|
+
args: block.input
|
|
632
|
+
});
|
|
633
|
+
sse(res, {
|
|
634
|
+
kind: "tool",
|
|
635
|
+
name: block.name,
|
|
636
|
+
status: "running",
|
|
637
|
+
callId: block.id,
|
|
638
|
+
args: block.input
|
|
639
|
+
});
|
|
640
|
+
}
|
|
302
641
|
}
|
|
303
642
|
} else if (message.type === "thinking") {
|
|
304
643
|
sse(res, { kind: "thinking", text: message.text });
|
|
305
644
|
} else if (message.type === "tool_call") {
|
|
306
|
-
|
|
645
|
+
ctx.buffer.upsertTool({
|
|
646
|
+
callId: message.call_id,
|
|
647
|
+
name: message.name,
|
|
648
|
+
status: message.status,
|
|
649
|
+
args: message.args,
|
|
650
|
+
result: message.result
|
|
651
|
+
});
|
|
652
|
+
sse(res, {
|
|
653
|
+
kind: "tool",
|
|
654
|
+
name: message.name,
|
|
655
|
+
status: message.status,
|
|
656
|
+
callId: message.call_id,
|
|
657
|
+
args: message.args,
|
|
658
|
+
result: message.result
|
|
659
|
+
});
|
|
307
660
|
} else if (message.type === "status") {
|
|
308
661
|
sse(res, { kind: "status", status: message.status, message: message.message });
|
|
309
662
|
}
|
|
663
|
+
scheduleFlush();
|
|
310
664
|
}
|
|
311
665
|
const result = await run.wait();
|
|
312
666
|
const prUrl = result.git?.branches?.find((b) => b.prUrl)?.prUrl;
|
|
667
|
+
finalPr = parsePrUrl(prUrl);
|
|
668
|
+
finalStatus = result.status ?? "finished";
|
|
313
669
|
sse(res, {
|
|
314
670
|
kind: "result",
|
|
315
671
|
status: result.status,
|
|
316
|
-
pr:
|
|
672
|
+
pr: finalPr,
|
|
317
673
|
durationMs: result.durationMs
|
|
318
674
|
});
|
|
319
675
|
} catch (err) {
|
|
676
|
+
finalStatus = "error";
|
|
320
677
|
const message = err instanceof Error ? err.message : String(err);
|
|
321
678
|
const retryable = err instanceof CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
322
679
|
sse(res, { kind: "error", message, retryable });
|
|
323
680
|
} finally {
|
|
681
|
+
await persist(finalStatus, finalPr);
|
|
324
682
|
if (!res.writableEnded) res.end();
|
|
325
683
|
}
|
|
326
684
|
}
|
|
685
|
+
async function ensureAgentForChat(storage, chat, user, opts, modelChoice) {
|
|
686
|
+
const existing = getLiveSession(chat.id);
|
|
687
|
+
if (existing && existing.githubLogin.toLowerCase() === user.githubLogin.toLowerCase()) {
|
|
688
|
+
return { agent: existing.agent, agentId: existing.agentId, resumed: true };
|
|
689
|
+
}
|
|
690
|
+
const resolved = await resolveModel(opts.cursorApiKey, modelChoice);
|
|
691
|
+
const cloud = {
|
|
692
|
+
repos: [
|
|
693
|
+
{
|
|
694
|
+
url: chat.repoUrl,
|
|
695
|
+
startingRef: chat.startingRef
|
|
696
|
+
}
|
|
697
|
+
],
|
|
698
|
+
autoCreatePR: chat.mode === "agent",
|
|
699
|
+
skipReviewerRequest: opts.skipReviewerRequest ?? true,
|
|
700
|
+
envVars: { GITHUB_TOKEN: user.accessToken },
|
|
701
|
+
...opts.envName ? { env: { type: "cloud", name: opts.envName } } : {}
|
|
702
|
+
};
|
|
703
|
+
if (chat.agentId) {
|
|
704
|
+
try {
|
|
705
|
+
const resumed = await Agent.resume(chat.agentId, {
|
|
706
|
+
apiKey: opts.cursorApiKey,
|
|
707
|
+
model: { id: resolved.cursorModelId },
|
|
708
|
+
cloud
|
|
709
|
+
});
|
|
710
|
+
registerLiveSession({
|
|
711
|
+
chatId: chat.id,
|
|
712
|
+
githubLogin: user.githubLogin,
|
|
713
|
+
agent: resumed,
|
|
714
|
+
agentId: resumed.agentId
|
|
715
|
+
});
|
|
716
|
+
if (resumed.agentId !== chat.agentId) {
|
|
717
|
+
await storage.patchChat(chat.id, user.githubLogin, { agentId: resumed.agentId });
|
|
718
|
+
}
|
|
719
|
+
return { agent: resumed, agentId: resumed.agentId, resumed: true };
|
|
720
|
+
} catch (err) {
|
|
721
|
+
console.warn(
|
|
722
|
+
`[coding-tab] resume failed for chat=${chat.id} agentId=${chat.agentId}; creating fresh agent`,
|
|
723
|
+
err instanceof Error ? err.message : err
|
|
724
|
+
);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const fresh = await Agent.create({
|
|
728
|
+
apiKey: opts.cursorApiKey,
|
|
729
|
+
model: { id: resolved.cursorModelId },
|
|
730
|
+
cloud
|
|
731
|
+
});
|
|
732
|
+
registerLiveSession({
|
|
733
|
+
chatId: chat.id,
|
|
734
|
+
githubLogin: user.githubLogin,
|
|
735
|
+
agent: fresh,
|
|
736
|
+
agentId: fresh.agentId
|
|
737
|
+
});
|
|
738
|
+
await storage.patchChat(chat.id, user.githubLogin, { agentId: fresh.agentId });
|
|
739
|
+
return { agent: fresh, agentId: fresh.agentId, resumed: false };
|
|
740
|
+
}
|
|
741
|
+
async function nextSeq(storage, chatId, login) {
|
|
742
|
+
const full = await storage.loadChat(chatId, login);
|
|
743
|
+
if (!full) throw Object.assign(new Error("chat_not_found"), { status: 404 });
|
|
744
|
+
const max = full.turns.reduce((acc, t) => Math.max(acc, t.seq), -1);
|
|
745
|
+
return max + 1;
|
|
746
|
+
}
|
|
327
747
|
function makeAgentRouter(opts) {
|
|
328
748
|
const router = Router2();
|
|
329
749
|
router.get("/models", async (_req, res) => {
|
|
@@ -335,144 +755,236 @@ function makeAgentRouter(opts) {
|
|
|
335
755
|
res.status(500).json({ error: err instanceof Error ? err.message : "models_failed" });
|
|
336
756
|
}
|
|
337
757
|
});
|
|
338
|
-
|
|
758
|
+
const handleSend = async (req, res, isExecute) => {
|
|
339
759
|
const user = req.user;
|
|
340
|
-
const
|
|
341
|
-
if (!
|
|
760
|
+
const body = req.body ?? {};
|
|
761
|
+
if (!body.chatId) {
|
|
762
|
+
res.status(400).json({ error: "missing_chatId" });
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
if (!isExecute && (!body.prompt || body.mode !== "plan" && body.mode !== "agent")) {
|
|
342
766
|
res.status(400).json({ error: "invalid_request" });
|
|
343
767
|
return;
|
|
344
768
|
}
|
|
769
|
+
const chat = await opts.storage.loadChat(body.chatId, user.githubLogin);
|
|
770
|
+
if (!chat) {
|
|
771
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
345
774
|
setupSseHeaders(res);
|
|
346
|
-
let
|
|
775
|
+
let assistantTurn = null;
|
|
347
776
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
777
|
+
if (!isExecute && body.mode && body.mode !== chat.mode) {
|
|
778
|
+
chat.mode = body.mode;
|
|
779
|
+
await opts.storage.patchChat(chat.id, user.githubLogin, { mode: body.mode });
|
|
780
|
+
}
|
|
781
|
+
let seq = await nextSeq(opts.storage, chat.id, user.githubLogin);
|
|
782
|
+
if (!isExecute) {
|
|
783
|
+
const userTurn = {
|
|
784
|
+
id: randomUUID2(),
|
|
785
|
+
chatId: chat.id,
|
|
786
|
+
seq: seq++,
|
|
787
|
+
role: "user",
|
|
788
|
+
isPlan: chat.mode === "plan",
|
|
789
|
+
status: "finished",
|
|
790
|
+
events: [{ kind: "text", id: randomUUID2(), text: body.prompt }],
|
|
791
|
+
prompt: body.prompt,
|
|
792
|
+
createdAt: Date.now()
|
|
793
|
+
};
|
|
794
|
+
await opts.storage.appendTurn(userTurn);
|
|
795
|
+
if (chat.title === "New chat") {
|
|
796
|
+
const title = deriveTitle(body.prompt);
|
|
797
|
+
chat.title = title;
|
|
798
|
+
await opts.storage.patchChat(chat.id, user.githubLogin, { title });
|
|
363
799
|
}
|
|
800
|
+
}
|
|
801
|
+
assistantTurn = {
|
|
802
|
+
id: randomUUID2(),
|
|
803
|
+
chatId: chat.id,
|
|
804
|
+
seq,
|
|
805
|
+
role: "assistant",
|
|
806
|
+
isPlan: !isExecute && chat.mode === "plan",
|
|
807
|
+
status: "running",
|
|
808
|
+
events: [],
|
|
809
|
+
createdAt: Date.now()
|
|
810
|
+
};
|
|
811
|
+
await opts.storage.appendTurn(assistantTurn);
|
|
812
|
+
const { agent } = await ensureAgentForChat(opts.storage, chat, user, opts, chat.model);
|
|
813
|
+
const sendText = isExecute ? EXECUTE_INSTRUCTION : modeInstructionPrefix(body.mode, body.prompt);
|
|
814
|
+
const run = await agent.send(sendText);
|
|
815
|
+
sse(res, {
|
|
816
|
+
kind: "ready",
|
|
817
|
+
chatId: chat.id,
|
|
818
|
+
turnId: assistantTurn.id,
|
|
819
|
+
agentId: agent.agentId,
|
|
820
|
+
runId: run.id
|
|
364
821
|
});
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
822
|
+
console.log(
|
|
823
|
+
`[coding-tab] ${isExecute ? "execute" : "send"} agent=${agent.agentId} run=${run.id} chat=${chat.id} login=${user.githubLogin}`
|
|
824
|
+
);
|
|
825
|
+
const buffer = new TurnBuffer();
|
|
826
|
+
await streamRun(res, run, {
|
|
827
|
+
storage: opts.storage,
|
|
828
|
+
chat,
|
|
829
|
+
turn: assistantTurn,
|
|
830
|
+
buffer
|
|
370
831
|
});
|
|
371
|
-
const run = await agent.send(modeInstructionPrefix(mode, prompt));
|
|
372
|
-
sse(res, { kind: "ready", sessionId: session.id, agentId: agent.agentId, runId: run.id });
|
|
373
|
-
console.log(`[coding-tab] start agent=${agent.agentId} run=${run.id} session=${session.id} login=${user.githubLogin}`);
|
|
374
|
-
await streamRun(res, run);
|
|
375
832
|
} catch (err) {
|
|
376
833
|
const message = err instanceof Error ? err.message : String(err);
|
|
377
834
|
const retryable = err instanceof CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
378
|
-
console.error(
|
|
835
|
+
console.error(`[coding-tab] /agent/${isExecute ? "execute" : "send"} failed`, err);
|
|
379
836
|
sse(res, { kind: "error", message, retryable });
|
|
837
|
+
if (assistantTurn) {
|
|
838
|
+
await opts.storage.patchTurn(assistantTurn.chatId, assistantTurn.id, {
|
|
839
|
+
status: "error",
|
|
840
|
+
events: [
|
|
841
|
+
...assistantTurn.events,
|
|
842
|
+
{ kind: "text", id: randomUUID2(), text: `[error] ${message}` }
|
|
843
|
+
]
|
|
844
|
+
}).catch(() => {
|
|
845
|
+
});
|
|
846
|
+
}
|
|
380
847
|
if (!res.writableEnded) res.end();
|
|
381
|
-
if (agent) await agent[Symbol.asyncDispose]().catch(() => {
|
|
382
|
-
});
|
|
383
848
|
}
|
|
384
849
|
req.on("close", () => {
|
|
385
850
|
if (!res.writableEnded) res.end();
|
|
386
851
|
});
|
|
387
|
-
}
|
|
388
|
-
router.post("/agent/
|
|
389
|
-
|
|
390
|
-
|
|
852
|
+
};
|
|
853
|
+
router.post("/agent/start", (req, res) => handleSend(req, res, false));
|
|
854
|
+
router.post("/agent/send", (req, res) => handleSend(req, res, false));
|
|
855
|
+
router.post("/agent/execute", (req, res) => handleSend(req, res, true));
|
|
856
|
+
router.post("/agent/cancel", async (req, res) => {
|
|
857
|
+
const { chatId, runId } = req.body ?? {};
|
|
858
|
+
if (!chatId || !runId) {
|
|
391
859
|
res.status(400).json({ error: "invalid_request" });
|
|
392
860
|
return;
|
|
393
861
|
}
|
|
394
|
-
const
|
|
395
|
-
if (!
|
|
862
|
+
const live = getLiveSession(chatId);
|
|
863
|
+
if (!live) {
|
|
396
864
|
res.status(404).json({ error: "session_not_found" });
|
|
397
865
|
return;
|
|
398
866
|
}
|
|
399
|
-
setupSseHeaders(res);
|
|
400
867
|
try {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
868
|
+
await Agent.cancelRun(runId, {
|
|
869
|
+
runtime: "cloud",
|
|
870
|
+
agentId: live.agentId,
|
|
871
|
+
apiKey: opts.cursorApiKey
|
|
872
|
+
});
|
|
873
|
+
res.json({ ok: true });
|
|
405
874
|
} catch (err) {
|
|
406
875
|
const message = err instanceof Error ? err.message : String(err);
|
|
407
|
-
console.error("[coding-tab] /agent/
|
|
408
|
-
|
|
409
|
-
if (!res.writableEnded) res.end();
|
|
876
|
+
console.error("[coding-tab] /agent/cancel failed", err);
|
|
877
|
+
res.status(500).json({ error: message });
|
|
410
878
|
}
|
|
411
|
-
req.on("close", () => {
|
|
412
|
-
if (!res.writableEnded) res.end();
|
|
413
|
-
});
|
|
414
879
|
});
|
|
415
|
-
router.post("/agent/
|
|
416
|
-
const {
|
|
417
|
-
if (!
|
|
880
|
+
router.post("/agent/dispose", async (req, res) => {
|
|
881
|
+
const { chatId } = req.body ?? {};
|
|
882
|
+
if (!chatId) {
|
|
418
883
|
res.status(400).json({ error: "invalid_request" });
|
|
419
884
|
return;
|
|
420
885
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
886
|
+
await disposeSessionsForChat(chatId);
|
|
887
|
+
res.json({ ok: true });
|
|
888
|
+
});
|
|
889
|
+
return router;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// src/server/chatRoutes.ts
|
|
893
|
+
import { Router as Router3 } from "express";
|
|
894
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
895
|
+
var VALID_MODES = ["plan", "agent"];
|
|
896
|
+
var VALID_MODELS = ["sonnet", "opus"];
|
|
897
|
+
function makeChatRouter(opts) {
|
|
898
|
+
const router = Router3();
|
|
899
|
+
router.get("/chats", async (req, res) => {
|
|
900
|
+
const user = req.user;
|
|
427
901
|
try {
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
console.log(`[coding-tab] execute agent=${session.agent.agentId} run=${run.id} session=${session.id}`);
|
|
431
|
-
await streamRun(res, run);
|
|
902
|
+
const items = await opts.storage.listChats(user.githubLogin);
|
|
903
|
+
res.json({ chats: items });
|
|
432
904
|
} catch (err) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
sse(res, { kind: "error", message });
|
|
436
|
-
if (!res.writableEnded) res.end();
|
|
905
|
+
console.error("[coding-tab] /chats list failed", err);
|
|
906
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "list_failed" });
|
|
437
907
|
}
|
|
438
|
-
req.on("close", () => {
|
|
439
|
-
if (!res.writableEnded) res.end();
|
|
440
|
-
});
|
|
441
908
|
});
|
|
442
|
-
router.post("/
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
909
|
+
router.post("/chats", async (req, res) => {
|
|
910
|
+
const user = req.user;
|
|
911
|
+
const body = req.body ?? {};
|
|
912
|
+
const mode = VALID_MODES.includes(body.mode) ? body.mode : "plan";
|
|
913
|
+
const model = VALID_MODELS.includes(body.model) ? body.model : "sonnet";
|
|
914
|
+
const now = Date.now();
|
|
915
|
+
const chat = {
|
|
916
|
+
id: randomUUID3(),
|
|
917
|
+
githubLogin: user.githubLogin,
|
|
918
|
+
title: (body.title ?? "").trim() || "New chat",
|
|
919
|
+
mode,
|
|
920
|
+
model,
|
|
921
|
+
repoUrl: body.repoUrl?.trim() || opts.defaultRepo.url,
|
|
922
|
+
startingRef: body.startingRef ?? opts.defaultRepo.ref,
|
|
923
|
+
createdAt: now,
|
|
924
|
+
updatedAt: now
|
|
925
|
+
};
|
|
926
|
+
try {
|
|
927
|
+
await opts.storage.createChat(chat);
|
|
928
|
+
res.status(201).json({ chat });
|
|
929
|
+
} catch (err) {
|
|
930
|
+
console.error("[coding-tab] /chats create failed", err);
|
|
931
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "create_failed" });
|
|
447
932
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
933
|
+
});
|
|
934
|
+
router.get("/chats/:id", async (req, res) => {
|
|
935
|
+
const user = req.user;
|
|
936
|
+
try {
|
|
937
|
+
const full = await opts.storage.loadChat(req.params.id, user.githubLogin);
|
|
938
|
+
if (!full) {
|
|
939
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
res.json({ chat: full });
|
|
943
|
+
} catch (err) {
|
|
944
|
+
console.error("[coding-tab] /chats/:id load failed", err);
|
|
945
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "load_failed" });
|
|
452
946
|
}
|
|
947
|
+
});
|
|
948
|
+
router.patch("/chats/:id", async (req, res) => {
|
|
949
|
+
const user = req.user;
|
|
950
|
+
const body = req.body ?? {};
|
|
951
|
+
const patch = {};
|
|
952
|
+
if (typeof body.title === "string") patch.title = body.title.trim() || "Untitled";
|
|
953
|
+
if (body.mode && VALID_MODES.includes(body.mode)) patch.mode = body.mode;
|
|
954
|
+
if (body.model && VALID_MODELS.includes(body.model)) patch.model = body.model;
|
|
453
955
|
try {
|
|
454
|
-
|
|
455
|
-
|
|
956
|
+
const updated = await opts.storage.patchChat(req.params.id, user.githubLogin, patch);
|
|
957
|
+
if (!updated) {
|
|
958
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
res.json({ chat: updated });
|
|
456
962
|
} catch (err) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
res.status(500).json({ error: message });
|
|
963
|
+
console.error("[coding-tab] /chats/:id patch failed", err);
|
|
964
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "patch_failed" });
|
|
460
965
|
}
|
|
461
966
|
});
|
|
462
|
-
router.
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
967
|
+
router.delete("/chats/:id", async (req, res) => {
|
|
968
|
+
const user = req.user;
|
|
969
|
+
try {
|
|
970
|
+
const removed = await opts.storage.deleteChat(req.params.id, user.githubLogin);
|
|
971
|
+
if (!removed) {
|
|
972
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
await disposeSessionsForChat(req.params.id).catch(() => {
|
|
976
|
+
});
|
|
977
|
+
res.json({ ok: true });
|
|
978
|
+
} catch (err) {
|
|
979
|
+
console.error("[coding-tab] /chats/:id delete failed", err);
|
|
980
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "delete_failed" });
|
|
467
981
|
}
|
|
468
|
-
await disposeSession(sessionId);
|
|
469
|
-
res.json({ ok: true });
|
|
470
982
|
});
|
|
471
983
|
return router;
|
|
472
984
|
}
|
|
473
985
|
|
|
474
986
|
// src/server/githubRoutes.ts
|
|
475
|
-
import { Router as
|
|
987
|
+
import { Router as Router4 } from "express";
|
|
476
988
|
import { Octokit } from "@octokit/rest";
|
|
477
989
|
function parsePrUrl2(prUrl) {
|
|
478
990
|
const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
@@ -480,7 +992,7 @@ function parsePrUrl2(prUrl) {
|
|
|
480
992
|
return { owner: match[1], repo: match[2], number: Number(match[3]) };
|
|
481
993
|
}
|
|
482
994
|
function makeGitHubRouter() {
|
|
483
|
-
const router =
|
|
995
|
+
const router = Router4();
|
|
484
996
|
router.get("/pr/status", async (req, res) => {
|
|
485
997
|
const prUrl = typeof req.query.prUrl === "string" ? req.query.prUrl : null;
|
|
486
998
|
if (!prUrl) {
|
|
@@ -526,6 +1038,21 @@ function makeGitHubRouter() {
|
|
|
526
1038
|
const user = req.user;
|
|
527
1039
|
const octokit = new Octokit({ auth: user.accessToken });
|
|
528
1040
|
try {
|
|
1041
|
+
const pr = await octokit.pulls.get({
|
|
1042
|
+
owner: parsed.owner,
|
|
1043
|
+
repo: parsed.repo,
|
|
1044
|
+
pull_number: parsed.number
|
|
1045
|
+
});
|
|
1046
|
+
if (pr.data.draft) {
|
|
1047
|
+
await octokit.graphql(
|
|
1048
|
+
`mutation($id: ID!) {
|
|
1049
|
+
markPullRequestReadyForReview(input: { pullRequestId: $id }) {
|
|
1050
|
+
pullRequest { id }
|
|
1051
|
+
}
|
|
1052
|
+
}`,
|
|
1053
|
+
{ id: pr.data.node_id }
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
529
1056
|
const merge = await octokit.pulls.merge({
|
|
530
1057
|
owner: parsed.owner,
|
|
531
1058
|
repo: parsed.repo,
|
|
@@ -535,34 +1062,35 @@ function makeGitHubRouter() {
|
|
|
535
1062
|
res.json({ sha: merge.data.sha, merged: merge.data.merged });
|
|
536
1063
|
} catch (err) {
|
|
537
1064
|
const message = err instanceof Error ? err.message : String(err);
|
|
1065
|
+
const status = err.status ?? 500;
|
|
538
1066
|
console.error("[coding-tab] /pr/merge failed", err);
|
|
539
|
-
res.status(500).json({ error: message });
|
|
1067
|
+
res.status(status >= 400 && status < 600 ? status : 500).json({ error: message });
|
|
540
1068
|
}
|
|
541
1069
|
});
|
|
542
1070
|
return router;
|
|
543
1071
|
}
|
|
544
1072
|
|
|
545
1073
|
// src/server/staticAssets.ts
|
|
546
|
-
import { Router as
|
|
547
|
-
import { readFile } from "fs/promises";
|
|
548
|
-
import { resolve, dirname } from "path";
|
|
1074
|
+
import { Router as Router5 } from "express";
|
|
1075
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1076
|
+
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
549
1077
|
import { fileURLToPath } from "url";
|
|
550
|
-
var here =
|
|
1078
|
+
var here = dirname2(fileURLToPath(import.meta.url));
|
|
551
1079
|
var ASSET_CANDIDATES = [
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
1080
|
+
resolve2(here, "browser.js"),
|
|
1081
|
+
resolve2(here, "..", "dist", "browser.js"),
|
|
1082
|
+
resolve2(here, "..", "browser.js")
|
|
555
1083
|
];
|
|
556
1084
|
var STYLE_CANDIDATES = [
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1085
|
+
resolve2(here, "style.css"),
|
|
1086
|
+
resolve2(here, "..", "dist", "style.css"),
|
|
1087
|
+
resolve2(here, "..", "style.css")
|
|
560
1088
|
];
|
|
561
1089
|
async function readFirst(paths) {
|
|
562
1090
|
let lastErr;
|
|
563
1091
|
for (const p of paths) {
|
|
564
1092
|
try {
|
|
565
|
-
const data = await
|
|
1093
|
+
const data = await readFile2(p);
|
|
566
1094
|
return { path: p, data };
|
|
567
1095
|
} catch (err) {
|
|
568
1096
|
lastErr = err;
|
|
@@ -571,12 +1099,12 @@ async function readFirst(paths) {
|
|
|
571
1099
|
throw lastErr instanceof Error ? lastErr : new Error("asset not found");
|
|
572
1100
|
}
|
|
573
1101
|
function makeAssetRouter(basePath) {
|
|
574
|
-
const router =
|
|
1102
|
+
const router = Router5();
|
|
575
1103
|
router.get("/browser.js", async (_req, res) => {
|
|
576
1104
|
try {
|
|
577
1105
|
const { data } = await readFirst(ASSET_CANDIDATES);
|
|
578
1106
|
res.set("Content-Type", "application/javascript; charset=utf-8");
|
|
579
|
-
res.set("Cache-Control", "public, max-age=
|
|
1107
|
+
res.set("Cache-Control", "public, max-age=0, must-revalidate");
|
|
580
1108
|
res.send(data);
|
|
581
1109
|
} catch (err) {
|
|
582
1110
|
res.status(500).send("// coding-tab browser bundle missing \u2014 did you run `npm run build`?");
|
|
@@ -586,7 +1114,7 @@ function makeAssetRouter(basePath) {
|
|
|
586
1114
|
try {
|
|
587
1115
|
const { data } = await readFirst(STYLE_CANDIDATES);
|
|
588
1116
|
res.set("Content-Type", "text/css; charset=utf-8");
|
|
589
|
-
res.set("Cache-Control", "public, max-age=
|
|
1117
|
+
res.set("Cache-Control", "public, max-age=0, must-revalidate");
|
|
590
1118
|
res.send(data);
|
|
591
1119
|
} catch {
|
|
592
1120
|
res.status(404).send("/* style.css missing */");
|
|
@@ -638,6 +1166,7 @@ function mountCodingTab(app, options) {
|
|
|
638
1166
|
if (!options.githubOAuth.allowedLogins || options.githubOAuth.allowedLogins.length === 0) {
|
|
639
1167
|
console.warn("[coding-tab] WARNING: allowedLogins is empty \u2014 anyone with a GitHub account can sign in.");
|
|
640
1168
|
}
|
|
1169
|
+
const storage = options.storage ?? createDefaultStorage(options.dataDir);
|
|
641
1170
|
const router = express.Router();
|
|
642
1171
|
router.use(express.json({ limit: "1mb" }));
|
|
643
1172
|
const assetRouter = makeAssetRouter(basePath);
|
|
@@ -656,11 +1185,14 @@ function mountCodingTab(app, options) {
|
|
|
656
1185
|
secure,
|
|
657
1186
|
allowedLogins: options.githubOAuth.allowedLogins
|
|
658
1187
|
});
|
|
1188
|
+
const chatRouter = makeChatRouter({ storage, defaultRepo: options.defaultRepo });
|
|
1189
|
+
router.use(requireAuth, chatRouter);
|
|
659
1190
|
const agentRouter = makeAgentRouter({
|
|
660
1191
|
cursorApiKey: options.cursorApiKey,
|
|
661
1192
|
defaultRepo: options.defaultRepo,
|
|
662
1193
|
envName: options.envName,
|
|
663
|
-
skipReviewerRequest: options.skipReviewerRequest
|
|
1194
|
+
skipReviewerRequest: options.skipReviewerRequest,
|
|
1195
|
+
storage
|
|
664
1196
|
});
|
|
665
1197
|
router.use(requireAuth, agentRouter);
|
|
666
1198
|
const githubRouter = makeGitHubRouter();
|
|
@@ -670,6 +1202,8 @@ function mountCodingTab(app, options) {
|
|
|
670
1202
|
return router;
|
|
671
1203
|
}
|
|
672
1204
|
export {
|
|
1205
|
+
FileChatStorage,
|
|
1206
|
+
createDefaultStorage,
|
|
673
1207
|
mountCodingTab
|
|
674
1208
|
};
|
|
675
1209
|
//# sourceMappingURL=server.js.map
|