@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.cjs
CHANGED
|
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/server/index.ts
|
|
31
31
|
var server_exports = {};
|
|
32
32
|
__export(server_exports, {
|
|
33
|
+
FileChatStorage: () => FileChatStorage,
|
|
34
|
+
createDefaultStorage: () => createDefaultStorage,
|
|
33
35
|
mountCodingTab: () => mountCodingTab
|
|
34
36
|
});
|
|
35
37
|
module.exports = __toCommonJS(server_exports);
|
|
@@ -39,7 +41,7 @@ var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${_
|
|
|
39
41
|
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
40
42
|
|
|
41
43
|
// src/server/index.ts
|
|
42
|
-
var
|
|
44
|
+
var import_express6 = __toESM(require("express"), 1);
|
|
43
45
|
|
|
44
46
|
// src/server/authRoutes.ts
|
|
45
47
|
var import_express = require("express");
|
|
@@ -203,6 +205,7 @@ function makeAuthRouter(opts) {
|
|
|
203
205
|
// src/server/agentRoutes.ts
|
|
204
206
|
var import_express2 = require("express");
|
|
205
207
|
var import_sdk2 = require("@cursor/sdk");
|
|
208
|
+
var import_node_crypto3 = require("crypto");
|
|
206
209
|
|
|
207
210
|
// src/server/models.ts
|
|
208
211
|
var import_sdk = require("@cursor/sdk");
|
|
@@ -243,7 +246,6 @@ async function resolveModel(apiKey, choice) {
|
|
|
243
246
|
}
|
|
244
247
|
|
|
245
248
|
// src/server/sessions.ts
|
|
246
|
-
var import_node_crypto2 = require("crypto");
|
|
247
249
|
var SESSION_IDLE_TTL_MS = 30 * 60 * 1e3;
|
|
248
250
|
var sessions = /* @__PURE__ */ new Map();
|
|
249
251
|
var sweeperStarted = false;
|
|
@@ -254,35 +256,41 @@ function startSweeper() {
|
|
|
254
256
|
const now = Date.now();
|
|
255
257
|
for (const [id, s] of sessions) {
|
|
256
258
|
if (now - s.lastUsedAt > SESSION_IDLE_TTL_MS) {
|
|
257
|
-
|
|
259
|
+
disposeSessionsForChat(id).catch(
|
|
260
|
+
(e) => console.error("[coding-tab] session sweep dispose failed", e)
|
|
261
|
+
);
|
|
258
262
|
}
|
|
259
263
|
}
|
|
260
264
|
}, 5 * 60 * 1e3).unref?.();
|
|
261
265
|
}
|
|
262
|
-
function
|
|
266
|
+
function registerLiveSession(opts) {
|
|
263
267
|
startSweeper();
|
|
264
|
-
const
|
|
268
|
+
const prev = sessions.get(opts.chatId);
|
|
269
|
+
if (prev) {
|
|
270
|
+
sessions.delete(opts.chatId);
|
|
271
|
+
prev.agent[Symbol.asyncDispose]().catch(() => {
|
|
272
|
+
});
|
|
273
|
+
}
|
|
265
274
|
const session = {
|
|
266
|
-
|
|
275
|
+
chatId: opts.chatId,
|
|
267
276
|
githubLogin: opts.githubLogin,
|
|
268
277
|
agent: opts.agent,
|
|
269
|
-
|
|
270
|
-
startingRef: opts.startingRef,
|
|
278
|
+
agentId: opts.agentId,
|
|
271
279
|
createdAt: Date.now(),
|
|
272
280
|
lastUsedAt: Date.now()
|
|
273
281
|
};
|
|
274
|
-
sessions.set(
|
|
282
|
+
sessions.set(opts.chatId, session);
|
|
275
283
|
return session;
|
|
276
284
|
}
|
|
277
|
-
function
|
|
278
|
-
const s = sessions.get(
|
|
285
|
+
function getLiveSession(chatId) {
|
|
286
|
+
const s = sessions.get(chatId);
|
|
279
287
|
if (s) s.lastUsedAt = Date.now();
|
|
280
288
|
return s;
|
|
281
289
|
}
|
|
282
|
-
async function
|
|
283
|
-
const s = sessions.get(
|
|
290
|
+
async function disposeSessionsForChat(chatId) {
|
|
291
|
+
const s = sessions.get(chatId);
|
|
284
292
|
if (!s) return;
|
|
285
|
-
sessions.delete(
|
|
293
|
+
sessions.delete(chatId);
|
|
286
294
|
try {
|
|
287
295
|
await s.agent[Symbol.asyncDispose]();
|
|
288
296
|
} catch (err) {
|
|
@@ -290,6 +298,235 @@ async function disposeSession(id) {
|
|
|
290
298
|
}
|
|
291
299
|
}
|
|
292
300
|
|
|
301
|
+
// src/server/storage.ts
|
|
302
|
+
var import_promises = require("fs/promises");
|
|
303
|
+
var import_node_path = require("path");
|
|
304
|
+
var import_node_crypto2 = require("crypto");
|
|
305
|
+
function safeId(s) {
|
|
306
|
+
return s.replace(/[^a-zA-Z0-9_.-]/g, "_");
|
|
307
|
+
}
|
|
308
|
+
async function ensureDir(p) {
|
|
309
|
+
await (0, import_promises.mkdir)(p, { recursive: true });
|
|
310
|
+
}
|
|
311
|
+
async function readJson(path) {
|
|
312
|
+
try {
|
|
313
|
+
const raw = await (0, import_promises.readFile)(path, "utf8");
|
|
314
|
+
return JSON.parse(raw);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
if (err.code === "ENOENT") return null;
|
|
317
|
+
throw err;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
async function writeJsonAtomic(path, data) {
|
|
321
|
+
await ensureDir((0, import_node_path.dirname)(path));
|
|
322
|
+
const tmp = `${path}.${(0, import_node_crypto2.randomUUID)()}.tmp`;
|
|
323
|
+
await (0, import_promises.writeFile)(tmp, JSON.stringify(data, null, 2), "utf8");
|
|
324
|
+
await (0, import_promises.rename)(tmp, path);
|
|
325
|
+
}
|
|
326
|
+
var MAX_BLOB_BYTES = 32 * 1024;
|
|
327
|
+
function truncateBlob(value) {
|
|
328
|
+
if (value === void 0 || value === null) return value;
|
|
329
|
+
try {
|
|
330
|
+
const str = typeof value === "string" ? value : JSON.stringify(value);
|
|
331
|
+
if (str.length <= MAX_BLOB_BYTES) return value;
|
|
332
|
+
const head = str.slice(0, MAX_BLOB_BYTES);
|
|
333
|
+
return `${head}
|
|
334
|
+
|
|
335
|
+
\u2026[truncated ${str.length - MAX_BLOB_BYTES} chars]`;
|
|
336
|
+
} catch {
|
|
337
|
+
return "[unserializable]";
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function sanitizeEvent(evt) {
|
|
341
|
+
if (evt.kind === "tool") {
|
|
342
|
+
return {
|
|
343
|
+
...evt,
|
|
344
|
+
args: truncateBlob(evt.args),
|
|
345
|
+
result: truncateBlob(evt.result)
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return evt;
|
|
349
|
+
}
|
|
350
|
+
var FileChatStorage = class {
|
|
351
|
+
dataDir;
|
|
352
|
+
/** Per-chat write mutex chain to keep concurrent appends consistent. */
|
|
353
|
+
chains = /* @__PURE__ */ new Map();
|
|
354
|
+
constructor(opts) {
|
|
355
|
+
this.dataDir = (0, import_node_path.resolve)(opts.dataDir);
|
|
356
|
+
}
|
|
357
|
+
chatPath(id) {
|
|
358
|
+
return (0, import_node_path.join)(this.dataDir, "chats", `${safeId(id)}.json`);
|
|
359
|
+
}
|
|
360
|
+
indexPath(login) {
|
|
361
|
+
return (0, import_node_path.join)(this.dataDir, "index", `${safeId(login.toLowerCase())}.json`);
|
|
362
|
+
}
|
|
363
|
+
/** Serialize all reads/writes for a single chat through a chained promise. */
|
|
364
|
+
withChat(id, fn) {
|
|
365
|
+
const prev = this.chains.get(id) ?? Promise.resolve();
|
|
366
|
+
const next = prev.then(fn, fn);
|
|
367
|
+
this.chains.set(
|
|
368
|
+
id,
|
|
369
|
+
next.finally(() => {
|
|
370
|
+
if (this.chains.get(id) === next) this.chains.delete(id);
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
return next;
|
|
374
|
+
}
|
|
375
|
+
async readChat(id) {
|
|
376
|
+
return readJson(this.chatPath(id));
|
|
377
|
+
}
|
|
378
|
+
async writeChat(file) {
|
|
379
|
+
await writeJsonAtomic(this.chatPath(file.chat.id), file);
|
|
380
|
+
}
|
|
381
|
+
async readIndex(login) {
|
|
382
|
+
const list = await readJson(this.indexPath(login));
|
|
383
|
+
return list ?? [];
|
|
384
|
+
}
|
|
385
|
+
async writeIndex(login, items) {
|
|
386
|
+
items.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
387
|
+
await writeJsonAtomic(this.indexPath(login), items);
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Rebuild the index for a user by scanning every chat file. Used as a
|
|
391
|
+
* self-heal path when the index gets out of sync (e.g. crash mid-write).
|
|
392
|
+
*/
|
|
393
|
+
async rebuildIndex(login) {
|
|
394
|
+
const dir = (0, import_node_path.join)(this.dataDir, "chats");
|
|
395
|
+
let entries = [];
|
|
396
|
+
try {
|
|
397
|
+
entries = await (0, import_promises.readdir)(dir);
|
|
398
|
+
} catch (err) {
|
|
399
|
+
if (err.code !== "ENOENT") throw err;
|
|
400
|
+
}
|
|
401
|
+
const items = [];
|
|
402
|
+
const target = login.toLowerCase();
|
|
403
|
+
for (const name of entries) {
|
|
404
|
+
if (!name.endsWith(".json")) continue;
|
|
405
|
+
const file = await readJson((0, import_node_path.join)(dir, name));
|
|
406
|
+
if (!file?.chat) continue;
|
|
407
|
+
if (file.chat.githubLogin.toLowerCase() !== target) continue;
|
|
408
|
+
items.push(this.toListItem(file.chat));
|
|
409
|
+
}
|
|
410
|
+
await this.writeIndex(login, items);
|
|
411
|
+
return items;
|
|
412
|
+
}
|
|
413
|
+
toListItem(c) {
|
|
414
|
+
return {
|
|
415
|
+
id: c.id,
|
|
416
|
+
title: c.title,
|
|
417
|
+
mode: c.mode,
|
|
418
|
+
model: c.model,
|
|
419
|
+
createdAt: c.createdAt,
|
|
420
|
+
updatedAt: c.updatedAt
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
async upsertIndex(chat) {
|
|
424
|
+
const items = await this.readIndex(chat.githubLogin);
|
|
425
|
+
const idx = items.findIndex((c) => c.id === chat.id);
|
|
426
|
+
const item = this.toListItem(chat);
|
|
427
|
+
if (idx >= 0) items[idx] = item;
|
|
428
|
+
else items.push(item);
|
|
429
|
+
await this.writeIndex(chat.githubLogin, items);
|
|
430
|
+
}
|
|
431
|
+
async removeFromIndex(login, chatId) {
|
|
432
|
+
const items = await this.readIndex(login);
|
|
433
|
+
const filtered = items.filter((c) => c.id !== chatId);
|
|
434
|
+
if (filtered.length !== items.length) {
|
|
435
|
+
await this.writeIndex(login, filtered);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
async listChats(login) {
|
|
439
|
+
const items = await this.readIndex(login);
|
|
440
|
+
if (items.length > 0) return items;
|
|
441
|
+
return this.rebuildIndex(login);
|
|
442
|
+
}
|
|
443
|
+
async loadChat(id, login) {
|
|
444
|
+
return this.withChat(id, async () => {
|
|
445
|
+
const file = await this.readChat(id);
|
|
446
|
+
if (!file) return null;
|
|
447
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
const turns = [...file.turns].sort((a, b) => a.seq - b.seq);
|
|
451
|
+
return { ...file.chat, turns };
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async createChat(chat) {
|
|
455
|
+
await this.withChat(chat.id, async () => {
|
|
456
|
+
await this.writeChat({ chat, turns: [] });
|
|
457
|
+
});
|
|
458
|
+
await this.upsertIndex(chat);
|
|
459
|
+
}
|
|
460
|
+
async patchChat(id, login, patch) {
|
|
461
|
+
const updated = await this.withChat(id, async () => {
|
|
462
|
+
const file = await this.readChat(id);
|
|
463
|
+
if (!file) return null;
|
|
464
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
const next = {
|
|
468
|
+
...file.chat,
|
|
469
|
+
...patch,
|
|
470
|
+
updatedAt: Date.now()
|
|
471
|
+
};
|
|
472
|
+
await this.writeChat({ chat: next, turns: file.turns });
|
|
473
|
+
return next;
|
|
474
|
+
});
|
|
475
|
+
if (updated) await this.upsertIndex(updated);
|
|
476
|
+
return updated;
|
|
477
|
+
}
|
|
478
|
+
async appendTurn(turn) {
|
|
479
|
+
const sanitized = {
|
|
480
|
+
...turn,
|
|
481
|
+
events: turn.events.map(sanitizeEvent)
|
|
482
|
+
};
|
|
483
|
+
let chat = null;
|
|
484
|
+
await this.withChat(turn.chatId, async () => {
|
|
485
|
+
const file = await this.readChat(turn.chatId);
|
|
486
|
+
if (!file) throw new Error(`chat_not_found:${turn.chatId}`);
|
|
487
|
+
file.turns = file.turns.filter((t) => t.id !== sanitized.id);
|
|
488
|
+
file.turns.push(sanitized);
|
|
489
|
+
file.chat = { ...file.chat, updatedAt: Date.now() };
|
|
490
|
+
chat = file.chat;
|
|
491
|
+
await this.writeChat(file);
|
|
492
|
+
});
|
|
493
|
+
if (chat) await this.upsertIndex(chat);
|
|
494
|
+
}
|
|
495
|
+
async patchTurn(chatId, turnId, patch) {
|
|
496
|
+
let chat = null;
|
|
497
|
+
await this.withChat(chatId, async () => {
|
|
498
|
+
const file = await this.readChat(chatId);
|
|
499
|
+
if (!file) return;
|
|
500
|
+
const idx = file.turns.findIndex((t) => t.id === turnId);
|
|
501
|
+
if (idx < 0) return;
|
|
502
|
+
const current = file.turns[idx];
|
|
503
|
+
const events = patch.events ? patch.events.map(sanitizeEvent) : current.events;
|
|
504
|
+
file.turns[idx] = { ...current, ...patch, events };
|
|
505
|
+
file.chat = { ...file.chat, updatedAt: Date.now() };
|
|
506
|
+
chat = file.chat;
|
|
507
|
+
await this.writeChat(file);
|
|
508
|
+
});
|
|
509
|
+
if (chat) await this.upsertIndex(chat);
|
|
510
|
+
}
|
|
511
|
+
async deleteChat(id, login) {
|
|
512
|
+
const removed = await this.withChat(id, async () => {
|
|
513
|
+
const file = await this.readChat(id);
|
|
514
|
+
if (!file) return false;
|
|
515
|
+
if (file.chat.githubLogin.toLowerCase() !== login.toLowerCase()) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
await (0, import_promises.rm)(this.chatPath(id), { force: true });
|
|
519
|
+
return true;
|
|
520
|
+
});
|
|
521
|
+
if (removed) await this.removeFromIndex(login, id);
|
|
522
|
+
return removed;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
function createDefaultStorage(dataDir) {
|
|
526
|
+
const dir = dataDir ?? (0, import_node_path.join)(process.cwd(), ".coding-tab-data");
|
|
527
|
+
return new FileChatStorage({ dataDir: dir });
|
|
528
|
+
}
|
|
529
|
+
|
|
293
530
|
// src/server/agentRoutes.ts
|
|
294
531
|
var PLAN_INSTRUCTION = `You are operating in PLAN MODE.
|
|
295
532
|
|
|
@@ -331,39 +568,221 @@ function setupSseHeaders(res) {
|
|
|
331
568
|
});
|
|
332
569
|
res.flushHeaders?.();
|
|
333
570
|
}
|
|
334
|
-
|
|
571
|
+
function deriveTitle(prompt) {
|
|
572
|
+
const trimmed = prompt.trim().replace(/\s+/g, " ");
|
|
573
|
+
if (trimmed.length <= 60) return trimmed || "New chat";
|
|
574
|
+
return `${trimmed.slice(0, 57)}\u2026`;
|
|
575
|
+
}
|
|
576
|
+
var TurnBuffer = class {
|
|
577
|
+
events = [];
|
|
578
|
+
lastWasText = false;
|
|
579
|
+
pushText(text) {
|
|
580
|
+
if (this.lastWasText) {
|
|
581
|
+
const last = this.events[this.events.length - 1];
|
|
582
|
+
if (last.kind === "text") {
|
|
583
|
+
last.text += text;
|
|
584
|
+
return last;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const evt = { kind: "text", id: (0, import_node_crypto3.randomUUID)(), text };
|
|
588
|
+
this.events.push(evt);
|
|
589
|
+
this.lastWasText = true;
|
|
590
|
+
return evt;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Mark the next text event as starting a new block (called when the SDK
|
|
594
|
+
* emits a non-streaming-delta boundary, e.g. a tool call or a new assistant
|
|
595
|
+
* message). The next `pushText` will start a fresh paragraph.
|
|
596
|
+
*/
|
|
597
|
+
endTextBlock() {
|
|
598
|
+
this.lastWasText = false;
|
|
599
|
+
}
|
|
600
|
+
upsertTool(args) {
|
|
601
|
+
this.endTextBlock();
|
|
602
|
+
const existing = this.events.find(
|
|
603
|
+
(e) => e.kind === "tool" && e.callId === args.callId
|
|
604
|
+
);
|
|
605
|
+
if (existing) {
|
|
606
|
+
existing.status = args.status;
|
|
607
|
+
if (args.args !== void 0) existing.args = args.args;
|
|
608
|
+
if (args.result !== void 0) existing.result = args.result;
|
|
609
|
+
return existing;
|
|
610
|
+
}
|
|
611
|
+
const evt = {
|
|
612
|
+
kind: "tool",
|
|
613
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
614
|
+
callId: args.callId,
|
|
615
|
+
name: args.name,
|
|
616
|
+
status: args.status,
|
|
617
|
+
args: args.args,
|
|
618
|
+
result: args.result
|
|
619
|
+
};
|
|
620
|
+
this.events.push(evt);
|
|
621
|
+
return evt;
|
|
622
|
+
}
|
|
623
|
+
snapshot() {
|
|
624
|
+
return this.events.map(sanitizeEvent);
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
async function streamRun(res, run, ctx) {
|
|
628
|
+
const flushDebounceMs = 750;
|
|
629
|
+
let flushPending = false;
|
|
630
|
+
let lastFlush = 0;
|
|
631
|
+
const persist = async (status, pr) => {
|
|
632
|
+
try {
|
|
633
|
+
await ctx.storage.patchTurn(ctx.chat.id, ctx.turn.id, {
|
|
634
|
+
events: ctx.buffer.snapshot(),
|
|
635
|
+
...status ? { status } : {},
|
|
636
|
+
...pr ? { pr } : {}
|
|
637
|
+
});
|
|
638
|
+
lastFlush = Date.now();
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error("[coding-tab] patchTurn failed", err);
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
const scheduleFlush = () => {
|
|
644
|
+
if (flushPending) return;
|
|
645
|
+
flushPending = true;
|
|
646
|
+
setTimeout(async () => {
|
|
647
|
+
flushPending = false;
|
|
648
|
+
const elapsed = Date.now() - lastFlush;
|
|
649
|
+
if (elapsed < flushDebounceMs) return;
|
|
650
|
+
await persist();
|
|
651
|
+
}, flushDebounceMs).unref?.();
|
|
652
|
+
};
|
|
653
|
+
let finalStatus = "finished";
|
|
654
|
+
let finalPr;
|
|
335
655
|
try {
|
|
336
656
|
for await (const message of run.stream()) {
|
|
337
657
|
if (res.writableEnded) break;
|
|
338
658
|
if (message.type === "assistant") {
|
|
659
|
+
ctx.buffer.endTextBlock();
|
|
339
660
|
for (const block of message.message.content) {
|
|
340
|
-
if (block.type === "text" && block.text)
|
|
341
|
-
|
|
661
|
+
if (block.type === "text" && block.text) {
|
|
662
|
+
ctx.buffer.pushText(block.text);
|
|
663
|
+
ctx.buffer.endTextBlock();
|
|
664
|
+
sse(res, { kind: "text", text: block.text });
|
|
665
|
+
} else if (block.type === "tool_use") {
|
|
666
|
+
ctx.buffer.upsertTool({
|
|
667
|
+
callId: block.id,
|
|
668
|
+
name: block.name,
|
|
669
|
+
status: "running",
|
|
670
|
+
args: block.input
|
|
671
|
+
});
|
|
672
|
+
sse(res, {
|
|
673
|
+
kind: "tool",
|
|
674
|
+
name: block.name,
|
|
675
|
+
status: "running",
|
|
676
|
+
callId: block.id,
|
|
677
|
+
args: block.input
|
|
678
|
+
});
|
|
679
|
+
}
|
|
342
680
|
}
|
|
343
681
|
} else if (message.type === "thinking") {
|
|
344
682
|
sse(res, { kind: "thinking", text: message.text });
|
|
345
683
|
} else if (message.type === "tool_call") {
|
|
346
|
-
|
|
684
|
+
ctx.buffer.upsertTool({
|
|
685
|
+
callId: message.call_id,
|
|
686
|
+
name: message.name,
|
|
687
|
+
status: message.status,
|
|
688
|
+
args: message.args,
|
|
689
|
+
result: message.result
|
|
690
|
+
});
|
|
691
|
+
sse(res, {
|
|
692
|
+
kind: "tool",
|
|
693
|
+
name: message.name,
|
|
694
|
+
status: message.status,
|
|
695
|
+
callId: message.call_id,
|
|
696
|
+
args: message.args,
|
|
697
|
+
result: message.result
|
|
698
|
+
});
|
|
347
699
|
} else if (message.type === "status") {
|
|
348
700
|
sse(res, { kind: "status", status: message.status, message: message.message });
|
|
349
701
|
}
|
|
702
|
+
scheduleFlush();
|
|
350
703
|
}
|
|
351
704
|
const result = await run.wait();
|
|
352
705
|
const prUrl = result.git?.branches?.find((b) => b.prUrl)?.prUrl;
|
|
706
|
+
finalPr = parsePrUrl(prUrl);
|
|
707
|
+
finalStatus = result.status ?? "finished";
|
|
353
708
|
sse(res, {
|
|
354
709
|
kind: "result",
|
|
355
710
|
status: result.status,
|
|
356
|
-
pr:
|
|
711
|
+
pr: finalPr,
|
|
357
712
|
durationMs: result.durationMs
|
|
358
713
|
});
|
|
359
714
|
} catch (err) {
|
|
715
|
+
finalStatus = "error";
|
|
360
716
|
const message = err instanceof Error ? err.message : String(err);
|
|
361
717
|
const retryable = err instanceof import_sdk2.CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
362
718
|
sse(res, { kind: "error", message, retryable });
|
|
363
719
|
} finally {
|
|
720
|
+
await persist(finalStatus, finalPr);
|
|
364
721
|
if (!res.writableEnded) res.end();
|
|
365
722
|
}
|
|
366
723
|
}
|
|
724
|
+
async function ensureAgentForChat(storage, chat, user, opts, modelChoice) {
|
|
725
|
+
const existing = getLiveSession(chat.id);
|
|
726
|
+
if (existing && existing.githubLogin.toLowerCase() === user.githubLogin.toLowerCase()) {
|
|
727
|
+
return { agent: existing.agent, agentId: existing.agentId, resumed: true };
|
|
728
|
+
}
|
|
729
|
+
const resolved = await resolveModel(opts.cursorApiKey, modelChoice);
|
|
730
|
+
const cloud = {
|
|
731
|
+
repos: [
|
|
732
|
+
{
|
|
733
|
+
url: chat.repoUrl,
|
|
734
|
+
startingRef: chat.startingRef
|
|
735
|
+
}
|
|
736
|
+
],
|
|
737
|
+
autoCreatePR: chat.mode === "agent",
|
|
738
|
+
skipReviewerRequest: opts.skipReviewerRequest ?? true,
|
|
739
|
+
envVars: { GITHUB_TOKEN: user.accessToken },
|
|
740
|
+
...opts.envName ? { env: { type: "cloud", name: opts.envName } } : {}
|
|
741
|
+
};
|
|
742
|
+
if (chat.agentId) {
|
|
743
|
+
try {
|
|
744
|
+
const resumed = await import_sdk2.Agent.resume(chat.agentId, {
|
|
745
|
+
apiKey: opts.cursorApiKey,
|
|
746
|
+
model: { id: resolved.cursorModelId },
|
|
747
|
+
cloud
|
|
748
|
+
});
|
|
749
|
+
registerLiveSession({
|
|
750
|
+
chatId: chat.id,
|
|
751
|
+
githubLogin: user.githubLogin,
|
|
752
|
+
agent: resumed,
|
|
753
|
+
agentId: resumed.agentId
|
|
754
|
+
});
|
|
755
|
+
if (resumed.agentId !== chat.agentId) {
|
|
756
|
+
await storage.patchChat(chat.id, user.githubLogin, { agentId: resumed.agentId });
|
|
757
|
+
}
|
|
758
|
+
return { agent: resumed, agentId: resumed.agentId, resumed: true };
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.warn(
|
|
761
|
+
`[coding-tab] resume failed for chat=${chat.id} agentId=${chat.agentId}; creating fresh agent`,
|
|
762
|
+
err instanceof Error ? err.message : err
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const fresh = await import_sdk2.Agent.create({
|
|
767
|
+
apiKey: opts.cursorApiKey,
|
|
768
|
+
model: { id: resolved.cursorModelId },
|
|
769
|
+
cloud
|
|
770
|
+
});
|
|
771
|
+
registerLiveSession({
|
|
772
|
+
chatId: chat.id,
|
|
773
|
+
githubLogin: user.githubLogin,
|
|
774
|
+
agent: fresh,
|
|
775
|
+
agentId: fresh.agentId
|
|
776
|
+
});
|
|
777
|
+
await storage.patchChat(chat.id, user.githubLogin, { agentId: fresh.agentId });
|
|
778
|
+
return { agent: fresh, agentId: fresh.agentId, resumed: false };
|
|
779
|
+
}
|
|
780
|
+
async function nextSeq(storage, chatId, login) {
|
|
781
|
+
const full = await storage.loadChat(chatId, login);
|
|
782
|
+
if (!full) throw Object.assign(new Error("chat_not_found"), { status: 404 });
|
|
783
|
+
const max = full.turns.reduce((acc, t) => Math.max(acc, t.seq), -1);
|
|
784
|
+
return max + 1;
|
|
785
|
+
}
|
|
367
786
|
function makeAgentRouter(opts) {
|
|
368
787
|
const router = (0, import_express2.Router)();
|
|
369
788
|
router.get("/models", async (_req, res) => {
|
|
@@ -375,144 +794,236 @@ function makeAgentRouter(opts) {
|
|
|
375
794
|
res.status(500).json({ error: err instanceof Error ? err.message : "models_failed" });
|
|
376
795
|
}
|
|
377
796
|
});
|
|
378
|
-
|
|
797
|
+
const handleSend = async (req, res, isExecute) => {
|
|
379
798
|
const user = req.user;
|
|
380
|
-
const
|
|
381
|
-
if (!
|
|
799
|
+
const body = req.body ?? {};
|
|
800
|
+
if (!body.chatId) {
|
|
801
|
+
res.status(400).json({ error: "missing_chatId" });
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (!isExecute && (!body.prompt || body.mode !== "plan" && body.mode !== "agent")) {
|
|
382
805
|
res.status(400).json({ error: "invalid_request" });
|
|
383
806
|
return;
|
|
384
807
|
}
|
|
808
|
+
const chat = await opts.storage.loadChat(body.chatId, user.githubLogin);
|
|
809
|
+
if (!chat) {
|
|
810
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
385
813
|
setupSseHeaders(res);
|
|
386
|
-
let
|
|
814
|
+
let assistantTurn = null;
|
|
387
815
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
816
|
+
if (!isExecute && body.mode && body.mode !== chat.mode) {
|
|
817
|
+
chat.mode = body.mode;
|
|
818
|
+
await opts.storage.patchChat(chat.id, user.githubLogin, { mode: body.mode });
|
|
819
|
+
}
|
|
820
|
+
let seq = await nextSeq(opts.storage, chat.id, user.githubLogin);
|
|
821
|
+
if (!isExecute) {
|
|
822
|
+
const userTurn = {
|
|
823
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
824
|
+
chatId: chat.id,
|
|
825
|
+
seq: seq++,
|
|
826
|
+
role: "user",
|
|
827
|
+
isPlan: chat.mode === "plan",
|
|
828
|
+
status: "finished",
|
|
829
|
+
events: [{ kind: "text", id: (0, import_node_crypto3.randomUUID)(), text: body.prompt }],
|
|
830
|
+
prompt: body.prompt,
|
|
831
|
+
createdAt: Date.now()
|
|
832
|
+
};
|
|
833
|
+
await opts.storage.appendTurn(userTurn);
|
|
834
|
+
if (chat.title === "New chat") {
|
|
835
|
+
const title = deriveTitle(body.prompt);
|
|
836
|
+
chat.title = title;
|
|
837
|
+
await opts.storage.patchChat(chat.id, user.githubLogin, { title });
|
|
403
838
|
}
|
|
839
|
+
}
|
|
840
|
+
assistantTurn = {
|
|
841
|
+
id: (0, import_node_crypto3.randomUUID)(),
|
|
842
|
+
chatId: chat.id,
|
|
843
|
+
seq,
|
|
844
|
+
role: "assistant",
|
|
845
|
+
isPlan: !isExecute && chat.mode === "plan",
|
|
846
|
+
status: "running",
|
|
847
|
+
events: [],
|
|
848
|
+
createdAt: Date.now()
|
|
849
|
+
};
|
|
850
|
+
await opts.storage.appendTurn(assistantTurn);
|
|
851
|
+
const { agent } = await ensureAgentForChat(opts.storage, chat, user, opts, chat.model);
|
|
852
|
+
const sendText = isExecute ? EXECUTE_INSTRUCTION : modeInstructionPrefix(body.mode, body.prompt);
|
|
853
|
+
const run = await agent.send(sendText);
|
|
854
|
+
sse(res, {
|
|
855
|
+
kind: "ready",
|
|
856
|
+
chatId: chat.id,
|
|
857
|
+
turnId: assistantTurn.id,
|
|
858
|
+
agentId: agent.agentId,
|
|
859
|
+
runId: run.id
|
|
404
860
|
});
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
861
|
+
console.log(
|
|
862
|
+
`[coding-tab] ${isExecute ? "execute" : "send"} agent=${agent.agentId} run=${run.id} chat=${chat.id} login=${user.githubLogin}`
|
|
863
|
+
);
|
|
864
|
+
const buffer = new TurnBuffer();
|
|
865
|
+
await streamRun(res, run, {
|
|
866
|
+
storage: opts.storage,
|
|
867
|
+
chat,
|
|
868
|
+
turn: assistantTurn,
|
|
869
|
+
buffer
|
|
410
870
|
});
|
|
411
|
-
const run = await agent.send(modeInstructionPrefix(mode, prompt));
|
|
412
|
-
sse(res, { kind: "ready", sessionId: session.id, agentId: agent.agentId, runId: run.id });
|
|
413
|
-
console.log(`[coding-tab] start agent=${agent.agentId} run=${run.id} session=${session.id} login=${user.githubLogin}`);
|
|
414
|
-
await streamRun(res, run);
|
|
415
871
|
} catch (err) {
|
|
416
872
|
const message = err instanceof Error ? err.message : String(err);
|
|
417
873
|
const retryable = err instanceof import_sdk2.CursorAgentError ? Boolean(err.isRetryable) : false;
|
|
418
|
-
console.error(
|
|
874
|
+
console.error(`[coding-tab] /agent/${isExecute ? "execute" : "send"} failed`, err);
|
|
419
875
|
sse(res, { kind: "error", message, retryable });
|
|
876
|
+
if (assistantTurn) {
|
|
877
|
+
await opts.storage.patchTurn(assistantTurn.chatId, assistantTurn.id, {
|
|
878
|
+
status: "error",
|
|
879
|
+
events: [
|
|
880
|
+
...assistantTurn.events,
|
|
881
|
+
{ kind: "text", id: (0, import_node_crypto3.randomUUID)(), text: `[error] ${message}` }
|
|
882
|
+
]
|
|
883
|
+
}).catch(() => {
|
|
884
|
+
});
|
|
885
|
+
}
|
|
420
886
|
if (!res.writableEnded) res.end();
|
|
421
|
-
if (agent) await agent[Symbol.asyncDispose]().catch(() => {
|
|
422
|
-
});
|
|
423
887
|
}
|
|
424
888
|
req.on("close", () => {
|
|
425
889
|
if (!res.writableEnded) res.end();
|
|
426
890
|
});
|
|
427
|
-
}
|
|
428
|
-
router.post("/agent/
|
|
429
|
-
|
|
430
|
-
|
|
891
|
+
};
|
|
892
|
+
router.post("/agent/start", (req, res) => handleSend(req, res, false));
|
|
893
|
+
router.post("/agent/send", (req, res) => handleSend(req, res, false));
|
|
894
|
+
router.post("/agent/execute", (req, res) => handleSend(req, res, true));
|
|
895
|
+
router.post("/agent/cancel", async (req, res) => {
|
|
896
|
+
const { chatId, runId } = req.body ?? {};
|
|
897
|
+
if (!chatId || !runId) {
|
|
431
898
|
res.status(400).json({ error: "invalid_request" });
|
|
432
899
|
return;
|
|
433
900
|
}
|
|
434
|
-
const
|
|
435
|
-
if (!
|
|
901
|
+
const live = getLiveSession(chatId);
|
|
902
|
+
if (!live) {
|
|
436
903
|
res.status(404).json({ error: "session_not_found" });
|
|
437
904
|
return;
|
|
438
905
|
}
|
|
439
|
-
setupSseHeaders(res);
|
|
440
906
|
try {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
907
|
+
await import_sdk2.Agent.cancelRun(runId, {
|
|
908
|
+
runtime: "cloud",
|
|
909
|
+
agentId: live.agentId,
|
|
910
|
+
apiKey: opts.cursorApiKey
|
|
911
|
+
});
|
|
912
|
+
res.json({ ok: true });
|
|
445
913
|
} catch (err) {
|
|
446
914
|
const message = err instanceof Error ? err.message : String(err);
|
|
447
|
-
console.error("[coding-tab] /agent/
|
|
448
|
-
|
|
449
|
-
if (!res.writableEnded) res.end();
|
|
915
|
+
console.error("[coding-tab] /agent/cancel failed", err);
|
|
916
|
+
res.status(500).json({ error: message });
|
|
450
917
|
}
|
|
451
|
-
req.on("close", () => {
|
|
452
|
-
if (!res.writableEnded) res.end();
|
|
453
|
-
});
|
|
454
918
|
});
|
|
455
|
-
router.post("/agent/
|
|
456
|
-
const {
|
|
457
|
-
if (!
|
|
919
|
+
router.post("/agent/dispose", async (req, res) => {
|
|
920
|
+
const { chatId } = req.body ?? {};
|
|
921
|
+
if (!chatId) {
|
|
458
922
|
res.status(400).json({ error: "invalid_request" });
|
|
459
923
|
return;
|
|
460
924
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
925
|
+
await disposeSessionsForChat(chatId);
|
|
926
|
+
res.json({ ok: true });
|
|
927
|
+
});
|
|
928
|
+
return router;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/server/chatRoutes.ts
|
|
932
|
+
var import_express3 = require("express");
|
|
933
|
+
var import_node_crypto4 = require("crypto");
|
|
934
|
+
var VALID_MODES = ["plan", "agent"];
|
|
935
|
+
var VALID_MODELS = ["sonnet", "opus"];
|
|
936
|
+
function makeChatRouter(opts) {
|
|
937
|
+
const router = (0, import_express3.Router)();
|
|
938
|
+
router.get("/chats", async (req, res) => {
|
|
939
|
+
const user = req.user;
|
|
467
940
|
try {
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
console.log(`[coding-tab] execute agent=${session.agent.agentId} run=${run.id} session=${session.id}`);
|
|
471
|
-
await streamRun(res, run);
|
|
941
|
+
const items = await opts.storage.listChats(user.githubLogin);
|
|
942
|
+
res.json({ chats: items });
|
|
472
943
|
} catch (err) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
sse(res, { kind: "error", message });
|
|
476
|
-
if (!res.writableEnded) res.end();
|
|
944
|
+
console.error("[coding-tab] /chats list failed", err);
|
|
945
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "list_failed" });
|
|
477
946
|
}
|
|
478
|
-
req.on("close", () => {
|
|
479
|
-
if (!res.writableEnded) res.end();
|
|
480
|
-
});
|
|
481
947
|
});
|
|
482
|
-
router.post("/
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
948
|
+
router.post("/chats", async (req, res) => {
|
|
949
|
+
const user = req.user;
|
|
950
|
+
const body = req.body ?? {};
|
|
951
|
+
const mode = VALID_MODES.includes(body.mode) ? body.mode : "plan";
|
|
952
|
+
const model = VALID_MODELS.includes(body.model) ? body.model : "sonnet";
|
|
953
|
+
const now = Date.now();
|
|
954
|
+
const chat = {
|
|
955
|
+
id: (0, import_node_crypto4.randomUUID)(),
|
|
956
|
+
githubLogin: user.githubLogin,
|
|
957
|
+
title: (body.title ?? "").trim() || "New chat",
|
|
958
|
+
mode,
|
|
959
|
+
model,
|
|
960
|
+
repoUrl: body.repoUrl?.trim() || opts.defaultRepo.url,
|
|
961
|
+
startingRef: body.startingRef ?? opts.defaultRepo.ref,
|
|
962
|
+
createdAt: now,
|
|
963
|
+
updatedAt: now
|
|
964
|
+
};
|
|
965
|
+
try {
|
|
966
|
+
await opts.storage.createChat(chat);
|
|
967
|
+
res.status(201).json({ chat });
|
|
968
|
+
} catch (err) {
|
|
969
|
+
console.error("[coding-tab] /chats create failed", err);
|
|
970
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "create_failed" });
|
|
487
971
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
972
|
+
});
|
|
973
|
+
router.get("/chats/:id", async (req, res) => {
|
|
974
|
+
const user = req.user;
|
|
975
|
+
try {
|
|
976
|
+
const full = await opts.storage.loadChat(req.params.id, user.githubLogin);
|
|
977
|
+
if (!full) {
|
|
978
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
res.json({ chat: full });
|
|
982
|
+
} catch (err) {
|
|
983
|
+
console.error("[coding-tab] /chats/:id load failed", err);
|
|
984
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "load_failed" });
|
|
492
985
|
}
|
|
986
|
+
});
|
|
987
|
+
router.patch("/chats/:id", async (req, res) => {
|
|
988
|
+
const user = req.user;
|
|
989
|
+
const body = req.body ?? {};
|
|
990
|
+
const patch = {};
|
|
991
|
+
if (typeof body.title === "string") patch.title = body.title.trim() || "Untitled";
|
|
992
|
+
if (body.mode && VALID_MODES.includes(body.mode)) patch.mode = body.mode;
|
|
993
|
+
if (body.model && VALID_MODELS.includes(body.model)) patch.model = body.model;
|
|
493
994
|
try {
|
|
494
|
-
await
|
|
495
|
-
|
|
995
|
+
const updated = await opts.storage.patchChat(req.params.id, user.githubLogin, patch);
|
|
996
|
+
if (!updated) {
|
|
997
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
res.json({ chat: updated });
|
|
496
1001
|
} catch (err) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
res.status(500).json({ error: message });
|
|
1002
|
+
console.error("[coding-tab] /chats/:id patch failed", err);
|
|
1003
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "patch_failed" });
|
|
500
1004
|
}
|
|
501
1005
|
});
|
|
502
|
-
router.
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1006
|
+
router.delete("/chats/:id", async (req, res) => {
|
|
1007
|
+
const user = req.user;
|
|
1008
|
+
try {
|
|
1009
|
+
const removed = await opts.storage.deleteChat(req.params.id, user.githubLogin);
|
|
1010
|
+
if (!removed) {
|
|
1011
|
+
res.status(404).json({ error: "chat_not_found" });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
await disposeSessionsForChat(req.params.id).catch(() => {
|
|
1015
|
+
});
|
|
1016
|
+
res.json({ ok: true });
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
console.error("[coding-tab] /chats/:id delete failed", err);
|
|
1019
|
+
res.status(500).json({ error: err instanceof Error ? err.message : "delete_failed" });
|
|
507
1020
|
}
|
|
508
|
-
await disposeSession(sessionId);
|
|
509
|
-
res.json({ ok: true });
|
|
510
1021
|
});
|
|
511
1022
|
return router;
|
|
512
1023
|
}
|
|
513
1024
|
|
|
514
1025
|
// src/server/githubRoutes.ts
|
|
515
|
-
var
|
|
1026
|
+
var import_express4 = require("express");
|
|
516
1027
|
var import_rest = require("@octokit/rest");
|
|
517
1028
|
function parsePrUrl2(prUrl) {
|
|
518
1029
|
const match = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
|
|
@@ -520,7 +1031,7 @@ function parsePrUrl2(prUrl) {
|
|
|
520
1031
|
return { owner: match[1], repo: match[2], number: Number(match[3]) };
|
|
521
1032
|
}
|
|
522
1033
|
function makeGitHubRouter() {
|
|
523
|
-
const router = (0,
|
|
1034
|
+
const router = (0, import_express4.Router)();
|
|
524
1035
|
router.get("/pr/status", async (req, res) => {
|
|
525
1036
|
const prUrl = typeof req.query.prUrl === "string" ? req.query.prUrl : null;
|
|
526
1037
|
if (!prUrl) {
|
|
@@ -566,6 +1077,21 @@ function makeGitHubRouter() {
|
|
|
566
1077
|
const user = req.user;
|
|
567
1078
|
const octokit = new import_rest.Octokit({ auth: user.accessToken });
|
|
568
1079
|
try {
|
|
1080
|
+
const pr = await octokit.pulls.get({
|
|
1081
|
+
owner: parsed.owner,
|
|
1082
|
+
repo: parsed.repo,
|
|
1083
|
+
pull_number: parsed.number
|
|
1084
|
+
});
|
|
1085
|
+
if (pr.data.draft) {
|
|
1086
|
+
await octokit.graphql(
|
|
1087
|
+
`mutation($id: ID!) {
|
|
1088
|
+
markPullRequestReadyForReview(input: { pullRequestId: $id }) {
|
|
1089
|
+
pullRequest { id }
|
|
1090
|
+
}
|
|
1091
|
+
}`,
|
|
1092
|
+
{ id: pr.data.node_id }
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
569
1095
|
const merge = await octokit.pulls.merge({
|
|
570
1096
|
owner: parsed.owner,
|
|
571
1097
|
repo: parsed.repo,
|
|
@@ -575,34 +1101,35 @@ function makeGitHubRouter() {
|
|
|
575
1101
|
res.json({ sha: merge.data.sha, merged: merge.data.merged });
|
|
576
1102
|
} catch (err) {
|
|
577
1103
|
const message = err instanceof Error ? err.message : String(err);
|
|
1104
|
+
const status = err.status ?? 500;
|
|
578
1105
|
console.error("[coding-tab] /pr/merge failed", err);
|
|
579
|
-
res.status(500).json({ error: message });
|
|
1106
|
+
res.status(status >= 400 && status < 600 ? status : 500).json({ error: message });
|
|
580
1107
|
}
|
|
581
1108
|
});
|
|
582
1109
|
return router;
|
|
583
1110
|
}
|
|
584
1111
|
|
|
585
1112
|
// src/server/staticAssets.ts
|
|
586
|
-
var
|
|
587
|
-
var
|
|
588
|
-
var
|
|
1113
|
+
var import_express5 = require("express");
|
|
1114
|
+
var import_promises2 = require("fs/promises");
|
|
1115
|
+
var import_node_path2 = require("path");
|
|
589
1116
|
var import_node_url = require("url");
|
|
590
|
-
var here = (0,
|
|
1117
|
+
var here = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
591
1118
|
var ASSET_CANDIDATES = [
|
|
592
|
-
(0,
|
|
593
|
-
(0,
|
|
594
|
-
(0,
|
|
1119
|
+
(0, import_node_path2.resolve)(here, "browser.js"),
|
|
1120
|
+
(0, import_node_path2.resolve)(here, "..", "dist", "browser.js"),
|
|
1121
|
+
(0, import_node_path2.resolve)(here, "..", "browser.js")
|
|
595
1122
|
];
|
|
596
1123
|
var STYLE_CANDIDATES = [
|
|
597
|
-
(0,
|
|
598
|
-
(0,
|
|
599
|
-
(0,
|
|
1124
|
+
(0, import_node_path2.resolve)(here, "style.css"),
|
|
1125
|
+
(0, import_node_path2.resolve)(here, "..", "dist", "style.css"),
|
|
1126
|
+
(0, import_node_path2.resolve)(here, "..", "style.css")
|
|
600
1127
|
];
|
|
601
1128
|
async function readFirst(paths) {
|
|
602
1129
|
let lastErr;
|
|
603
1130
|
for (const p of paths) {
|
|
604
1131
|
try {
|
|
605
|
-
const data = await (0,
|
|
1132
|
+
const data = await (0, import_promises2.readFile)(p);
|
|
606
1133
|
return { path: p, data };
|
|
607
1134
|
} catch (err) {
|
|
608
1135
|
lastErr = err;
|
|
@@ -611,12 +1138,12 @@ async function readFirst(paths) {
|
|
|
611
1138
|
throw lastErr instanceof Error ? lastErr : new Error("asset not found");
|
|
612
1139
|
}
|
|
613
1140
|
function makeAssetRouter(basePath) {
|
|
614
|
-
const router = (0,
|
|
1141
|
+
const router = (0, import_express5.Router)();
|
|
615
1142
|
router.get("/browser.js", async (_req, res) => {
|
|
616
1143
|
try {
|
|
617
1144
|
const { data } = await readFirst(ASSET_CANDIDATES);
|
|
618
1145
|
res.set("Content-Type", "application/javascript; charset=utf-8");
|
|
619
|
-
res.set("Cache-Control", "public, max-age=
|
|
1146
|
+
res.set("Cache-Control", "public, max-age=0, must-revalidate");
|
|
620
1147
|
res.send(data);
|
|
621
1148
|
} catch (err) {
|
|
622
1149
|
res.status(500).send("// coding-tab browser bundle missing \u2014 did you run `npm run build`?");
|
|
@@ -626,7 +1153,7 @@ function makeAssetRouter(basePath) {
|
|
|
626
1153
|
try {
|
|
627
1154
|
const { data } = await readFirst(STYLE_CANDIDATES);
|
|
628
1155
|
res.set("Content-Type", "text/css; charset=utf-8");
|
|
629
|
-
res.set("Cache-Control", "public, max-age=
|
|
1156
|
+
res.set("Cache-Control", "public, max-age=0, must-revalidate");
|
|
630
1157
|
res.send(data);
|
|
631
1158
|
} catch {
|
|
632
1159
|
res.status(404).send("/* style.css missing */");
|
|
@@ -678,8 +1205,9 @@ function mountCodingTab(app, options) {
|
|
|
678
1205
|
if (!options.githubOAuth.allowedLogins || options.githubOAuth.allowedLogins.length === 0) {
|
|
679
1206
|
console.warn("[coding-tab] WARNING: allowedLogins is empty \u2014 anyone with a GitHub account can sign in.");
|
|
680
1207
|
}
|
|
681
|
-
const
|
|
682
|
-
router.
|
|
1208
|
+
const storage = options.storage ?? createDefaultStorage(options.dataDir);
|
|
1209
|
+
const router = import_express6.default.Router();
|
|
1210
|
+
router.use(import_express6.default.json({ limit: "1mb" }));
|
|
683
1211
|
const assetRouter = makeAssetRouter(basePath);
|
|
684
1212
|
router.use(assetRouter);
|
|
685
1213
|
const authRouter = makeAuthRouter({
|
|
@@ -696,11 +1224,14 @@ function mountCodingTab(app, options) {
|
|
|
696
1224
|
secure,
|
|
697
1225
|
allowedLogins: options.githubOAuth.allowedLogins
|
|
698
1226
|
});
|
|
1227
|
+
const chatRouter = makeChatRouter({ storage, defaultRepo: options.defaultRepo });
|
|
1228
|
+
router.use(requireAuth, chatRouter);
|
|
699
1229
|
const agentRouter = makeAgentRouter({
|
|
700
1230
|
cursorApiKey: options.cursorApiKey,
|
|
701
1231
|
defaultRepo: options.defaultRepo,
|
|
702
1232
|
envName: options.envName,
|
|
703
|
-
skipReviewerRequest: options.skipReviewerRequest
|
|
1233
|
+
skipReviewerRequest: options.skipReviewerRequest,
|
|
1234
|
+
storage
|
|
704
1235
|
});
|
|
705
1236
|
router.use(requireAuth, agentRouter);
|
|
706
1237
|
const githubRouter = makeGitHubRouter();
|
|
@@ -711,6 +1242,8 @@ function mountCodingTab(app, options) {
|
|
|
711
1242
|
}
|
|
712
1243
|
// Annotate the CommonJS export names for ESM import in node:
|
|
713
1244
|
0 && (module.exports = {
|
|
1245
|
+
FileChatStorage,
|
|
1246
|
+
createDefaultStorage,
|
|
714
1247
|
mountCodingTab
|
|
715
1248
|
});
|
|
716
1249
|
//# sourceMappingURL=server.cjs.map
|