@binarycheater/research-sidecar 0.1.1 → 0.1.2
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/dist/client/assets/{index-BpVgCKdz.css → index-C3mAbt-i.css} +1 -1
- package/dist/client/assets/index-DTKJmZxM.js +321 -0
- package/dist/client/index.html +2 -2
- package/dist-server/lib/files.js +71 -0
- package/dist-server/lib/researchGraph.js +76 -0
- package/dist-server/server/index.js +17 -3
- package/package.json +1 -1
- package/dist/client/assets/index-D7VDrQ1Q.js +0 -324
package/dist/client/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Research Sidecar</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-DTKJmZxM.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-C3mAbt-i.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
package/dist-server/lib/files.js
CHANGED
|
@@ -77,6 +77,40 @@ export async function readWorkspaceFile(workspaceRoot, requestedPath) {
|
|
|
77
77
|
addedAt: new Date().toISOString()
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
+
export async function readWorkspaceRawFile(workspaceRoot, requestedPath) {
|
|
81
|
+
const fullPath = resolveWorkspacePath(workspaceRoot, requestedPath);
|
|
82
|
+
const info = await stat(fullPath);
|
|
83
|
+
if (!info.isFile()) {
|
|
84
|
+
throw new Error("Requested path is not a file.");
|
|
85
|
+
}
|
|
86
|
+
const content = await readFile(fullPath);
|
|
87
|
+
return {
|
|
88
|
+
path: requestedPath,
|
|
89
|
+
content,
|
|
90
|
+
bytes: content.byteLength,
|
|
91
|
+
mimeType: detectRawMimeType(requestedPath)
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function resolveWorkspaceFileForOpen(workspaceRoot, requestedPath) {
|
|
95
|
+
const fullPath = resolveWorkspacePath(workspaceRoot, requestedPath);
|
|
96
|
+
const info = await stat(fullPath);
|
|
97
|
+
if (!info.isFile()) {
|
|
98
|
+
throw new Error("Requested path is not a file.");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
path: requestedPath,
|
|
102
|
+
fullPath
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function getWorkspaceOpenCommand(fullPath, platform = process.platform) {
|
|
106
|
+
if (platform === "darwin") {
|
|
107
|
+
return { command: "open", args: [fullPath] };
|
|
108
|
+
}
|
|
109
|
+
if (platform === "win32") {
|
|
110
|
+
return { command: "cmd", args: ["/c", "start", "", fullPath] };
|
|
111
|
+
}
|
|
112
|
+
return { command: "xdg-open", args: [fullPath] };
|
|
113
|
+
}
|
|
80
114
|
export async function writeWorkspaceFile(workspaceRoot, requestedPath, content, allowedExtensions) {
|
|
81
115
|
assertWritableWorkspacePath(requestedPath, allowedExtensions);
|
|
82
116
|
const fullPath = resolveWorkspacePath(workspaceRoot, requestedPath);
|
|
@@ -97,6 +131,43 @@ export function detectFilePreviewFormat(path) {
|
|
|
97
131
|
}
|
|
98
132
|
return { format: "text", mimeType: "text/plain" };
|
|
99
133
|
}
|
|
134
|
+
function detectRawMimeType(path) {
|
|
135
|
+
const extension = extname(path).toLowerCase();
|
|
136
|
+
switch (extension) {
|
|
137
|
+
case ".apng":
|
|
138
|
+
return "image/apng";
|
|
139
|
+
case ".avif":
|
|
140
|
+
return "image/avif";
|
|
141
|
+
case ".gif":
|
|
142
|
+
return "image/gif";
|
|
143
|
+
case ".jpg":
|
|
144
|
+
case ".jpeg":
|
|
145
|
+
return "image/jpeg";
|
|
146
|
+
case ".png":
|
|
147
|
+
return "image/png";
|
|
148
|
+
case ".svg":
|
|
149
|
+
return "image/svg+xml";
|
|
150
|
+
case ".webp":
|
|
151
|
+
return "image/webp";
|
|
152
|
+
case ".html":
|
|
153
|
+
case ".htm":
|
|
154
|
+
return "text/html; charset=utf-8";
|
|
155
|
+
case ".md":
|
|
156
|
+
case ".markdown":
|
|
157
|
+
return "text/markdown; charset=utf-8";
|
|
158
|
+
case ".css":
|
|
159
|
+
return "text/css; charset=utf-8";
|
|
160
|
+
case ".js":
|
|
161
|
+
case ".mjs":
|
|
162
|
+
return "text/javascript; charset=utf-8";
|
|
163
|
+
case ".json":
|
|
164
|
+
return "application/json; charset=utf-8";
|
|
165
|
+
case ".pdf":
|
|
166
|
+
return "application/pdf";
|
|
167
|
+
default:
|
|
168
|
+
return "application/octet-stream";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
100
171
|
function assertWritableWorkspacePath(path, allowedExtensions) {
|
|
101
172
|
const extension = extname(path).toLowerCase();
|
|
102
173
|
if (CODE_EXTENSIONS.has(extension)) {
|
|
@@ -67,9 +67,57 @@ export function getExpandableDescendantIds(graph, rootId) {
|
|
|
67
67
|
}
|
|
68
68
|
return result;
|
|
69
69
|
}
|
|
70
|
+
export function getNextExpandedIdsForNodeClick(graph, currentIds, nodeId) {
|
|
71
|
+
const next = new Set(currentIds);
|
|
72
|
+
const descendantIds = getHierarchyDescendantIds(graph, nodeId);
|
|
73
|
+
if (next.has(nodeId)) {
|
|
74
|
+
next.delete(nodeId);
|
|
75
|
+
for (const descendantId of descendantIds) {
|
|
76
|
+
next.delete(descendantId);
|
|
77
|
+
}
|
|
78
|
+
return next;
|
|
79
|
+
}
|
|
80
|
+
next.add(nodeId);
|
|
81
|
+
for (const descendantId of descendantIds) {
|
|
82
|
+
next.delete(descendantId);
|
|
83
|
+
}
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
export function getFocusedResearchGraph(graph, options) {
|
|
87
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
88
|
+
const focus = byId.get(options.focusId);
|
|
89
|
+
if (!focus) {
|
|
90
|
+
return { rootId: graph.rootId, nodes: [], edges: [], childCounts: {} };
|
|
91
|
+
}
|
|
92
|
+
const hierarchyEdges = graph.edges.filter((edge) => hierarchyEdgeKinds.has(edge.kind));
|
|
93
|
+
const childrenByParent = groupEdges(hierarchyEdges, "from");
|
|
94
|
+
const parentsByChild = groupEdges(hierarchyEdges, "to");
|
|
95
|
+
const childCounts = Object.fromEntries(graph.nodes.map((node) => [node.id, childrenByParent.get(node.id)?.length || 0]));
|
|
96
|
+
const visibleIds = new Set([options.focusId]);
|
|
97
|
+
for (const edge of graph.edges) {
|
|
98
|
+
if (edge.from === options.focusId)
|
|
99
|
+
visibleIds.add(edge.to);
|
|
100
|
+
if (edge.to === options.focusId)
|
|
101
|
+
visibleIds.add(edge.from);
|
|
102
|
+
}
|
|
103
|
+
if (options.includeAntecedents) {
|
|
104
|
+
for (const ancestorId of getAncestorIds(options.focusId, parentsByChild)) {
|
|
105
|
+
visibleIds.add(ancestorId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const nodes = graph.nodes.filter((node) => visibleIds.has(node.id));
|
|
109
|
+
const edges = graph.edges.filter((edge) => visibleIds.has(edge.from) && visibleIds.has(edge.to));
|
|
110
|
+
return {
|
|
111
|
+
rootId: focus.id,
|
|
112
|
+
nodes,
|
|
113
|
+
edges,
|
|
114
|
+
childCounts
|
|
115
|
+
};
|
|
116
|
+
}
|
|
70
117
|
export function layoutResearchGraph(graph, options) {
|
|
71
118
|
const hierarchyEdges = graph.edges.filter((edge) => hierarchyEdgeKinds.has(edge.kind));
|
|
72
119
|
const childrenByParent = groupEdges(hierarchyEdges, "from");
|
|
120
|
+
const parentsByChild = groupEdges(hierarchyEdges, "to");
|
|
73
121
|
const depthById = new Map([[graph.rootId, 0]]);
|
|
74
122
|
const queue = [graph.rootId];
|
|
75
123
|
for (let index = 0; index < queue.length; index += 1) {
|
|
@@ -82,6 +130,17 @@ export function layoutResearchGraph(graph, options) {
|
|
|
82
130
|
queue.push(edge.to);
|
|
83
131
|
}
|
|
84
132
|
}
|
|
133
|
+
const parentQueue = [graph.rootId];
|
|
134
|
+
for (let index = 0; index < parentQueue.length; index += 1) {
|
|
135
|
+
const childId = parentQueue[index];
|
|
136
|
+
const childDepth = depthById.get(childId) || 0;
|
|
137
|
+
for (const edge of parentsByChild.get(childId) || []) {
|
|
138
|
+
if (depthById.has(edge.from))
|
|
139
|
+
continue;
|
|
140
|
+
depthById.set(edge.from, childDepth - 1);
|
|
141
|
+
parentQueue.push(edge.from);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
85
144
|
const rows = new Map();
|
|
86
145
|
for (const node of graph.nodes) {
|
|
87
146
|
const depth = depthById.get(node.id) ?? 0;
|
|
@@ -114,6 +173,23 @@ function collectExpandedTree(id, childrenByParent, expandedIds, visibleIds) {
|
|
|
114
173
|
collectExpandedTree(edge.to, childrenByParent, expandedIds, visibleIds);
|
|
115
174
|
}
|
|
116
175
|
}
|
|
176
|
+
function getHierarchyDescendantIds(graph, rootId) {
|
|
177
|
+
const hierarchyEdges = graph.edges.filter((edge) => hierarchyEdgeKinds.has(edge.kind));
|
|
178
|
+
const childrenByParent = groupEdges(hierarchyEdges, "from");
|
|
179
|
+
const result = [];
|
|
180
|
+
const queue = [rootId];
|
|
181
|
+
const seen = new Set([rootId]);
|
|
182
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
183
|
+
for (const edge of childrenByParent.get(queue[index]) || []) {
|
|
184
|
+
if (seen.has(edge.to))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(edge.to);
|
|
187
|
+
result.push(edge.to);
|
|
188
|
+
queue.push(edge.to);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
117
193
|
function getAncestorIds(id, parentsByChild) {
|
|
118
194
|
const ancestors = [];
|
|
119
195
|
const seen = new Set();
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import express from "express";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
2
3
|
import { resolve } from "node:path";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { buildContextPacket } from "../lib/context.js";
|
|
5
|
-
import { readWorkspaceFile } from "../lib/files.js";
|
|
6
|
+
import { getWorkspaceOpenCommand, readWorkspaceFile, readWorkspaceRawFile, resolveWorkspaceFileForOpen } from "../lib/files.js";
|
|
6
7
|
import { discoverGraphManifests } from "../lib/graphDiscovery.js";
|
|
7
8
|
import { DEFAULT_REVIEW_PROMPT } from "../lib/prompt.js";
|
|
8
9
|
import { loadResearchGraphManifest } from "../lib/researchGraphManifest.js";
|
|
@@ -75,7 +76,7 @@ app.get("/api/workspace/file", async (req, res, next) => {
|
|
|
75
76
|
app.get("/api/workspace/raw", async (req, res, next) => {
|
|
76
77
|
try {
|
|
77
78
|
const path = String(req.query.path || "");
|
|
78
|
-
const snapshot = await
|
|
79
|
+
const snapshot = await readWorkspaceRawFile(config.workspaceRoot, path);
|
|
79
80
|
res.type(snapshot.mimeType).send(snapshot.content);
|
|
80
81
|
}
|
|
81
82
|
catch (error) {
|
|
@@ -85,13 +86,26 @@ app.get("/api/workspace/raw", async (req, res, next) => {
|
|
|
85
86
|
app.get(/^\/api\/workspace\/raw-path\/(.+)$/, async (req, res, next) => {
|
|
86
87
|
try {
|
|
87
88
|
const path = decodeURIComponent(String(req.params[0] || ""));
|
|
88
|
-
const snapshot = await
|
|
89
|
+
const snapshot = await readWorkspaceRawFile(config.workspaceRoot, path);
|
|
89
90
|
res.type(snapshot.mimeType).send(snapshot.content);
|
|
90
91
|
}
|
|
91
92
|
catch (error) {
|
|
92
93
|
next(error);
|
|
93
94
|
}
|
|
94
95
|
});
|
|
96
|
+
app.post("/api/workspace/open", async (req, res, next) => {
|
|
97
|
+
try {
|
|
98
|
+
const target = await resolveWorkspaceFileForOpen(config.workspaceRoot, String(req.body?.path || ""));
|
|
99
|
+
const { command, args } = getWorkspaceOpenCommand(target.fullPath);
|
|
100
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
101
|
+
child.on("error", () => undefined);
|
|
102
|
+
child.unref();
|
|
103
|
+
res.json({ opened: true, path: target.path });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
next(error);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
95
109
|
app.get("/api/research-graph", async (_req, res, next) => {
|
|
96
110
|
try {
|
|
97
111
|
res.json(await loadResearchGraphManifest(config.workspaceRoot, config.graphManifestPath));
|