newpr 0.4.0 → 0.5.1
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/package.json +2 -2
- package/src/analyzer/pipeline.ts +42 -1
- package/src/config/store.ts +1 -0
- package/src/github/fetch-pr.ts +1 -0
- package/src/history/store.ts +25 -1
- package/src/llm/prompts.ts +37 -17
- package/src/llm/slides.ts +381 -0
- package/src/plugins/cartoon.ts +34 -0
- package/src/plugins/registry.ts +20 -0
- package/src/plugins/slides.ts +39 -0
- package/src/plugins/types.ts +33 -0
- package/src/types/github.ts +1 -0
- package/src/types/output.ts +26 -0
- package/src/web/client/App.tsx +7 -1
- package/src/web/client/components/AppShell.tsx +3 -1
- package/src/web/client/components/ChatSection.tsx +74 -15
- package/src/web/client/components/DetailPane.tsx +6 -4
- package/src/web/client/components/DiffViewer.tsx +241 -37
- package/src/web/client/components/Markdown.tsx +2 -2
- package/src/web/client/components/ResultsScreen.tsx +37 -3
- package/src/web/client/components/SettingsPanel.tsx +173 -21
- package/src/web/client/hooks/useBackgroundAnalyses.ts +17 -12
- package/src/web/client/hooks/useChatStore.ts +34 -31
- package/src/web/client/hooks/useFeatures.ts +8 -5
- package/src/web/client/hooks/useOutdatedCheck.ts +41 -0
- package/src/web/client/lib/notify.ts +21 -0
- package/src/web/client/lib/shiki.ts +29 -4
- package/src/web/client/panels/SlidesPanel.tsx +316 -0
- package/src/web/server/routes.ts +407 -5
- package/src/web/server.ts +30 -0
- package/src/web/styles/built.css +1 -1
package/src/web/server/routes.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import type { NewprConfig } from "../../types/config.ts";
|
|
2
2
|
import type { NewprOutput, ChatMessage, ChatToolCall, ChatSegment } from "../../types/output.ts";
|
|
3
3
|
import { DEFAULT_CONFIG } from "../../types/config.ts";
|
|
4
|
-
import { listSessions, loadSession, loadSinglePatch, savePatchesSidecar, loadCommentsSidecar, saveCommentsSidecar, loadChatSidecar, saveChatSidecar, loadPatchesSidecar, saveCartoonSidecar, loadCartoonSidecar } from "../../history/store.ts";
|
|
4
|
+
import { listSessions, loadSession, loadSinglePatch, savePatchesSidecar, loadCommentsSidecar, saveCommentsSidecar, loadChatSidecar, saveChatSidecar, loadPatchesSidecar, saveCartoonSidecar, loadCartoonSidecar, saveSlidesSidecar, loadSlidesSidecar } from "../../history/store.ts";
|
|
5
5
|
import type { DiffComment } from "../../types/output.ts";
|
|
6
6
|
import { fetchPrDiff } from "../../github/fetch-diff.ts";
|
|
7
7
|
import { fetchPrBody, fetchPrComments } from "../../github/fetch-pr.ts";
|
|
8
8
|
import { parseDiff } from "../../diff/parser.ts";
|
|
9
9
|
import { parsePrInput } from "../../github/parse-pr.ts";
|
|
10
|
-
import { writeStoredConfig, type StoredConfig } from "../../config/store.ts";
|
|
10
|
+
import { readStoredConfig, writeStoredConfig, type StoredConfig } from "../../config/store.ts";
|
|
11
11
|
import { startAnalysis, getSession, cancelAnalysis, subscribe, listActiveSessions } from "./session-manager.ts";
|
|
12
12
|
import { generateCartoon } from "../../llm/cartoon.ts";
|
|
13
|
+
import { generateSlides } from "../../llm/slides.ts";
|
|
14
|
+
import { getPlugin, getAllPlugins } from "../../plugins/registry.ts";
|
|
13
15
|
import { chatWithTools, type ChatTool, type ChatStreamEvent } from "../../llm/client.ts";
|
|
14
16
|
import { detectAgents, runAgent } from "../../workspace/agent.ts";
|
|
15
17
|
import { randomBytes } from "node:crypto";
|
|
@@ -70,6 +72,24 @@ export function createRoutes(token: string, config: NewprConfig, options: RouteO
|
|
|
70
72
|
return { login: "anonymous" };
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
interface SlideJob {
|
|
76
|
+
status: "running" | "done" | "error";
|
|
77
|
+
message: string;
|
|
78
|
+
current: number;
|
|
79
|
+
total: number;
|
|
80
|
+
plan?: { stylePrompt: string; slides: Array<{ index: number; title: string; contentPrompt: string }> };
|
|
81
|
+
imagePrompts?: Array<{ index: number; prompt: string }>;
|
|
82
|
+
}
|
|
83
|
+
const slideJobs = new Map<string, SlideJob>();
|
|
84
|
+
|
|
85
|
+
interface PluginJob {
|
|
86
|
+
status: "running" | "done" | "error";
|
|
87
|
+
message: string;
|
|
88
|
+
current: number;
|
|
89
|
+
total: number;
|
|
90
|
+
}
|
|
91
|
+
const pluginJobs = new Map<string, PluginJob>();
|
|
92
|
+
|
|
73
93
|
function buildChatSystemPrompt(data: NewprOutput): string {
|
|
74
94
|
const fileSummaries = data.files
|
|
75
95
|
.map((f) => `- ${f.path} (${f.status}, +${f.additions}/-${f.deletions}): ${f.summary}`)
|
|
@@ -218,12 +238,44 @@ $$
|
|
|
218
238
|
},
|
|
219
239
|
},
|
|
220
240
|
},
|
|
241
|
+
{
|
|
242
|
+
type: "function",
|
|
243
|
+
function: {
|
|
244
|
+
name: "create_review_comment",
|
|
245
|
+
description: "Create an inline review comment on a specific line or line range of a file in this PR. The comment will be posted to GitHub. Use this when the user asks to leave a comment, suggestion, or feedback on specific code.",
|
|
246
|
+
parameters: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
path: { type: "string", description: "File path (e.g. 'src/auth/session.ts')" },
|
|
250
|
+
line: { type: "number", description: "Line number to comment on (end line if range)" },
|
|
251
|
+
start_line: { type: "number", description: "Start line for multi-line comment (optional)" },
|
|
252
|
+
body: { type: "string", description: "Comment body in markdown" },
|
|
253
|
+
},
|
|
254
|
+
required: ["path", "line", "body"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
type: "function",
|
|
260
|
+
function: {
|
|
261
|
+
name: "submit_review",
|
|
262
|
+
description: "Submit a PR review with a verdict: APPROVE, REQUEST_CHANGES, or COMMENT. Use when the user asks to approve or request changes on the PR.",
|
|
263
|
+
parameters: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
event: { type: "string", enum: ["APPROVE", "REQUEST_CHANGES", "COMMENT"], description: "Review action" },
|
|
267
|
+
body: { type: "string", description: "Optional review summary message" },
|
|
268
|
+
},
|
|
269
|
+
required: ["event"],
|
|
270
|
+
},
|
|
271
|
+
},
|
|
272
|
+
},
|
|
221
273
|
];
|
|
222
274
|
}
|
|
223
275
|
|
|
224
276
|
return {
|
|
225
277
|
"POST /api/analysis": async (req: Request) => {
|
|
226
|
-
const body = await req.json() as { pr: string };
|
|
278
|
+
const body = await req.json() as { pr: string; reuseSessionId?: string };
|
|
227
279
|
if (!body.pr) return json({ error: "Missing 'pr' field" }, 400);
|
|
228
280
|
|
|
229
281
|
const result = startAnalysis(body.pr, token, config);
|
|
@@ -231,6 +283,7 @@ $$
|
|
|
231
283
|
|
|
232
284
|
return json({
|
|
233
285
|
sessionId: result.sessionId,
|
|
286
|
+
reuseSessionId: body.reuseSessionId,
|
|
234
287
|
eventsUrl: `/api/analysis/${result.sessionId}/events`,
|
|
235
288
|
});
|
|
236
289
|
},
|
|
@@ -471,7 +524,38 @@ $$
|
|
|
471
524
|
}
|
|
472
525
|
},
|
|
473
526
|
|
|
527
|
+
"GET /api/models": async () => {
|
|
528
|
+
if (!config.openrouter_api_key) return json([]);
|
|
529
|
+
try {
|
|
530
|
+
const res = await fetch("https://openrouter.ai/api/v1/models", {
|
|
531
|
+
headers: { Authorization: `Bearer ${config.openrouter_api_key}` },
|
|
532
|
+
});
|
|
533
|
+
if (!res.ok) return json([]);
|
|
534
|
+
const data = await res.json() as { data?: Array<{ id: string; name: string; created?: number; context_length?: number }> };
|
|
535
|
+
const models = (data.data ?? [])
|
|
536
|
+
.filter((m) => m.id && !m.id.includes(":free") && !m.id.includes(":extended"))
|
|
537
|
+
.map((m) => ({
|
|
538
|
+
id: m.id,
|
|
539
|
+
name: m.name ?? m.id,
|
|
540
|
+
provider: m.id.split("/")[0] ?? "",
|
|
541
|
+
created: m.created ?? 0,
|
|
542
|
+
contextLength: m.context_length,
|
|
543
|
+
}))
|
|
544
|
+
.sort((a, b) => {
|
|
545
|
+
const provCmp = a.provider.localeCompare(b.provider);
|
|
546
|
+
if (provCmp !== 0) return provCmp;
|
|
547
|
+
return b.created - a.created;
|
|
548
|
+
});
|
|
549
|
+
return json(models);
|
|
550
|
+
} catch {
|
|
551
|
+
return json([]);
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
|
|
474
555
|
"GET /api/config": async () => {
|
|
556
|
+
const stored = await readStoredConfig();
|
|
557
|
+
const pluginList = getAllPlugins().map((p) => ({ id: p.id, name: p.name }));
|
|
558
|
+
const enabledPlugins = stored.enabled_plugins ?? pluginList.map((p) => p.id);
|
|
475
559
|
return json({
|
|
476
560
|
model: config.model,
|
|
477
561
|
agent: config.agent ?? null,
|
|
@@ -481,6 +565,8 @@ $$
|
|
|
481
565
|
concurrency: config.concurrency,
|
|
482
566
|
has_api_key: !!config.openrouter_api_key,
|
|
483
567
|
has_github_token: !!token,
|
|
568
|
+
enabled_plugins: enabledPlugins,
|
|
569
|
+
available_plugins: pluginList,
|
|
484
570
|
defaults: {
|
|
485
571
|
model: DEFAULT_CONFIG.model,
|
|
486
572
|
language: DEFAULT_CONFIG.language,
|
|
@@ -528,14 +614,20 @@ $$
|
|
|
528
614
|
update.concurrency = body.concurrency;
|
|
529
615
|
config.concurrency = body.concurrency;
|
|
530
616
|
}
|
|
617
|
+
if ((body as Record<string, unknown>).enabled_plugins !== undefined) {
|
|
618
|
+
update.enabled_plugins = (body as Record<string, unknown>).enabled_plugins as string[];
|
|
619
|
+
}
|
|
531
620
|
|
|
532
621
|
await writeStoredConfig(update);
|
|
533
622
|
return json({ ok: true });
|
|
534
623
|
},
|
|
535
624
|
|
|
536
|
-
"GET /api/features": () => {
|
|
625
|
+
"GET /api/features": async () => {
|
|
537
626
|
const { getVersion } = require("../../version.ts");
|
|
538
|
-
|
|
627
|
+
const stored = await readStoredConfig();
|
|
628
|
+
const allPluginIds = getAllPlugins().map((p) => p.id);
|
|
629
|
+
const enabledPlugins = stored.enabled_plugins ?? allPluginIds;
|
|
630
|
+
return json({ cartoon: !!options.cartoon, version: getVersion(), enabledPlugins });
|
|
539
631
|
},
|
|
540
632
|
|
|
541
633
|
"POST /api/review": async (req: Request) => {
|
|
@@ -726,6 +818,107 @@ $$
|
|
|
726
818
|
return json({ ok: true });
|
|
727
819
|
},
|
|
728
820
|
|
|
821
|
+
"POST /api/sessions/:id/ask-inline": async (req: Request) => {
|
|
822
|
+
const url = new URL(req.url);
|
|
823
|
+
const segments = url.pathname.split("/");
|
|
824
|
+
const sessionId = segments[3]!;
|
|
825
|
+
|
|
826
|
+
if (!config.openrouter_api_key) {
|
|
827
|
+
return json({ error: "OpenRouter API key required" }, 400);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const body = await req.json() as { message: string };
|
|
831
|
+
if (!body.message?.trim()) return json({ error: "Missing message" }, 400);
|
|
832
|
+
|
|
833
|
+
const sessionData = await loadSession(sessionId);
|
|
834
|
+
if (!sessionData) return json({ error: "Session not found" }, 404);
|
|
835
|
+
|
|
836
|
+
const systemPrompt = buildChatSystemPrompt(sessionData);
|
|
837
|
+
const apiMessages = [
|
|
838
|
+
{ role: "system" as const, content: systemPrompt },
|
|
839
|
+
{ role: "user" as const, content: body.message.trim() },
|
|
840
|
+
];
|
|
841
|
+
|
|
842
|
+
const encoder = new TextEncoder();
|
|
843
|
+
const stream = new ReadableStream({
|
|
844
|
+
async start(controller) {
|
|
845
|
+
const send = (eventType: string, data: string) => {
|
|
846
|
+
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${data}\n\n`));
|
|
847
|
+
};
|
|
848
|
+
try {
|
|
849
|
+
await chatWithTools(
|
|
850
|
+
{ api_key: config.openrouter_api_key, model: config.model, timeout: config.timeout },
|
|
851
|
+
apiMessages as Parameters<typeof chatWithTools>[1],
|
|
852
|
+
buildChatTools(),
|
|
853
|
+
async (name: string, args: Record<string, unknown>): Promise<string> => {
|
|
854
|
+
if (name === "get_file_diff") {
|
|
855
|
+
const filePath = args.path as string;
|
|
856
|
+
if (!filePath) return "Error: path required";
|
|
857
|
+
const patches = await loadPatchesSidecar(sessionId);
|
|
858
|
+
if (patches?.[filePath]) return patches[filePath];
|
|
859
|
+
const patch = await loadSinglePatch(sessionId, filePath);
|
|
860
|
+
if (patch) return patch;
|
|
861
|
+
return `File "${filePath}" not found`;
|
|
862
|
+
}
|
|
863
|
+
if (name === "list_files") {
|
|
864
|
+
return sessionData.files.map((f) => `${f.path} (${f.status}): ${f.summary}`).join("\n");
|
|
865
|
+
}
|
|
866
|
+
return `Tool ${name} not available in inline mode`;
|
|
867
|
+
},
|
|
868
|
+
(event: ChatStreamEvent) => {
|
|
869
|
+
if (event.type === "text") send("text", JSON.stringify({ content: event.content }));
|
|
870
|
+
else if (event.type === "error") send("chat_error", JSON.stringify({ message: event.error }));
|
|
871
|
+
else if (event.type === "done") send("done", JSON.stringify({}));
|
|
872
|
+
},
|
|
873
|
+
);
|
|
874
|
+
send("done", JSON.stringify({}));
|
|
875
|
+
} catch (err) {
|
|
876
|
+
send("chat_error", JSON.stringify({ message: err instanceof Error ? err.message : String(err) }));
|
|
877
|
+
} finally {
|
|
878
|
+
controller.close();
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
return new Response(stream, {
|
|
884
|
+
headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" },
|
|
885
|
+
});
|
|
886
|
+
},
|
|
887
|
+
|
|
888
|
+
"GET /api/sessions/:id/outdated": async (req: Request) => {
|
|
889
|
+
const url = new URL(req.url);
|
|
890
|
+
const segments = url.pathname.split("/");
|
|
891
|
+
const id = segments[3]!;
|
|
892
|
+
const sessionData = await loadSession(id);
|
|
893
|
+
if (!sessionData) return json({ error: "Session not found" }, 404);
|
|
894
|
+
|
|
895
|
+
const prUrl = sessionData.meta.pr_url;
|
|
896
|
+
const analyzedUpdatedAt = sessionData.meta.pr_updated_at;
|
|
897
|
+
if (!analyzedUpdatedAt) return json({ outdated: false, reason: "no_baseline" });
|
|
898
|
+
|
|
899
|
+
try {
|
|
900
|
+
const pr = parsePrInput(prUrl);
|
|
901
|
+
const res = await fetch(
|
|
902
|
+
`https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}`,
|
|
903
|
+
{ headers: ghHeaders },
|
|
904
|
+
);
|
|
905
|
+
if (!res.ok) return json({ outdated: false, reason: "api_error" });
|
|
906
|
+
const data = await res.json() as { updated_at?: string; title?: string; state?: string; merged?: boolean; draft?: boolean };
|
|
907
|
+
const currentUpdatedAt = data.updated_at ?? "";
|
|
908
|
+
const outdated = currentUpdatedAt !== analyzedUpdatedAt;
|
|
909
|
+
return json({
|
|
910
|
+
outdated,
|
|
911
|
+
analyzed_at: sessionData.meta.analyzed_at,
|
|
912
|
+
analyzed_updated_at: analyzedUpdatedAt,
|
|
913
|
+
current_updated_at: currentUpdatedAt,
|
|
914
|
+
current_title: data.title,
|
|
915
|
+
current_state: data.draft ? "draft" : data.merged ? "merged" : data.state === "closed" ? "closed" : "open",
|
|
916
|
+
});
|
|
917
|
+
} catch {
|
|
918
|
+
return json({ outdated: false, reason: "fetch_error" });
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
|
|
729
922
|
"GET /api/sessions/:id/chat": async (req: Request) => {
|
|
730
923
|
const url = new URL(req.url);
|
|
731
924
|
const segments = url.pathname.split("/");
|
|
@@ -983,6 +1176,67 @@ $$
|
|
|
983
1176
|
return `Fetch error: ${err instanceof Error ? err.message : String(err)}`;
|
|
984
1177
|
}
|
|
985
1178
|
}
|
|
1179
|
+
case "create_review_comment": {
|
|
1180
|
+
const filePath = args.path as string;
|
|
1181
|
+
const line = args.line as number;
|
|
1182
|
+
const startLine = args.start_line as number | undefined;
|
|
1183
|
+
const body = args.body as string;
|
|
1184
|
+
if (!filePath || !line || !body) return "Error: path, line, and body are required";
|
|
1185
|
+
try {
|
|
1186
|
+
const pr = parsePrInput(sessionData.meta.pr_url);
|
|
1187
|
+
const sha = await fetchHeadSha(pr);
|
|
1188
|
+
if (!sha) return "Error: could not determine HEAD SHA";
|
|
1189
|
+
const ghBody: Record<string, unknown> = {
|
|
1190
|
+
commit_id: sha,
|
|
1191
|
+
path: filePath,
|
|
1192
|
+
line,
|
|
1193
|
+
side: "RIGHT",
|
|
1194
|
+
body,
|
|
1195
|
+
};
|
|
1196
|
+
if (startLine && startLine !== line) {
|
|
1197
|
+
ghBody.start_line = startLine;
|
|
1198
|
+
ghBody.start_side = "RIGHT";
|
|
1199
|
+
}
|
|
1200
|
+
const res = await fetch(
|
|
1201
|
+
`https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/comments`,
|
|
1202
|
+
{ method: "POST", headers: ghHeaders, body: JSON.stringify(ghBody) },
|
|
1203
|
+
);
|
|
1204
|
+
if (!res.ok) {
|
|
1205
|
+
const errBody = await res.text();
|
|
1206
|
+
return `GitHub API error ${res.status}: ${errBody.slice(0, 200)}`;
|
|
1207
|
+
}
|
|
1208
|
+
const data = await res.json() as { id?: number; html_url?: string };
|
|
1209
|
+
return `Comment created on ${filePath}:${startLine && startLine !== line ? `${startLine}-` : ""}${line}. ${data.html_url ?? ""}`;
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
case "submit_review": {
|
|
1215
|
+
const event = args.event as string;
|
|
1216
|
+
const body = (args.body as string) ?? "";
|
|
1217
|
+
if (!event || !["APPROVE", "REQUEST_CHANGES", "COMMENT"].includes(event)) {
|
|
1218
|
+
return "Error: event must be APPROVE, REQUEST_CHANGES, or COMMENT";
|
|
1219
|
+
}
|
|
1220
|
+
try {
|
|
1221
|
+
const pr = parsePrInput(sessionData.meta.pr_url);
|
|
1222
|
+
const res = await fetch(
|
|
1223
|
+
`https://api.github.com/repos/${pr.owner}/${pr.repo}/pulls/${pr.number}/reviews`,
|
|
1224
|
+
{
|
|
1225
|
+
method: "POST",
|
|
1226
|
+
headers: ghHeaders,
|
|
1227
|
+
body: JSON.stringify({ body, event }),
|
|
1228
|
+
},
|
|
1229
|
+
);
|
|
1230
|
+
if (!res.ok) {
|
|
1231
|
+
const errBody = await res.text();
|
|
1232
|
+
return `GitHub API error ${res.status}: ${errBody.slice(0, 200)}`;
|
|
1233
|
+
}
|
|
1234
|
+
const data = await res.json() as { html_url?: string; state?: string };
|
|
1235
|
+
return `Review submitted: ${data.state ?? event}. ${data.html_url ?? ""}`;
|
|
1236
|
+
} catch (err) {
|
|
1237
|
+
return `Error: ${err instanceof Error ? err.message : String(err)}`;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
986
1240
|
default:
|
|
987
1241
|
return `Unknown tool: ${name}`;
|
|
988
1242
|
}
|
|
@@ -1127,5 +1381,153 @@ $$
|
|
|
1127
1381
|
return json({ error: msg }, 500);
|
|
1128
1382
|
}
|
|
1129
1383
|
},
|
|
1384
|
+
"GET /api/sessions/:id/slides": async (req: Request) => {
|
|
1385
|
+
const url = new URL(req.url);
|
|
1386
|
+
const segments = url.pathname.split("/");
|
|
1387
|
+
const id = segments[3]!;
|
|
1388
|
+
const deck = await loadSlidesSidecar(id);
|
|
1389
|
+
if (!deck) return json(null);
|
|
1390
|
+
return json(deck);
|
|
1391
|
+
},
|
|
1392
|
+
|
|
1393
|
+
"POST /api/slides": async (req: Request) => {
|
|
1394
|
+
if (!config.openrouter_api_key) return json({ error: "OpenRouter API key required" }, 400);
|
|
1395
|
+
|
|
1396
|
+
const body = await req.json() as { sessionId?: string; language?: string; resume?: boolean };
|
|
1397
|
+
const sessionId = body.sessionId;
|
|
1398
|
+
if (!sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1399
|
+
|
|
1400
|
+
const data = await loadSession(sessionId);
|
|
1401
|
+
if (!data) return json({ error: "Session not found" }, 404);
|
|
1402
|
+
|
|
1403
|
+
if (slideJobs.has(sessionId) && slideJobs.get(sessionId)!.status === "running") {
|
|
1404
|
+
return json({ status: "already_running" });
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
const existingDeck = body.resume ? await loadSlidesSidecar(sessionId) : null;
|
|
1408
|
+
const job: SlideJob = { status: "running", message: "Planning slide deck...", current: 0, total: 0 };
|
|
1409
|
+
slideJobs.set(sessionId, job);
|
|
1410
|
+
|
|
1411
|
+
(async () => {
|
|
1412
|
+
try {
|
|
1413
|
+
const deck = await generateSlides(
|
|
1414
|
+
config.openrouter_api_key,
|
|
1415
|
+
data,
|
|
1416
|
+
config.model,
|
|
1417
|
+
body.language ?? config.language,
|
|
1418
|
+
(msg, current, total) => {
|
|
1419
|
+
job.message = msg;
|
|
1420
|
+
job.current = current;
|
|
1421
|
+
job.total = total;
|
|
1422
|
+
},
|
|
1423
|
+
existingDeck,
|
|
1424
|
+
(plan, prompts) => {
|
|
1425
|
+
job.plan = plan;
|
|
1426
|
+
job.imagePrompts = prompts;
|
|
1427
|
+
},
|
|
1428
|
+
(partialDeck) => {
|
|
1429
|
+
saveSlidesSidecar(sessionId, partialDeck).catch(() => {});
|
|
1430
|
+
},
|
|
1431
|
+
);
|
|
1432
|
+
await saveSlidesSidecar(sessionId, deck);
|
|
1433
|
+
job.status = "done";
|
|
1434
|
+
job.message = `Generated ${deck.slides.length} slides`;
|
|
1435
|
+
job.total = deck.slides.length;
|
|
1436
|
+
job.current = deck.slides.length;
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
job.status = "error";
|
|
1439
|
+
job.message = err instanceof Error ? err.message : String(err);
|
|
1440
|
+
}
|
|
1441
|
+
})();
|
|
1442
|
+
|
|
1443
|
+
return json({ status: "started" });
|
|
1444
|
+
},
|
|
1445
|
+
|
|
1446
|
+
"GET /api/slides/status": async (req: Request) => {
|
|
1447
|
+
const url = new URL(req.url);
|
|
1448
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
1449
|
+
if (!sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1450
|
+
const job = slideJobs.get(sessionId);
|
|
1451
|
+
if (!job) return json({ status: "idle" });
|
|
1452
|
+
return json(job);
|
|
1453
|
+
},
|
|
1454
|
+
|
|
1455
|
+
"GET /api/plugins": () => {
|
|
1456
|
+
const plugins = getAllPlugins().map((p) => ({
|
|
1457
|
+
id: p.id,
|
|
1458
|
+
name: p.name,
|
|
1459
|
+
description: p.description,
|
|
1460
|
+
icon: p.icon,
|
|
1461
|
+
tabLabel: p.tabLabel,
|
|
1462
|
+
}));
|
|
1463
|
+
return json(plugins);
|
|
1464
|
+
},
|
|
1465
|
+
|
|
1466
|
+
"GET /api/plugins/:id/data": async (req: Request) => {
|
|
1467
|
+
const url = new URL(req.url);
|
|
1468
|
+
const segments = url.pathname.split("/");
|
|
1469
|
+
const pluginId = segments[3]!;
|
|
1470
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
1471
|
+
if (!sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1472
|
+
const plugin = getPlugin(pluginId);
|
|
1473
|
+
if (!plugin) return json({ error: `Unknown plugin: ${pluginId}` }, 404);
|
|
1474
|
+
const data = await plugin.load(sessionId);
|
|
1475
|
+
return json(data);
|
|
1476
|
+
},
|
|
1477
|
+
|
|
1478
|
+
"POST /api/plugins/:id/generate": async (req: Request) => {
|
|
1479
|
+
const url = new URL(req.url);
|
|
1480
|
+
const segments = url.pathname.split("/");
|
|
1481
|
+
const pluginId = segments[3]!;
|
|
1482
|
+
const body = await req.json() as { sessionId?: string; resume?: boolean };
|
|
1483
|
+
const sessionId = body.sessionId;
|
|
1484
|
+
if (!sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1485
|
+
if (!config.openrouter_api_key) return json({ error: "API key required" }, 400);
|
|
1486
|
+
|
|
1487
|
+
const plugin = getPlugin(pluginId);
|
|
1488
|
+
if (!plugin) return json({ error: `Unknown plugin: ${pluginId}` }, 404);
|
|
1489
|
+
|
|
1490
|
+
const data = await loadSession(sessionId);
|
|
1491
|
+
if (!data) return json({ error: "Session not found" }, 404);
|
|
1492
|
+
|
|
1493
|
+
const jobKey = `${pluginId}:${sessionId}`;
|
|
1494
|
+
if (pluginJobs.has(jobKey) && pluginJobs.get(jobKey)!.status === "running") {
|
|
1495
|
+
return json({ status: "already_running" });
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
const job: PluginJob = { status: "running", message: "Starting...", current: 0, total: 0 };
|
|
1499
|
+
pluginJobs.set(jobKey, job);
|
|
1500
|
+
|
|
1501
|
+
const existingData = body.resume ? await plugin.load(sessionId) : null;
|
|
1502
|
+
|
|
1503
|
+
(async () => {
|
|
1504
|
+
try {
|
|
1505
|
+
const result = await plugin.generate(
|
|
1506
|
+
{ apiKey: config.openrouter_api_key, sessionId, data, language: config.language },
|
|
1507
|
+
(event) => { job.message = event.message; job.current = event.current; job.total = event.total; },
|
|
1508
|
+
existingData,
|
|
1509
|
+
);
|
|
1510
|
+
await plugin.save(sessionId, result.data);
|
|
1511
|
+
job.status = "done";
|
|
1512
|
+
job.message = "Complete";
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
job.status = "error";
|
|
1515
|
+
job.message = err instanceof Error ? err.message : String(err);
|
|
1516
|
+
}
|
|
1517
|
+
})();
|
|
1518
|
+
|
|
1519
|
+
return json({ status: "started" });
|
|
1520
|
+
},
|
|
1521
|
+
|
|
1522
|
+
"GET /api/plugins/:id/status": async (req: Request) => {
|
|
1523
|
+
const url = new URL(req.url);
|
|
1524
|
+
const segments = url.pathname.split("/");
|
|
1525
|
+
const pluginId = segments[3]!;
|
|
1526
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
1527
|
+
if (!sessionId) return json({ error: "Missing sessionId" }, 400);
|
|
1528
|
+
const job = pluginJobs.get(`${pluginId}:${sessionId}`);
|
|
1529
|
+
if (!job) return json({ status: "idle" });
|
|
1530
|
+
return json(job);
|
|
1531
|
+
},
|
|
1130
1532
|
};
|
|
1131
1533
|
}
|
package/src/web/server.ts
CHANGED
|
@@ -70,6 +70,9 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
|
70
70
|
GET: routes["GET /api/config"],
|
|
71
71
|
PUT: routes["PUT /api/config"],
|
|
72
72
|
},
|
|
73
|
+
"/api/models": {
|
|
74
|
+
GET: routes["GET /api/models"],
|
|
75
|
+
},
|
|
73
76
|
},
|
|
74
77
|
fetch(req) {
|
|
75
78
|
const url = new URL(req.url);
|
|
@@ -102,6 +105,12 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
|
102
105
|
if (path.match(/^\/api\/sessions\/[^/]+\/comments$/) && req.method === "POST") {
|
|
103
106
|
return routes["POST /api/sessions/:id/comments"](req);
|
|
104
107
|
}
|
|
108
|
+
if (path.match(/^\/api\/sessions\/[^/]+\/ask-inline$/) && req.method === "POST") {
|
|
109
|
+
return routes["POST /api/sessions/:id/ask-inline"](req);
|
|
110
|
+
}
|
|
111
|
+
if (path.match(/^\/api\/sessions\/[^/]+\/outdated$/) && req.method === "GET") {
|
|
112
|
+
return routes["GET /api/sessions/:id/outdated"](req);
|
|
113
|
+
}
|
|
105
114
|
if (path.match(/^\/api\/sessions\/[^/]+\/chat\/undo$/) && req.method === "POST") {
|
|
106
115
|
return routes["POST /api/sessions/:id/chat/undo"](req);
|
|
107
116
|
}
|
|
@@ -132,6 +141,27 @@ export async function startWebServer(options: WebServerOptions): Promise<void> {
|
|
|
132
141
|
if (path === "/api/cartoon" && req.method === "POST") {
|
|
133
142
|
return routes["POST /api/cartoon"](req);
|
|
134
143
|
}
|
|
144
|
+
if (path.match(/^\/api\/sessions\/[^/]+\/slides$/) && req.method === "GET") {
|
|
145
|
+
return routes["GET /api/sessions/:id/slides"](req);
|
|
146
|
+
}
|
|
147
|
+
if (path === "/api/slides" && req.method === "POST") {
|
|
148
|
+
return routes["POST /api/slides"](req);
|
|
149
|
+
}
|
|
150
|
+
if (path === "/api/slides/status" && req.method === "GET") {
|
|
151
|
+
return routes["GET /api/slides/status"](req);
|
|
152
|
+
}
|
|
153
|
+
if (path === "/api/plugins" && req.method === "GET") {
|
|
154
|
+
return routes["GET /api/plugins"]();
|
|
155
|
+
}
|
|
156
|
+
if (path.match(/^\/api\/plugins\/[^/]+\/data$/) && req.method === "GET") {
|
|
157
|
+
return routes["GET /api/plugins/:id/data"](req);
|
|
158
|
+
}
|
|
159
|
+
if (path.match(/^\/api\/plugins\/[^/]+\/generate$/) && req.method === "POST") {
|
|
160
|
+
return routes["POST /api/plugins/:id/generate"](req);
|
|
161
|
+
}
|
|
162
|
+
if (path.match(/^\/api\/plugins\/[^/]+\/status$/) && req.method === "GET") {
|
|
163
|
+
return routes["GET /api/plugins/:id/status"](req);
|
|
164
|
+
}
|
|
135
165
|
if (path === "/api/review" && req.method === "POST") {
|
|
136
166
|
return routes["POST /api/review"](req);
|
|
137
167
|
}
|