research-copilot 0.2.12 → 0.2.15
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/app/out/main/index.mjs +314 -53
- package/app/out/preload/index.js +7 -0
- package/app/out/renderer/assets/{MilkdownMarkdownEditor-BqfydyHs.js → MilkdownMarkdownEditor-UNNnHq-Q.js} +50 -50
- package/app/out/renderer/assets/{arc-B102x0uC.js → arc-CpQkTVSV.js} +1 -1
- package/app/out/renderer/assets/{blockDiagram-c4efeb88-hlEjcPtb.js → blockDiagram-c4efeb88-DV91zeYk.js} +8 -8
- package/app/out/renderer/assets/{c4Diagram-c83219d4-Dc1nfavC.js → c4Diagram-c83219d4-BYZtgyKD.js} +3 -3
- package/app/out/renderer/assets/{channel-ZMdB8bH1.js → channel-DKZcFGxg.js} +1 -1
- package/app/out/renderer/assets/{classDiagram-beda092f-Gbpax7vO.js → classDiagram-beda092f-bfcCUskQ.js} +6 -6
- package/app/out/renderer/assets/{classDiagram-v2-2358418a-_KwHXPjs.js → classDiagram-v2-2358418a-BR3pIwy5.js} +10 -10
- package/app/out/renderer/assets/{clone-DDahKPIj.js → clone-DYFbSe8n.js} +1 -1
- package/app/out/renderer/assets/{createText-1719965b-BNW0X7Oe.js → createText-1719965b-DzSrNkTO.js} +2 -2
- package/app/out/renderer/assets/{edges-96097737-G9l1oUoZ.js → edges-96097737-NtlJJRFS.js} +3 -3
- package/app/out/renderer/assets/{erDiagram-0228fc6a-BUs4XtQY.js → erDiagram-0228fc6a-D-IkKUzx.js} +5 -5
- package/app/out/renderer/assets/{flowDb-c6c81e3f-CQ-jcycx.js → flowDb-c6c81e3f-Ci22GdDY.js} +1 -1
- package/app/out/renderer/assets/{flowDiagram-50d868cf-uu8ab--w.js → flowDiagram-50d868cf-zRBNrGzH.js} +12 -12
- package/app/out/renderer/assets/{flowDiagram-v2-4f6560a1-CAw9BkoT.js → flowDiagram-v2-4f6560a1-vnbqAJeS.js} +12 -12
- package/app/out/renderer/assets/{flowchart-elk-definition-6af322e1-BjQ21EHx.js → flowchart-elk-definition-6af322e1-CuVXk2cv.js} +6 -6
- package/app/out/renderer/assets/{ganttDiagram-a2739b55-CP8oCEhK.js → ganttDiagram-a2739b55-BKt7y8Nk.js} +3 -3
- package/app/out/renderer/assets/{gitGraphDiagram-82fe8481-BwBuEj3Z.js → gitGraphDiagram-82fe8481-MVrgAPy8.js} +2 -2
- package/app/out/renderer/assets/{graph-Cm7VMO24.js → graph-47tN-JjA.js} +1 -1
- package/app/out/renderer/assets/{index-5325376f-rxU_OBcM.js → index-5325376f-BAwly5ON.js} +6 -6
- package/app/out/renderer/assets/{index-YHTq32NV.js → index-B7PIn7Sy.js} +3 -3
- package/app/out/renderer/assets/{index-BOSzW1Fp.js → index-BCAfDfd2.js} +3 -3
- package/app/out/renderer/assets/{index-D075LvBi.js → index-BtnFDu4c.js} +3 -3
- package/app/out/renderer/assets/{index-CmuzUeEb.js → index-C9k23NfW.js} +3 -3
- package/app/out/renderer/assets/{index-DwyuGyq2.js → index-CI7jql7H.js} +3 -3
- package/app/out/renderer/assets/{index-B86Rm5VH.js → index-CTXUhrja.js} +5 -5
- package/app/out/renderer/assets/{index-CXW3LNrw.js → index-CZhJgR4h.js} +6 -6
- package/app/out/renderer/assets/{index-C90yL_Oq.js → index-CaKrYTYv.js} +116 -72
- package/app/out/renderer/assets/{index-CnSaLake.js → index-Cdfq9GLa.js} +6 -6
- package/app/out/renderer/assets/{index-PcwO-UQr.js → index-ChRW2wyg.js} +4 -4
- package/app/out/renderer/assets/{index-qYYoWrK0.js → index-D38yt-gB.js} +3 -3
- package/app/out/renderer/assets/{index-BireTC8B.js → index-DF3gD3Ct.js} +6 -6
- package/app/out/renderer/assets/{index-JxH0rooB.js → index-DP9hc671.js} +6 -6
- package/app/out/renderer/assets/{index-DnIy5Txo.js → index-DcHrpeaZ.js} +3 -3
- package/app/out/renderer/assets/{index-B0bfGE4l.js → index-DnGwgCJt.js} +3 -3
- package/app/out/renderer/assets/{index-CZmZWnTr.js → index-DqAuRK1m.js} +3 -3
- package/app/out/renderer/assets/{index-UvRVROtt.js → index-DuccscxT.js} +3 -3
- package/app/out/renderer/assets/{index-BurA4U4Q.js → index-DyfldTxE.js} +4 -4
- package/app/out/renderer/assets/{index-Cnl3rlb4.js → index-IxItY9MF.js} +3 -3
- package/app/out/renderer/assets/{index-21ZazB27.js → index-LPLJgcfa.js} +3 -3
- package/app/out/renderer/assets/{index-CYcmNTWS.js → index-PvzYpUGb.js} +1 -1
- package/app/out/renderer/assets/{index-CdJBtB9B.js → index-Z0DiYIAm.js} +6 -6
- package/app/out/renderer/assets/{index-DQ1LKaqr.js → index-aAuuSY42.js} +6 -6
- package/app/out/renderer/assets/{infoDiagram-8eee0895-DX3y3-A9.js → infoDiagram-8eee0895-B7zSsSA5.js} +2 -2
- package/app/out/renderer/assets/{journeyDiagram-c64418c1-CWFt3XGT.js → journeyDiagram-c64418c1-D6rYrYE9.js} +4 -4
- package/app/out/renderer/assets/{layout-BlEAfCmy.js → layout-C3nD7S8q.js} +2 -2
- package/app/out/renderer/assets/{line-C8wQDOie.js → line-BD7gUCLm.js} +1 -1
- package/app/out/renderer/assets/{linear-B-vozVTM.js → linear-Dml1F21i.js} +1 -1
- package/app/out/renderer/assets/{mindmap-definition-8da855dc-BCWxRP64.js → mindmap-definition-8da855dc-IY0DZul4.js} +3 -3
- package/app/out/renderer/assets/{pieDiagram-a8764435-CjTt_TYd.js → pieDiagram-a8764435-CrWonzc_.js} +3 -3
- package/app/out/renderer/assets/{quadrantDiagram-1e28029f-D1Ke9L8a.js → quadrantDiagram-1e28029f-Bca0GRbN.js} +3 -3
- package/app/out/renderer/assets/{requirementDiagram-08caed73-qZ5fBq-x.js → requirementDiagram-08caed73-Dz2SRyGA.js} +5 -5
- package/app/out/renderer/assets/{sankeyDiagram-a04cb91d-B-KtJYmc.js → sankeyDiagram-a04cb91d-NfJN0Agl.js} +2 -2
- package/app/out/renderer/assets/{sequenceDiagram-c5b8d532-BjqWnQP_.js → sequenceDiagram-c5b8d532-D2Dx4CJP.js} +3 -3
- package/app/out/renderer/assets/{stateDiagram-1ecb1508-DQo_1-ne.js → stateDiagram-1ecb1508-CClex8bq.js} +6 -6
- package/app/out/renderer/assets/{stateDiagram-v2-c2b004d7-BNb4d-sS.js → stateDiagram-v2-c2b004d7-vkISAVpU.js} +10 -10
- package/app/out/renderer/assets/{styles-b4e223ce-pjL5kdCz.js → styles-b4e223ce-q4XIFTqx.js} +1 -1
- package/app/out/renderer/assets/{styles-ca3715f6-CtyHB9Sz.js → styles-ca3715f6-DEA1MmgW.js} +1 -1
- package/app/out/renderer/assets/{styles-d45a18b0-r6zLUSmM.js → styles-d45a18b0-JdnahWSX.js} +4 -4
- package/app/out/renderer/assets/{svgDrawCommon-b86b1483-CbHYm_eu.js → svgDrawCommon-b86b1483-nssZCQ5K.js} +1 -1
- package/app/out/renderer/assets/{timeline-definition-faaaa080-DvoW_Frb.js → timeline-definition-faaaa080-ChRZSyE8.js} +3 -3
- package/app/out/renderer/assets/{xychartDiagram-f5964ef8-C-G_0ZnR.js → xychartDiagram-f5964ef8-BrgVCZqF.js} +5 -5
- package/app/out/renderer/index.html +1 -1
- package/package.json +1 -1
package/app/out/main/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { setMaxListeners } from "node:events";
|
|
|
3
3
|
import fs, { existsSync as existsSync$1 } from "node:fs";
|
|
4
4
|
import { execFile, spawn, execSync } from "node:child_process";
|
|
5
5
|
import path, { resolve, join, sep, isAbsolute, extname, dirname, basename, relative } from "path";
|
|
6
|
-
import { existsSync, statSync, readdirSync as readdirSync$1, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync, openSync, constants, writeSync, closeSync } from "fs";
|
|
6
|
+
import { existsSync, statSync, readdirSync as readdirSync$1, readFileSync, writeFileSync, mkdirSync, appendFileSync, rmSync, unlinkSync, renameSync, openSync, constants, writeSync, closeSync, watch } from "fs";
|
|
7
7
|
import os$1, { homedir } from "os";
|
|
8
8
|
import { createHash, randomUUID } from "crypto";
|
|
9
9
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
@@ -633,6 +633,43 @@ function registerUsageHandlers(handle, getCtx, loadUsageTotals2, resetUsageTotal
|
|
|
633
633
|
return resetUsageTotals2(baseDir);
|
|
634
634
|
});
|
|
635
635
|
}
|
|
636
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
637
|
+
const inFlightLogin = /* @__PURE__ */ new Map();
|
|
638
|
+
function loginErrorMessage(raw, label) {
|
|
639
|
+
const msg = (raw instanceof Error ? raw.message : String(raw ?? "")).trim();
|
|
640
|
+
return msg || `${label} OAuth login failed`;
|
|
641
|
+
}
|
|
642
|
+
async function runLoginWithTimeout(key, label, runLogin, onSuccess) {
|
|
643
|
+
if (inFlightLogin.has(key)) {
|
|
644
|
+
return { success: false, error: `${label} sign-in is already in progress. Cancel it first.` };
|
|
645
|
+
}
|
|
646
|
+
let rejectManual = () => {
|
|
647
|
+
};
|
|
648
|
+
const manualPromise = new Promise((_, reject) => {
|
|
649
|
+
rejectManual = reject;
|
|
650
|
+
});
|
|
651
|
+
manualPromise.catch(() => {
|
|
652
|
+
});
|
|
653
|
+
const attempt = {
|
|
654
|
+
abort: (reason) => rejectManual(new Error(reason))
|
|
655
|
+
};
|
|
656
|
+
inFlightLogin.set(key, attempt);
|
|
657
|
+
const timer = setTimeout(() => {
|
|
658
|
+
attempt.abort(
|
|
659
|
+
`${label} sign-in timed out after ${Math.round(LOGIN_TIMEOUT_MS / 6e4)} minutes. Please try again.`
|
|
660
|
+
);
|
|
661
|
+
}, LOGIN_TIMEOUT_MS);
|
|
662
|
+
try {
|
|
663
|
+
const creds = await runLogin(() => manualPromise);
|
|
664
|
+
onSuccess(creds);
|
|
665
|
+
return { success: true };
|
|
666
|
+
} catch (err) {
|
|
667
|
+
return { success: false, error: loginErrorMessage(err, label) };
|
|
668
|
+
} finally {
|
|
669
|
+
clearTimeout(timer);
|
|
670
|
+
if (inFlightLogin.get(key) === attempt) inFlightLogin.delete(key);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
636
673
|
function registerAuthHandlers(handleRaw) {
|
|
637
674
|
handleRaw("auth:get-anthropic-status", () => {
|
|
638
675
|
const hasApiKey = !!(process.env.ANTHROPIC_API_KEY || "").trim();
|
|
@@ -659,8 +696,10 @@ function registerAuthHandlers(handleRaw) {
|
|
|
659
696
|
handleRaw("auth:anthropic-sub-login", async () => {
|
|
660
697
|
const { loginAnthropic } = await import("@mariozechner/pi-ai/oauth");
|
|
661
698
|
const { shell: shell2 } = await import("electron");
|
|
662
|
-
|
|
663
|
-
|
|
699
|
+
return runLoginWithTimeout(
|
|
700
|
+
"anthropic-sub",
|
|
701
|
+
"Anthropic",
|
|
702
|
+
(onManualCodeInput) => loginAnthropic({
|
|
664
703
|
onAuth: (info) => {
|
|
665
704
|
shell2.openExternal(info.url);
|
|
666
705
|
},
|
|
@@ -670,13 +709,17 @@ function registerAuthHandlers(handleRaw) {
|
|
|
670
709
|
},
|
|
671
710
|
onProgress: (msg) => {
|
|
672
711
|
console.log("[OAuth Anthropic]", msg);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
712
|
+
},
|
|
713
|
+
onManualCodeInput
|
|
714
|
+
}),
|
|
715
|
+
(creds) => saveAnthropicSubCredentials(creds)
|
|
716
|
+
);
|
|
717
|
+
});
|
|
718
|
+
handleRaw("auth:anthropic-sub-cancel", () => {
|
|
719
|
+
const attempt = inFlightLogin.get("anthropic-sub");
|
|
720
|
+
if (!attempt) return { success: false, error: "No Anthropic sign-in in progress" };
|
|
721
|
+
attempt.abort("Anthropic sign-in cancelled.");
|
|
722
|
+
return { success: true };
|
|
680
723
|
});
|
|
681
724
|
handleRaw("auth:anthropic-sub-logout", () => {
|
|
682
725
|
clearAnthropicSubCredentials();
|
|
@@ -704,8 +747,10 @@ function registerAuthHandlers(handleRaw) {
|
|
|
704
747
|
handleRaw("auth:openai-codex-login", async () => {
|
|
705
748
|
const { loginOpenAICodex } = await import("@mariozechner/pi-ai/oauth");
|
|
706
749
|
const { shell: shell2 } = await import("electron");
|
|
707
|
-
|
|
708
|
-
|
|
750
|
+
return runLoginWithTimeout(
|
|
751
|
+
"openai-codex",
|
|
752
|
+
"ChatGPT",
|
|
753
|
+
(onManualCodeInput) => loginOpenAICodex({
|
|
709
754
|
onAuth: (info) => {
|
|
710
755
|
shell2.openExternal(info.url);
|
|
711
756
|
},
|
|
@@ -715,13 +760,17 @@ function registerAuthHandlers(handleRaw) {
|
|
|
715
760
|
},
|
|
716
761
|
onProgress: (msg) => {
|
|
717
762
|
console.log("[OAuth]", msg);
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
763
|
+
},
|
|
764
|
+
onManualCodeInput
|
|
765
|
+
}),
|
|
766
|
+
(creds) => saveCodexCredentials(creds)
|
|
767
|
+
);
|
|
768
|
+
});
|
|
769
|
+
handleRaw("auth:openai-codex-cancel", () => {
|
|
770
|
+
const attempt = inFlightLogin.get("openai-codex");
|
|
771
|
+
if (!attempt) return { success: false, error: "No ChatGPT sign-in in progress" };
|
|
772
|
+
attempt.abort("ChatGPT sign-in cancelled.");
|
|
773
|
+
return { success: true };
|
|
725
774
|
});
|
|
726
775
|
handleRaw("auth:openai-codex-logout", () => {
|
|
727
776
|
clearCodexCredentials();
|
|
@@ -1255,6 +1304,32 @@ function readLatestSessionSummary(projectPath, sessionId) {
|
|
|
1255
1304
|
});
|
|
1256
1305
|
return readJson(join(dir, files[0]), null);
|
|
1257
1306
|
}
|
|
1307
|
+
function readOrphanMessages(projectPath, sessionId, cutoffMs) {
|
|
1308
|
+
const file = join(projectPath, PATHS.sessions, `${sessionId}.jsonl`);
|
|
1309
|
+
if (!existsSync(file)) return [];
|
|
1310
|
+
const result = [];
|
|
1311
|
+
let raw;
|
|
1312
|
+
try {
|
|
1313
|
+
raw = readFileSync(file, "utf-8");
|
|
1314
|
+
} catch {
|
|
1315
|
+
return [];
|
|
1316
|
+
}
|
|
1317
|
+
for (const line of raw.split("\n")) {
|
|
1318
|
+
if (!line) continue;
|
|
1319
|
+
let msg;
|
|
1320
|
+
try {
|
|
1321
|
+
msg = JSON.parse(line);
|
|
1322
|
+
} catch {
|
|
1323
|
+
continue;
|
|
1324
|
+
}
|
|
1325
|
+
if (!msg || typeof msg !== "object") continue;
|
|
1326
|
+
if (msg.role !== "user" && msg.role !== "assistant") continue;
|
|
1327
|
+
if (typeof msg.timestamp !== "number" || msg.timestamp <= cutoffMs) continue;
|
|
1328
|
+
if (typeof msg.content !== "string" || msg.content.length === 0) continue;
|
|
1329
|
+
result.push({ role: msg.role, content: msg.content, timestamp: msg.timestamp });
|
|
1330
|
+
}
|
|
1331
|
+
return result;
|
|
1332
|
+
}
|
|
1258
1333
|
function generateCiteKey$1(authors, year, title) {
|
|
1259
1334
|
const firstAuthor = authors[0] || "unknown";
|
|
1260
1335
|
const lastName = firstAuthor.split(/\s+/).pop()?.toLowerCase() || "unknown";
|
|
@@ -2619,29 +2694,30 @@ If conversation history contains previous literature-search results with coverag
|
|
|
2619
2694
|
## Rules
|
|
2620
2695
|
|
|
2621
2696
|
1. You MUST provide a \`relevanceJustification\` for EVERY paper explaining WHY it received that score
|
|
2622
|
-
2. After scoring all papers, perform a FORCED RANKING: cut the bottom 30% — papers in the bottom 30% get excluded from
|
|
2697
|
+
2. After scoring all papers, perform a FORCED RANKING: cut the bottom 30% — papers in the bottom 30% get excluded from scoredPapers even if their score is above threshold
|
|
2623
2698
|
3. Auto-save threshold is **>= 7**. Papers scoring 7+ are saved to the local library. Be decisive: if a paper is meaningfully relevant (not just tangential), score it >= 7.
|
|
2624
2699
|
4. Approve only if at least 3 papers score >= 7 AND coverage >= 0.5. If confidence is low or critical coverage is missing, request targeted refinement.
|
|
2625
2700
|
5. If not approved, suggest at most 2-3 **targeted refinement queries** for specific missing sub-topics — NOT broad re-searches. These queries run through the FULL search pipeline again, so be selective. CRITICAL: Your refinement queries MUST be DIFFERENT from the "Queries used" listed at the bottom — the system will reject duplicate queries. Use different terminology, synonyms, or narrower/broader scope to find what the original queries missed
|
|
2626
2701
|
6. Track cumulative coverage across sub-topics
|
|
2627
|
-
7. Output size guard: include AT MOST 25
|
|
2702
|
+
7. Output size guard: include AT MOST 25 scoredPapers. If there are any reasonably relevant papers, include at least 3 (do NOT return an empty list unless ZERO papers are even tangentially relevant).
|
|
2703
|
+
|
|
2704
|
+
## CRITICAL: Compact output contract
|
|
2628
2705
|
|
|
2629
|
-
|
|
2706
|
+
The caller already holds the full paper metadata (title, authors, abstract, venue, doi, year, url, citationCount, source). You MUST NOT echo any of it back. Your ONLY job is to return scoring decisions keyed by the paper's **index number** — the 1-based number shown at the start of each paper line in the input (e.g. \`1. [semantic_scholar] "Foo..."\` → \`index: 1\`).
|
|
2630
2707
|
|
|
2631
|
-
|
|
2632
|
-
-
|
|
2633
|
-
-
|
|
2634
|
-
-
|
|
2635
|
-
- doi (string or null), venue (string or null), citationCount (number or null)
|
|
2708
|
+
- Output ONLY integers for \`index\`, integers 0-10 for \`relevanceScore\`, and a short 1-2 sentence \`relevanceJustification\`.
|
|
2709
|
+
- Do NOT include title, authors, abstract, venue, doi, year, url, source, or citationCount fields in scoredPapers. Those will be merged by the caller.
|
|
2710
|
+
- Every \`index\` MUST refer to a paper actually shown in the input. Do not invent indices.
|
|
2711
|
+
- Do not wrap the JSON in markdown fences unless strictly necessary; raw JSON is preferred.
|
|
2636
2712
|
|
|
2637
|
-
|
|
2713
|
+
Keeping the output compact is REQUIRED — echoing metadata caused past responses to be truncated, making the entire review unparseable.
|
|
2638
2714
|
|
|
2639
2715
|
## Output JSON
|
|
2640
2716
|
|
|
2641
2717
|
{
|
|
2642
2718
|
"approved": boolean,
|
|
2643
|
-
"
|
|
2644
|
-
{ "
|
|
2719
|
+
"scoredPapers": [
|
|
2720
|
+
{ "index": 1, "relevanceScore": 9, "relevanceJustification": "1-2 sentence explanation" }
|
|
2645
2721
|
],
|
|
2646
2722
|
"confidence": number,
|
|
2647
2723
|
"coverage": {
|
|
@@ -3427,20 +3503,46 @@ Additional context: ${extraContext}` : `Research request: ${query}`;
|
|
|
3427
3503
|
try {
|
|
3428
3504
|
const reviewText = await ctx.callLlm(REVIEWER_SYSTEM, reviewInput);
|
|
3429
3505
|
const parsed = safeJsonParse(reviewText);
|
|
3430
|
-
if (!parsed) {
|
|
3506
|
+
if (!parsed || !Array.isArray(parsed.scoredPapers)) {
|
|
3507
|
+
const fallbackCap = ctx.settings?.researchIntensity?.reviewCap ?? 25;
|
|
3431
3508
|
review = {
|
|
3432
3509
|
approved: true,
|
|
3433
|
-
relevantPapers: deduplicated.slice(0,
|
|
3434
|
-
|
|
3435
|
-
|
|
3510
|
+
relevantPapers: deduplicated.slice(0, fallbackCap).map((p) => ({
|
|
3511
|
+
...p,
|
|
3512
|
+
relevanceScore: 7,
|
|
3513
|
+
relevanceJustification: "Review parsing failed; included with default score 7 so the paper is still saved. Re-review manually."
|
|
3514
|
+
})),
|
|
3515
|
+
confidence: 0.3,
|
|
3516
|
+
coverage: { score: 0.3, coveredTopics: [], missingTopics: [], gaps: ["Review parsing failed"] },
|
|
3436
3517
|
issues: ["Review parsing failed"],
|
|
3437
3518
|
additionalQueries: null
|
|
3438
3519
|
};
|
|
3439
3520
|
pipelineWarnings.push(
|
|
3440
|
-
"LLM review parsing failed — papers
|
|
3521
|
+
"LLM review parsing failed — papers saved with default relevance score of 7. Relevance scores are NOT accurate. Re-review the saved papers manually."
|
|
3441
3522
|
);
|
|
3442
3523
|
} else {
|
|
3443
|
-
|
|
3524
|
+
const seenIndices = /* @__PURE__ */ new Set();
|
|
3525
|
+
const merged = [];
|
|
3526
|
+
for (const s of parsed.scoredPapers) {
|
|
3527
|
+
const idx = (Number(s?.index) | 0) - 1;
|
|
3528
|
+
if (idx < 0 || idx >= deduplicated.length) continue;
|
|
3529
|
+
if (seenIndices.has(idx)) continue;
|
|
3530
|
+
seenIndices.add(idx);
|
|
3531
|
+
const score = Number(s?.relevanceScore);
|
|
3532
|
+
merged.push({
|
|
3533
|
+
...deduplicated[idx],
|
|
3534
|
+
relevanceScore: Number.isFinite(score) ? score : 0,
|
|
3535
|
+
relevanceJustification: typeof s?.relevanceJustification === "string" ? s.relevanceJustification : ""
|
|
3536
|
+
});
|
|
3537
|
+
}
|
|
3538
|
+
review = {
|
|
3539
|
+
approved: parsed.approved ?? false,
|
|
3540
|
+
relevantPapers: merged,
|
|
3541
|
+
confidence: typeof parsed.confidence === "number" ? parsed.confidence : 0.5,
|
|
3542
|
+
coverage: parsed.coverage ?? { score: 0.5, coveredTopics: [], missingTopics: [], gaps: [] },
|
|
3543
|
+
issues: Array.isArray(parsed.issues) ? parsed.issues : [],
|
|
3544
|
+
additionalQueries: parsed.additionalQueries ?? null
|
|
3545
|
+
};
|
|
3444
3546
|
}
|
|
3445
3547
|
} catch (err) {
|
|
3446
3548
|
return toAgentResult("literature-search", toolError("EXECUTION_FAILED", `Review step failed: ${err.message}`, {
|
|
@@ -6511,6 +6613,7 @@ function getWikiRoot() {
|
|
|
6511
6613
|
return join(homedir(), ".research-pilot", "paper-wiki");
|
|
6512
6614
|
}
|
|
6513
6615
|
const GENERATOR_VERSION = 3;
|
|
6616
|
+
const HASH_SCHEMA_VERSION = 2;
|
|
6514
6617
|
function isValidArxivId(arxivId) {
|
|
6515
6618
|
const bare = arxivId.replace(/^https?:\/\/arxiv\.org\/abs\//, "").replace(/v\d+$/, "");
|
|
6516
6619
|
if (/^\d{4}\.\d{4,}$/.test(bare)) return true;
|
|
@@ -6529,6 +6632,18 @@ function computeCanonicalKey(artifact) {
|
|
|
6529
6632
|
return { canonicalKey: `title:${title}:${artifact.year ?? "nd"}`, keySource: "title+year" };
|
|
6530
6633
|
}
|
|
6531
6634
|
function computeSemanticHash(artifact) {
|
|
6635
|
+
const projection = {
|
|
6636
|
+
title: artifact.title,
|
|
6637
|
+
authors: artifact.authors,
|
|
6638
|
+
abstract: artifact.abstract,
|
|
6639
|
+
year: artifact.year,
|
|
6640
|
+
venue: artifact.venue,
|
|
6641
|
+
doi: artifact.doi,
|
|
6642
|
+
arxivId: artifact.arxivId
|
|
6643
|
+
};
|
|
6644
|
+
return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 16);
|
|
6645
|
+
}
|
|
6646
|
+
function computeSemanticHashV1(artifact) {
|
|
6532
6647
|
const projection = {
|
|
6533
6648
|
title: artifact.title,
|
|
6534
6649
|
authors: artifact.authors,
|
|
@@ -6544,6 +6659,10 @@ function computeSemanticHash(artifact) {
|
|
|
6544
6659
|
};
|
|
6545
6660
|
return createHash("sha256").update(JSON.stringify(projection)).digest("hex").slice(0, 16);
|
|
6546
6661
|
}
|
|
6662
|
+
function canSilentRestampLegacyWatermark(priorWatermark, artifact) {
|
|
6663
|
+
if ((priorWatermark.hashSchemaVersion ?? 1) >= HASH_SCHEMA_VERSION) return false;
|
|
6664
|
+
return priorWatermark.semanticHash === computeSemanticHashV1(artifact);
|
|
6665
|
+
}
|
|
6547
6666
|
function canonicalKeyToSlug(canonicalKey) {
|
|
6548
6667
|
return canonicalKey.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120);
|
|
6549
6668
|
}
|
|
@@ -6629,6 +6748,25 @@ function markPaperProcessed(entry) {
|
|
|
6629
6748
|
const content = Array.from(existing.values()).map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
6630
6749
|
safeWriteFile(path2, content);
|
|
6631
6750
|
}
|
|
6751
|
+
function restampProcessedBatch(updates) {
|
|
6752
|
+
if (updates.length === 0) return;
|
|
6753
|
+
const existing = readProcessedWatermark();
|
|
6754
|
+
let changed = 0;
|
|
6755
|
+
for (const upd of updates) {
|
|
6756
|
+
const entry = existing.get(upd.canonicalKey);
|
|
6757
|
+
if (!entry) continue;
|
|
6758
|
+
existing.set(upd.canonicalKey, {
|
|
6759
|
+
...entry,
|
|
6760
|
+
semanticHash: upd.semanticHash,
|
|
6761
|
+
hashSchemaVersion: upd.hashSchemaVersion
|
|
6762
|
+
});
|
|
6763
|
+
changed++;
|
|
6764
|
+
}
|
|
6765
|
+
if (changed === 0) return;
|
|
6766
|
+
const path2 = processedPath();
|
|
6767
|
+
const content = Array.from(existing.values()).map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
6768
|
+
safeWriteFile(path2, content);
|
|
6769
|
+
}
|
|
6632
6770
|
function markFulltextFailure(canonicalKey) {
|
|
6633
6771
|
const existing = readProcessedWatermark();
|
|
6634
6772
|
const entry = existing.get(canonicalKey);
|
|
@@ -7042,6 +7180,17 @@ function parsePaperPage(content, slug) {
|
|
|
7042
7180
|
repairUsed
|
|
7043
7181
|
};
|
|
7044
7182
|
}
|
|
7183
|
+
function synthesizeMinimalSidecar(canonicalKey, slug, sourceTier, generatorVersion) {
|
|
7184
|
+
return {
|
|
7185
|
+
schemaVersion: 3,
|
|
7186
|
+
canonicalKey,
|
|
7187
|
+
slug,
|
|
7188
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7189
|
+
generator_version: generatorVersion,
|
|
7190
|
+
source_tier: sourceTier,
|
|
7191
|
+
paper_type: "empirical"
|
|
7192
|
+
};
|
|
7193
|
+
}
|
|
7045
7194
|
function serializeMetaBlock(sidecar) {
|
|
7046
7195
|
const json = JSON.stringify(sidecar, null, 2);
|
|
7047
7196
|
return `${WIKI_META_OPEN}
|
|
@@ -8731,6 +8880,14 @@ function buildSessionSummaryContext(summary) {
|
|
|
8731
8880
|
];
|
|
8732
8881
|
return lines.join("\n");
|
|
8733
8882
|
}
|
|
8883
|
+
function buildRecentConversationContext(messages) {
|
|
8884
|
+
const lines = ["## Recent Conversation (resumed from prior session)", ""];
|
|
8885
|
+
for (const msg of messages) {
|
|
8886
|
+
const speaker = msg.role === "user" ? "User" : "Assistant";
|
|
8887
|
+
lines.push(`**${speaker}:** ${msg.content}`, "");
|
|
8888
|
+
}
|
|
8889
|
+
return lines.join("\n").trimEnd();
|
|
8890
|
+
}
|
|
8734
8891
|
function writeExplainSnapshot(projectPath, snapshot) {
|
|
8735
8892
|
const explainDir = join(projectPath, PATHS.explainDir);
|
|
8736
8893
|
mkdirSync(explainDir, { recursive: true });
|
|
@@ -8875,12 +9032,13 @@ async function createCoordinator(config) {
|
|
|
8875
9032
|
const skillsCatalog = buildSkillsCatalogPrompt(skills);
|
|
8876
9033
|
const baseSystemPrompt = SYSTEM_PROMPT + (skillsCatalog ? "\n\n" + skillsCatalog : "");
|
|
8877
9034
|
let compactionSummary;
|
|
9035
|
+
let bootstrapDone = false;
|
|
8878
9036
|
const agent = new Agent({
|
|
8879
9037
|
initialState: {
|
|
8880
9038
|
systemPrompt: baseSystemPrompt,
|
|
8881
9039
|
model: piModel ?? void 0,
|
|
8882
9040
|
tools: allTools,
|
|
8883
|
-
thinkingLevel: reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
|
|
9041
|
+
thinkingLevel: reasoningEffort === "max" ? "xhigh" : reasoningEffort === "high" ? "high" : reasoningEffort === "medium" ? "medium" : "low"
|
|
8884
9042
|
},
|
|
8885
9043
|
sessionId,
|
|
8886
9044
|
getApiKey: resolveApiKey,
|
|
@@ -9078,6 +9236,22 @@ ${historyText}`,
|
|
|
9078
9236
|
const mentionContext = buildMentionContext(mentions);
|
|
9079
9237
|
const latestSummary = readLatestSessionSummary(projectPath, sessionId);
|
|
9080
9238
|
const summaryContext = latestSummary ? buildSessionSummaryContext(latestSummary) : "";
|
|
9239
|
+
let bootstrapContext = "";
|
|
9240
|
+
if (!bootstrapDone) {
|
|
9241
|
+
bootstrapDone = true;
|
|
9242
|
+
try {
|
|
9243
|
+
const cutoffMs = latestSummary ? Date.parse(latestSummary.createdAt) || 0 : 0;
|
|
9244
|
+
const orphans = readOrphanMessages(projectPath, sessionId, cutoffMs);
|
|
9245
|
+
if (orphans.length > 0) {
|
|
9246
|
+
bootstrapContext = buildRecentConversationContext(orphans);
|
|
9247
|
+
if (debug) {
|
|
9248
|
+
console.log(`[Bootstrap] Recovered ${orphans.length} orphan message(s) from prior session`);
|
|
9249
|
+
}
|
|
9250
|
+
}
|
|
9251
|
+
} catch (err) {
|
|
9252
|
+
if (debug) console.warn("[Bootstrap] Failed to read orphan messages:", err);
|
|
9253
|
+
}
|
|
9254
|
+
}
|
|
9081
9255
|
const persistence = classifyPersistenceDecision(message);
|
|
9082
9256
|
const explain = {
|
|
9083
9257
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -9119,6 +9293,7 @@ ${agentMdContent}`;
|
|
|
9119
9293
|
agent.state.systemPrompt = enrichedSystem;
|
|
9120
9294
|
const contextParts = [];
|
|
9121
9295
|
if (summaryContext) contextParts.push(summaryContext);
|
|
9296
|
+
if (bootstrapContext) contextParts.push(bootstrapContext);
|
|
9122
9297
|
if (skillSummariesPrompt) contextParts.push(skillSummariesPrompt);
|
|
9123
9298
|
if (mentionContext) contextParts.push(mentionContext);
|
|
9124
9299
|
let userMessage = contextParts.length > 0 ? `${contextParts.join("\n\n")}
|
|
@@ -9162,6 +9337,23 @@ ${message}` : message;
|
|
|
9162
9337
|
}
|
|
9163
9338
|
}
|
|
9164
9339
|
writeExplainSnapshot(projectPath, explain);
|
|
9340
|
+
const stopReason = lastMsg && "stopReason" in lastMsg ? lastMsg.stopReason : void 0;
|
|
9341
|
+
const errorMessage = lastMsg && "errorMessage" in lastMsg ? lastMsg.errorMessage : void 0;
|
|
9342
|
+
if (stopReason === "aborted") {
|
|
9343
|
+
return { success: false, error: "Generation stopped by user." };
|
|
9344
|
+
}
|
|
9345
|
+
if (stopReason === "error") {
|
|
9346
|
+
return {
|
|
9347
|
+
success: false,
|
|
9348
|
+
error: errorMessage || "The LLM request failed. This usually indicates an issue on the model server or with authentication. If you are using a Claude or ChatGPT subscription, try signing out and back in via Settings. Otherwise verify your API key, model selection, and network connection."
|
|
9349
|
+
};
|
|
9350
|
+
}
|
|
9351
|
+
if (!responseText && responseImages.length === 0) {
|
|
9352
|
+
return {
|
|
9353
|
+
success: false,
|
|
9354
|
+
error: `The model returned an empty response (stopReason=${stopReason ?? "unknown"}). This usually indicates a transient issue with the LLM server. Please try again. If the problem persists and you are using a Claude or ChatGPT subscription, try signing out and back in via Settings.`
|
|
9355
|
+
};
|
|
9356
|
+
}
|
|
9165
9357
|
turnCount++;
|
|
9166
9358
|
turnHistory.push({
|
|
9167
9359
|
userMessage: message.slice(0, 300),
|
|
@@ -11131,6 +11323,7 @@ function scanForNewContent(projectPaths) {
|
|
|
11131
11323
|
const processed = readProcessedWatermark();
|
|
11132
11324
|
const toProcess = [];
|
|
11133
11325
|
const provenanceOnly = [];
|
|
11326
|
+
const restamps = [];
|
|
11134
11327
|
const existingProvenance = readProvenance();
|
|
11135
11328
|
const provenanceIndex = /* @__PURE__ */ new Map();
|
|
11136
11329
|
for (const entry of existingProvenance) {
|
|
@@ -11189,6 +11382,20 @@ function scanForNewContent(projectPaths) {
|
|
|
11189
11382
|
provenanceIndex.delete(fallbackKey);
|
|
11190
11383
|
}
|
|
11191
11384
|
}
|
|
11385
|
+
const priorWatermark = processed.get(canonicalKey);
|
|
11386
|
+
if (priorWatermark && canSilentRestampLegacyWatermark(priorWatermark, artifact)) {
|
|
11387
|
+
const restamped = {
|
|
11388
|
+
...priorWatermark,
|
|
11389
|
+
semanticHash,
|
|
11390
|
+
hashSchemaVersion: HASH_SCHEMA_VERSION
|
|
11391
|
+
};
|
|
11392
|
+
processed.set(canonicalKey, restamped);
|
|
11393
|
+
restamps.push({
|
|
11394
|
+
canonicalKey,
|
|
11395
|
+
semanticHash,
|
|
11396
|
+
hashSchemaVersion: HASH_SCHEMA_VERSION
|
|
11397
|
+
});
|
|
11398
|
+
}
|
|
11192
11399
|
const watermark = processed.get(canonicalKey);
|
|
11193
11400
|
const knownProvenance = provenanceIndex.get(canonicalKey) || /* @__PURE__ */ new Set();
|
|
11194
11401
|
const siblings = entries.filter((e) => e.artifact.id !== artifact.id || e.projectPath !== projectPath).map((e) => ({ projectPath: e.projectPath, artifact: e.artifact }));
|
|
@@ -11227,6 +11434,9 @@ function scanForNewContent(projectPaths) {
|
|
|
11227
11434
|
const bTime = new Date(b.artifact.createdAt).getTime();
|
|
11228
11435
|
return bTime - aTime;
|
|
11229
11436
|
});
|
|
11437
|
+
if (restamps.length > 0) {
|
|
11438
|
+
restampProcessedBatch(restamps);
|
|
11439
|
+
}
|
|
11230
11440
|
return { toProcess, provenanceOnly };
|
|
11231
11441
|
}
|
|
11232
11442
|
const ARXIV_RATE_LIMIT_MS = 3e3;
|
|
@@ -11347,19 +11557,6 @@ function buildPaperUserContent(artifact, fulltext, existingConceptSlugs) {
|
|
|
11347
11557
|
parts.push(`
|
|
11348
11558
|
Abstract:
|
|
11349
11559
|
${artifact.abstract || "(no abstract)"}`);
|
|
11350
|
-
if (artifact.keyFindings?.length) {
|
|
11351
|
-
parts.push(`
|
|
11352
|
-
Key Findings:
|
|
11353
|
-
${artifact.keyFindings.map((f) => `- ${f}`).join("\n")}`);
|
|
11354
|
-
}
|
|
11355
|
-
if (artifact.relevanceJustification) {
|
|
11356
|
-
parts.push(`
|
|
11357
|
-
Relevance: ${artifact.relevanceJustification}`);
|
|
11358
|
-
}
|
|
11359
|
-
if (artifact.subTopic) {
|
|
11360
|
-
parts.push(`
|
|
11361
|
-
Sub-topic: ${artifact.subTopic}`);
|
|
11362
|
-
}
|
|
11363
11560
|
if (fulltext) {
|
|
11364
11561
|
const truncated = fulltext.length > 3e4 ? fulltext.slice(0, 3e4) + "\n\n[... truncated for length ...]" : fulltext;
|
|
11365
11562
|
parts.push(`
|
|
@@ -11510,9 +11707,11 @@ function recordSidecarStatus(entry) {
|
|
|
11510
11707
|
const content = Array.from(existing.values()).map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
11511
11708
|
safeWriteFile(statusFilePath(), content);
|
|
11512
11709
|
}
|
|
11710
|
+
const MAX_REPAIR_ATTEMPTS = 2;
|
|
11513
11711
|
function listStaleOrMissing(currentGeneratorVersion) {
|
|
11514
11712
|
const result = [];
|
|
11515
11713
|
for (const entry of readSidecarStatus().values()) {
|
|
11714
|
+
if ((entry.repairAttempts ?? 0) >= MAX_REPAIR_ATTEMPTS) continue;
|
|
11516
11715
|
if (entry.status === "missing" || entry.generator_version < currentGeneratorVersion) {
|
|
11517
11716
|
result.push(entry);
|
|
11518
11717
|
}
|
|
@@ -11642,6 +11841,7 @@ function createWikiAgent(config) {
|
|
|
11642
11841
|
let errors = 0;
|
|
11643
11842
|
if (!acquireProcessLock()) {
|
|
11644
11843
|
log("process lock held by another instance, skipping");
|
|
11844
|
+
emitStatus(0);
|
|
11645
11845
|
return { processed: 0, errors: 0, pendingRemaining: 0 };
|
|
11646
11846
|
}
|
|
11647
11847
|
try {
|
|
@@ -11650,6 +11850,7 @@ function createWikiAgent(config) {
|
|
|
11650
11850
|
const projectPaths = config.projectPaths();
|
|
11651
11851
|
if (projectPaths.length === 0) {
|
|
11652
11852
|
log("no project paths, nothing to scan");
|
|
11853
|
+
emitStatus(0);
|
|
11653
11854
|
return { processed: 0, errors: 0, pendingRemaining: 0 };
|
|
11654
11855
|
}
|
|
11655
11856
|
const { toProcess, provenanceOnly } = scanForNewContent(projectPaths);
|
|
@@ -11776,7 +11977,16 @@ function createWikiAgent(config) {
|
|
|
11776
11977
|
if (!result || !shouldContinue()) return;
|
|
11777
11978
|
const paperPath = join(getWikiRoot(), "papers", `${slug}.md`);
|
|
11778
11979
|
safeWriteFile(paperPath, result.content);
|
|
11779
|
-
|
|
11980
|
+
let parseOutcome = parsePaperPage(result.content, slug);
|
|
11981
|
+
if (parseOutcome.status === "missing") {
|
|
11982
|
+
const sourceTier = result.fulltextStatus === "fulltext" ? "fulltext" : "abstract-only";
|
|
11983
|
+
const fallback = synthesizeMinimalSidecar(canonicalKey, slug, sourceTier, GENERATOR_VERSION);
|
|
11984
|
+
const patched = writeMetaBlockInto(parseOutcome.body, fallback);
|
|
11985
|
+
safeWriteFile(paperPath, patched);
|
|
11986
|
+
parseOutcome = parsePaperPage(patched, slug);
|
|
11987
|
+
log(`synthesized fallback sidecar for ${slug}`);
|
|
11988
|
+
}
|
|
11989
|
+
const priorStatus = scanResult.reason === "repair" ? readSidecarStatus().get(slug) : void 0;
|
|
11780
11990
|
recordSidecarStatus({
|
|
11781
11991
|
slug,
|
|
11782
11992
|
status: parseOutcome.status,
|
|
@@ -11784,7 +11994,8 @@ function createWikiAgent(config) {
|
|
|
11784
11994
|
droppedFields: parseOutcome.droppedFields,
|
|
11785
11995
|
generator_version: GENERATOR_VERSION,
|
|
11786
11996
|
recorded_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11787
|
-
repairUsed: parseOutcome.repairUsed
|
|
11997
|
+
repairUsed: parseOutcome.repairUsed,
|
|
11998
|
+
repairAttempts: scanResult.reason === "repair" ? (priorStatus?.repairAttempts ?? 0) + 1 : void 0
|
|
11788
11999
|
});
|
|
11789
12000
|
mergeProjectContextIntoPage(slug, projectPath, artifact);
|
|
11790
12001
|
if (!shouldContinue()) return;
|
|
@@ -11813,6 +12024,7 @@ function createWikiAgent(config) {
|
|
|
11813
12024
|
semanticHash,
|
|
11814
12025
|
fulltextStatus: result.fulltextStatus,
|
|
11815
12026
|
generatorVersion: GENERATOR_VERSION,
|
|
12027
|
+
hashSchemaVersion: HASH_SCHEMA_VERSION,
|
|
11816
12028
|
processedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
11817
12029
|
};
|
|
11818
12030
|
markPaperProcessed(entry);
|
|
@@ -11865,6 +12077,7 @@ function createWikiAgent(config) {
|
|
|
11865
12077
|
start() {
|
|
11866
12078
|
if (state !== "created") return;
|
|
11867
12079
|
state = "idle";
|
|
12080
|
+
emitStatus(0);
|
|
11868
12081
|
log(`starting (delay ${config.pacing.startupDelayMs / 1e3}s)`);
|
|
11869
12082
|
timer = setTimeout(() => tick(), config.pacing.startupDelayMs);
|
|
11870
12083
|
},
|
|
@@ -12325,6 +12538,7 @@ function resetUsageTotals(baseDir) {
|
|
|
12325
12538
|
writeAtomically(usagePath(baseDir), JSON.stringify(cleared, null, 2));
|
|
12326
12539
|
return cleared;
|
|
12327
12540
|
}
|
|
12541
|
+
let loggedLinuxWatchWarning = false;
|
|
12328
12542
|
function compareVersions(a, b) {
|
|
12329
12543
|
const pa = a.split(".").map(Number);
|
|
12330
12544
|
const pb = b.split(".").map(Number);
|
|
@@ -12407,7 +12621,8 @@ function createWindowRuntimeState() {
|
|
|
12407
12621
|
projectPath: "",
|
|
12408
12622
|
sessionId: crypto.randomUUID(),
|
|
12409
12623
|
isClosing: false,
|
|
12410
|
-
realtimeBuffer: createRealtimeBuffer()
|
|
12624
|
+
realtimeBuffer: createRealtimeBuffer(),
|
|
12625
|
+
fsWatcher: null
|
|
12411
12626
|
};
|
|
12412
12627
|
}
|
|
12413
12628
|
function getOrCreateWindowState(win) {
|
|
@@ -12432,6 +12647,10 @@ function registerWindow(win) {
|
|
|
12432
12647
|
win.on("closed", () => {
|
|
12433
12648
|
const state = windowStates.get(key);
|
|
12434
12649
|
if (!state) return;
|
|
12650
|
+
if (state.fsWatcher) {
|
|
12651
|
+
state.fsWatcher.close();
|
|
12652
|
+
state.fsWatcher = null;
|
|
12653
|
+
}
|
|
12435
12654
|
if (state.coordinator) {
|
|
12436
12655
|
state.coordinator.destroy().catch(() => {
|
|
12437
12656
|
});
|
|
@@ -13484,6 +13703,43 @@ ${msg.content}
|
|
|
13484
13703
|
writeFileSync(result.filePath, markdown, "utf-8");
|
|
13485
13704
|
return { success: true, path: result.filePath };
|
|
13486
13705
|
});
|
|
13706
|
+
const IGNORED_SEGMENTS = /* @__PURE__ */ new Set(["node_modules", ".git", ".research-pilot"]);
|
|
13707
|
+
function startFsWatcher(state, win) {
|
|
13708
|
+
if (state.fsWatcher) {
|
|
13709
|
+
state.fsWatcher.close();
|
|
13710
|
+
state.fsWatcher = null;
|
|
13711
|
+
}
|
|
13712
|
+
if (!state.projectPath) return;
|
|
13713
|
+
if (process.platform === "linux" && !loggedLinuxWatchWarning) {
|
|
13714
|
+
loggedLinuxWatchWarning = true;
|
|
13715
|
+
console.warn(
|
|
13716
|
+
"[fs-watcher] Recursive fs.watch is not supported on Linux. Only top-level changes in the workspace will auto-refresh the file tree; changes in subdirectories will require a manual refresh."
|
|
13717
|
+
);
|
|
13718
|
+
}
|
|
13719
|
+
let debounceTimer = null;
|
|
13720
|
+
try {
|
|
13721
|
+
state.fsWatcher = watch(state.projectPath, { recursive: true }, (_event, filename) => {
|
|
13722
|
+
if (filename) {
|
|
13723
|
+
const segments = filename.toString().split(/[/\\]/);
|
|
13724
|
+
if (segments.some((s) => IGNORED_SEGMENTS.has(s))) return;
|
|
13725
|
+
}
|
|
13726
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
13727
|
+
debounceTimer = setTimeout(() => {
|
|
13728
|
+
debounceTimer = null;
|
|
13729
|
+
safeSend(win, "fs:external-change");
|
|
13730
|
+
}, 500);
|
|
13731
|
+
});
|
|
13732
|
+
state.fsWatcher.on("error", () => {
|
|
13733
|
+
if (debounceTimer) {
|
|
13734
|
+
clearTimeout(debounceTimer);
|
|
13735
|
+
debounceTimer = null;
|
|
13736
|
+
}
|
|
13737
|
+
state.fsWatcher?.close();
|
|
13738
|
+
state.fsWatcher = null;
|
|
13739
|
+
});
|
|
13740
|
+
} catch {
|
|
13741
|
+
}
|
|
13742
|
+
}
|
|
13487
13743
|
async function openProjectFolder(state, win, projectPath) {
|
|
13488
13744
|
if (!projectPath || !existsSync(projectPath)) return null;
|
|
13489
13745
|
if (state.coordinator) {
|
|
@@ -13496,6 +13752,7 @@ ${msg.content}
|
|
|
13496
13752
|
state.realtimeBuffer.reset();
|
|
13497
13753
|
state.projectPath = projectPath;
|
|
13498
13754
|
initializeProject(state.projectPath);
|
|
13755
|
+
startFsWatcher(state, win);
|
|
13499
13756
|
state.sessionId = loadOrCreateSessionId(PATHS.root, state.projectPath);
|
|
13500
13757
|
const prefsFile = join(state.projectPath, PATHS.root, "preferences.json");
|
|
13501
13758
|
if (existsSync(prefsFile)) {
|
|
@@ -13577,6 +13834,10 @@ ${msg.content}
|
|
|
13577
13834
|
handleWindow("project:close", async ({ state }) => {
|
|
13578
13835
|
state.isClosing = true;
|
|
13579
13836
|
try {
|
|
13837
|
+
if (state.fsWatcher) {
|
|
13838
|
+
state.fsWatcher.close();
|
|
13839
|
+
state.fsWatcher = null;
|
|
13840
|
+
}
|
|
13580
13841
|
if (state.coordinator) {
|
|
13581
13842
|
try {
|
|
13582
13843
|
state.coordinator.abort();
|
package/app/out/preload/index.js
CHANGED
|
@@ -27,11 +27,13 @@ const api = {
|
|
|
27
27
|
// OpenAI Codex (ChatGPT Subscription) OAuth
|
|
28
28
|
getOpenAICodexStatus: () => electron.ipcRenderer.invoke("auth:get-openai-codex-status"),
|
|
29
29
|
openaiCodexLogin: () => electron.ipcRenderer.invoke("auth:openai-codex-login"),
|
|
30
|
+
openaiCodexCancel: () => electron.ipcRenderer.invoke("auth:openai-codex-cancel"),
|
|
30
31
|
openaiCodexLogout: () => electron.ipcRenderer.invoke("auth:openai-codex-logout"),
|
|
31
32
|
// Anthropic Subscription (Claude Pro/Max) OAuth — enabled by default
|
|
32
33
|
isClaudeSubEnabled: () => true,
|
|
33
34
|
getAnthropicSubStatus: () => electron.ipcRenderer.invoke("auth:get-anthropic-sub-status"),
|
|
34
35
|
anthropicSubLogin: () => electron.ipcRenderer.invoke("auth:anthropic-sub-login"),
|
|
36
|
+
anthropicSubCancel: () => electron.ipcRenderer.invoke("auth:anthropic-sub-cancel"),
|
|
35
37
|
anthropicSubLogout: () => electron.ipcRenderer.invoke("auth:anthropic-sub-logout"),
|
|
36
38
|
pickPreferredModel: () => electron.ipcRenderer.invoke("config:pick-preferred-model"),
|
|
37
39
|
getApiKeyStatus: () => electron.ipcRenderer.invoke("config:get-api-key-status"),
|
|
@@ -105,6 +107,11 @@ const api = {
|
|
|
105
107
|
electron.ipcRenderer.on("agent:entity-created", handler);
|
|
106
108
|
return () => electron.ipcRenderer.removeListener("agent:entity-created", handler);
|
|
107
109
|
},
|
|
110
|
+
onExternalChange: (cb) => {
|
|
111
|
+
const handler = () => cb();
|
|
112
|
+
electron.ipcRenderer.on("fs:external-change", handler);
|
|
113
|
+
return () => electron.ipcRenderer.removeListener("fs:external-change", handler);
|
|
114
|
+
},
|
|
108
115
|
onFileCreated: (cb) => {
|
|
109
116
|
const handler = (_, path) => cb(path);
|
|
110
117
|
electron.ipcRenderer.on("agent:file-created", handler);
|