create-interview-cockpit 0.24.0 → 0.26.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/package.json +1 -1
- package/template/client/src/api.ts +26 -4
- package/template/client/src/components/ChatView.tsx +24 -3
- package/template/client/src/components/CodeContextPanel.tsx +242 -60
- package/template/client/src/components/FileViewerModal.tsx +209 -49
- package/template/client/src/components/GitDiffPanel.tsx +403 -73
- package/template/client/src/components/GitDiffViewerModal.tsx +2 -1
- package/template/client/src/components/MarkdownRenderer.tsx +8 -1
- package/template/client/src/store.ts +17 -2
- package/template/client/src/types.ts +8 -0
- package/template/cockpit.json +1 -1
- package/template/server/src/index.ts +1739 -185
- package/template/server/src/storage.ts +2 -0
package/package.json
CHANGED
|
@@ -740,15 +740,26 @@ export async function streamFrontendLabAsk(
|
|
|
740
740
|
|
|
741
741
|
// --- Code Context ---
|
|
742
742
|
|
|
743
|
-
export
|
|
743
|
+
export interface CodeContextTreeResponse {
|
|
744
|
+
roots: import("./types").CodeContextRoot[];
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export async function fetchCodeContextTree(): Promise<CodeContextTreeResponse> {
|
|
744
748
|
const res = await fetch(`${BASE}/code-context/tree`);
|
|
745
|
-
|
|
749
|
+
const data = await res.json();
|
|
750
|
+
if (Array.isArray(data)) {
|
|
751
|
+
return { roots: [{ id: "default", name: "Code Context", files: data }] };
|
|
752
|
+
}
|
|
753
|
+
return { roots: Array.isArray(data?.roots) ? data.roots : [] };
|
|
746
754
|
}
|
|
747
755
|
|
|
748
756
|
// --- Git Diff Context ---
|
|
749
757
|
|
|
750
758
|
export interface GitBranchesResponse {
|
|
751
759
|
enabled: boolean;
|
|
760
|
+
rootId?: string;
|
|
761
|
+
rootName?: string;
|
|
762
|
+
roots?: Array<{ id: string; name: string; path?: string }>;
|
|
752
763
|
head?: string;
|
|
753
764
|
defaultBranch?: string;
|
|
754
765
|
branches: string[];
|
|
@@ -757,6 +768,8 @@ export interface GitBranchesResponse {
|
|
|
757
768
|
}
|
|
758
769
|
|
|
759
770
|
export interface GitDiffTreeResponse {
|
|
771
|
+
rootId?: string;
|
|
772
|
+
rootName?: string;
|
|
760
773
|
base: string;
|
|
761
774
|
baseSha?: string;
|
|
762
775
|
head: string;
|
|
@@ -773,12 +786,18 @@ export interface GitDiffTreeResponse {
|
|
|
773
786
|
}>;
|
|
774
787
|
}
|
|
775
788
|
|
|
776
|
-
export async function fetchGitBranches(
|
|
777
|
-
|
|
789
|
+
export async function fetchGitBranches(params?: {
|
|
790
|
+
rootId?: string;
|
|
791
|
+
}): Promise<GitBranchesResponse> {
|
|
792
|
+
const qs = new URLSearchParams();
|
|
793
|
+
if (params?.rootId) qs.set("root", params.rootId);
|
|
794
|
+
const suffix = qs.toString() ? `?${qs}` : "";
|
|
795
|
+
const res = await fetch(`${BASE}/code-context/git/branches${suffix}`);
|
|
778
796
|
return res.json();
|
|
779
797
|
}
|
|
780
798
|
|
|
781
799
|
export async function fetchGitDiffTree(params: {
|
|
800
|
+
rootId?: string;
|
|
782
801
|
base: string;
|
|
783
802
|
head: string;
|
|
784
803
|
mode: "two-dot" | "three-dot" | "working-tree";
|
|
@@ -788,12 +807,14 @@ export async function fetchGitDiffTree(params: {
|
|
|
788
807
|
head: params.head,
|
|
789
808
|
mode: params.mode,
|
|
790
809
|
});
|
|
810
|
+
if (params.rootId) qs.set("root", params.rootId);
|
|
791
811
|
const res = await fetch(`${BASE}/code-context/git/diff-tree?${qs}`);
|
|
792
812
|
if (!res.ok) throw new Error((await res.json()).error || "diff-tree failed");
|
|
793
813
|
return res.json();
|
|
794
814
|
}
|
|
795
815
|
|
|
796
816
|
export async function fetchGitDiffFile(params: {
|
|
817
|
+
rootId?: string;
|
|
797
818
|
base: string;
|
|
798
819
|
head: string;
|
|
799
820
|
mode: "two-dot" | "three-dot" | "working-tree";
|
|
@@ -807,6 +828,7 @@ export async function fetchGitDiffFile(params: {
|
|
|
807
828
|
path: params.path,
|
|
808
829
|
view: params.view,
|
|
809
830
|
});
|
|
831
|
+
if (params.rootId) qs.set("root", params.rootId);
|
|
810
832
|
const res = await fetch(`${BASE}/code-context/git/diff-file?${qs}`);
|
|
811
833
|
if (!res.ok) throw new Error((await res.json()).error || "diff-file failed");
|
|
812
834
|
return res.json();
|
|
@@ -1103,8 +1103,29 @@ function ContextSummary({
|
|
|
1103
1103
|
questionFileCount: number;
|
|
1104
1104
|
codeContextFiles: string[];
|
|
1105
1105
|
}) {
|
|
1106
|
-
const { openFileViewer } = useStore();
|
|
1106
|
+
const { openFileViewer, codeContextRoots } = useStore();
|
|
1107
1107
|
const [expanded, setExpanded] = useState(false);
|
|
1108
|
+
const formatCodeFileLabel = (fileId: string) => {
|
|
1109
|
+
const sep = "::";
|
|
1110
|
+
const idx = fileId.indexOf(sep);
|
|
1111
|
+
const rootId = idx === -1 ? null : fileId.slice(0, idx);
|
|
1112
|
+
const relativePath = idx === -1 ? fileId : fileId.slice(idx + sep.length);
|
|
1113
|
+
const root = rootId
|
|
1114
|
+
? codeContextRoots.find((candidate) => candidate.id === rootId)
|
|
1115
|
+
: codeContextRoots[0];
|
|
1116
|
+
const fileName = relativePath.split("/").pop() ?? relativePath;
|
|
1117
|
+
return root ? `${root.name} › ${fileName}` : fileName;
|
|
1118
|
+
};
|
|
1119
|
+
const formatCodeFileTitle = (fileId: string) => {
|
|
1120
|
+
const sep = "::";
|
|
1121
|
+
const idx = fileId.indexOf(sep);
|
|
1122
|
+
const rootId = idx === -1 ? null : fileId.slice(0, idx);
|
|
1123
|
+
const relativePath = idx === -1 ? fileId : fileId.slice(idx + sep.length);
|
|
1124
|
+
const root = rootId
|
|
1125
|
+
? codeContextRoots.find((candidate) => candidate.id === rootId)
|
|
1126
|
+
: codeContextRoots[0];
|
|
1127
|
+
return root ? `${root.name}/${relativePath}` : relativePath;
|
|
1128
|
+
};
|
|
1108
1129
|
const VISIBLE_COUNT = 2;
|
|
1109
1130
|
const visibleFiles = expanded
|
|
1110
1131
|
? codeContextFiles
|
|
@@ -1128,10 +1149,10 @@ function ContextSummary({
|
|
|
1128
1149
|
<button
|
|
1129
1150
|
key={f}
|
|
1130
1151
|
onClick={() => openFileViewer(f)}
|
|
1131
|
-
title={f}
|
|
1152
|
+
title={formatCodeFileTitle(f)}
|
|
1132
1153
|
className="bg-slate-800 hover:bg-slate-700 hover:text-slate-300 px-1.5 py-0.5 rounded transition-colors cursor-pointer"
|
|
1133
1154
|
>
|
|
1134
|
-
{f
|
|
1155
|
+
{formatCodeFileLabel(f)}
|
|
1135
1156
|
</button>
|
|
1136
1157
|
))}
|
|
1137
1158
|
{!expanded && hiddenCount > 0 && (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { useStore } from "../store";
|
|
3
|
+
import type { CodeContextRoot } from "../types";
|
|
3
4
|
import FileViewerModal from "./FileViewerModal";
|
|
4
5
|
import NotesModal, { notesKey } from "./NotesModal";
|
|
5
6
|
import GitDiffPanel from "./GitDiffPanel";
|
|
@@ -35,6 +36,55 @@ interface TreeNode {
|
|
|
35
36
|
files: string[]; // full file paths that are direct children
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
const CODE_CONTEXT_ROOT_SEPARATOR = "::";
|
|
40
|
+
|
|
41
|
+
function codeContextFileId(rootId: string, filePath: string): string {
|
|
42
|
+
return `${rootId}${CODE_CONTEXT_ROOT_SEPARATOR}${filePath}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseCodeContextFileId(fileId: string) {
|
|
46
|
+
const idx = fileId.indexOf(CODE_CONTEXT_ROOT_SEPARATOR);
|
|
47
|
+
if (idx === -1) return { rootId: null, relativePath: fileId };
|
|
48
|
+
return {
|
|
49
|
+
rootId: fileId.slice(0, idx),
|
|
50
|
+
relativePath: fileId.slice(idx + CODE_CONTEXT_ROOT_SEPARATOR.length),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeSelectedCodeFiles(
|
|
55
|
+
files: string[],
|
|
56
|
+
roots: CodeContextRoot[],
|
|
57
|
+
): string[] {
|
|
58
|
+
if (roots.length === 0) return files;
|
|
59
|
+
const rootIds = new Set(roots.map((root) => root.id));
|
|
60
|
+
const primaryRoot = roots[0];
|
|
61
|
+
return files.map((file) => {
|
|
62
|
+
const parsed = parseCodeContextFileId(file);
|
|
63
|
+
if (parsed.rootId && rootIds.has(parsed.rootId)) return file;
|
|
64
|
+
if (primaryRoot?.files.includes(file)) {
|
|
65
|
+
return codeContextFileId(primaryRoot.id, file);
|
|
66
|
+
}
|
|
67
|
+
return file;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function codeContextDisplayPath(fileId: string, roots: CodeContextRoot[]) {
|
|
72
|
+
const parsed = parseCodeContextFileId(fileId);
|
|
73
|
+
const root = parsed.rootId
|
|
74
|
+
? roots.find((candidate) => candidate.id === parsed.rootId)
|
|
75
|
+
: roots[0];
|
|
76
|
+
return root ? `${root.name}/${parsed.relativePath}` : parsed.relativePath;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function codeContextShortLabel(fileId: string, roots: CodeContextRoot[]) {
|
|
80
|
+
const parsed = parseCodeContextFileId(fileId);
|
|
81
|
+
const root = parsed.rootId
|
|
82
|
+
? roots.find((candidate) => candidate.id === parsed.rootId)
|
|
83
|
+
: roots[0];
|
|
84
|
+
const fileName = parsed.relativePath.split("/").pop() ?? parsed.relativePath;
|
|
85
|
+
return root ? `${root.name} › ${fileName}` : fileName;
|
|
86
|
+
}
|
|
87
|
+
|
|
38
88
|
function buildTree(paths: string[]): TreeNode {
|
|
39
89
|
const root: TreeNode = { name: "", path: "", children: [], files: [] };
|
|
40
90
|
|
|
@@ -70,6 +120,7 @@ function getAllFiles(node: TreeNode): string[] {
|
|
|
70
120
|
// ─── Folder node component ──────────────────────────────
|
|
71
121
|
|
|
72
122
|
interface FolderNodeProps {
|
|
123
|
+
rootId: string;
|
|
73
124
|
node: TreeNode;
|
|
74
125
|
selectedFiles: string[];
|
|
75
126
|
onToggleFile: (path: string) => void;
|
|
@@ -82,6 +133,7 @@ interface FolderNodeProps {
|
|
|
82
133
|
}
|
|
83
134
|
|
|
84
135
|
function FolderNode({
|
|
136
|
+
rootId,
|
|
85
137
|
node,
|
|
86
138
|
selectedFiles,
|
|
87
139
|
onToggleFile,
|
|
@@ -93,10 +145,12 @@ function FolderNode({
|
|
|
93
145
|
onOpenFile,
|
|
94
146
|
}: FolderNodeProps) {
|
|
95
147
|
const allFiles = getAllFiles(node);
|
|
148
|
+
const allFileIds = allFiles.map((file) => codeContextFileId(rootId, file));
|
|
96
149
|
const selectedCount = allFiles.filter((f) =>
|
|
97
|
-
selectedFiles.includes(f),
|
|
150
|
+
selectedFiles.includes(codeContextFileId(rootId, f)),
|
|
98
151
|
).length;
|
|
99
|
-
const
|
|
152
|
+
const folderKey = codeContextFileId(rootId, node.path);
|
|
153
|
+
const isExpanded = expandedFolders.has(folderKey);
|
|
100
154
|
|
|
101
155
|
// Filter: if searching, only show nodes that have matching files
|
|
102
156
|
const matchingFiles = filter
|
|
@@ -130,7 +184,7 @@ function FolderNode({
|
|
|
130
184
|
style={{ paddingLeft: `${depth * 12 + 4}px` }}
|
|
131
185
|
>
|
|
132
186
|
<button
|
|
133
|
-
onClick={() => onToggleExpand(
|
|
187
|
+
onClick={() => onToggleExpand(folderKey)}
|
|
134
188
|
className="shrink-0 text-slate-500 hover:text-slate-300"
|
|
135
189
|
>
|
|
136
190
|
{isExpanded ? (
|
|
@@ -140,7 +194,7 @@ function FolderNode({
|
|
|
140
194
|
)}
|
|
141
195
|
</button>
|
|
142
196
|
<button
|
|
143
|
-
onClick={() => onToggleFolder(
|
|
197
|
+
onClick={() => onToggleFolder(allFileIds)}
|
|
144
198
|
className={`shrink-0 ${checkColor} hover:text-cyan-300`}
|
|
145
199
|
>
|
|
146
200
|
<CheckIcon className="w-3.5 h-3.5" />
|
|
@@ -151,7 +205,7 @@ function FolderNode({
|
|
|
151
205
|
<Folder className="w-3 h-3 text-amber-500/70 shrink-0" />
|
|
152
206
|
)}
|
|
153
207
|
<button
|
|
154
|
-
onClick={() => onToggleExpand(
|
|
208
|
+
onClick={() => onToggleExpand(folderKey)}
|
|
155
209
|
className="text-xs text-slate-400 hover:text-slate-200 truncate text-left flex-1"
|
|
156
210
|
>
|
|
157
211
|
{node.name}
|
|
@@ -170,6 +224,7 @@ function FolderNode({
|
|
|
170
224
|
.map((child) => (
|
|
171
225
|
<FolderNode
|
|
172
226
|
key={child.path}
|
|
227
|
+
rootId={rootId}
|
|
173
228
|
node={child}
|
|
174
229
|
selectedFiles={selectedFiles}
|
|
175
230
|
onToggleFile={onToggleFile}
|
|
@@ -184,7 +239,8 @@ function FolderNode({
|
|
|
184
239
|
{matchingFiles
|
|
185
240
|
.sort((a, b) => a.localeCompare(b))
|
|
186
241
|
.map((filePath) => {
|
|
187
|
-
const
|
|
242
|
+
const fileId = codeContextFileId(rootId, filePath);
|
|
243
|
+
const isSelected = selectedFiles.includes(fileId);
|
|
188
244
|
const fileName = filePath.split("/").pop()!;
|
|
189
245
|
return (
|
|
190
246
|
<div
|
|
@@ -195,14 +251,164 @@ function FolderNode({
|
|
|
195
251
|
style={{ paddingLeft: `${(depth + 1) * 12 + 4 + 14}px` }}
|
|
196
252
|
>
|
|
197
253
|
<button
|
|
198
|
-
onClick={() => onToggleFile(
|
|
254
|
+
onClick={() => onToggleFile(fileId)}
|
|
199
255
|
className="flex items-center gap-1 flex-1 min-w-0 hover:text-slate-300 transition-colors text-xs text-left"
|
|
200
256
|
>
|
|
201
257
|
<File className="w-3 h-3 shrink-0" />
|
|
202
258
|
<span className="truncate">{fileName}</span>
|
|
203
259
|
</button>
|
|
204
260
|
<button
|
|
205
|
-
onClick={() => onOpenFile(
|
|
261
|
+
onClick={() => onOpenFile(fileId)}
|
|
262
|
+
className="shrink-0 mr-1 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
263
|
+
title="View file"
|
|
264
|
+
>
|
|
265
|
+
<Eye className="w-3 h-3" />
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
})}
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface CodeContextRootNodeProps {
|
|
277
|
+
root: CodeContextRoot;
|
|
278
|
+
selectedFiles: string[];
|
|
279
|
+
onToggleFile: (path: string) => void;
|
|
280
|
+
onToggleFolder: (paths: string[]) => void;
|
|
281
|
+
expandedFolders: Set<string>;
|
|
282
|
+
onToggleExpand: (path: string) => void;
|
|
283
|
+
filter: string;
|
|
284
|
+
onOpenFile: (path: string) => void;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function CodeContextRootNode({
|
|
288
|
+
root,
|
|
289
|
+
selectedFiles,
|
|
290
|
+
onToggleFile,
|
|
291
|
+
onToggleFolder,
|
|
292
|
+
expandedFolders,
|
|
293
|
+
onToggleExpand,
|
|
294
|
+
filter,
|
|
295
|
+
onOpenFile,
|
|
296
|
+
}: CodeContextRootNodeProps) {
|
|
297
|
+
const tree = buildTree(root.files);
|
|
298
|
+
const rootKey = `root${CODE_CONTEXT_ROOT_SEPARATOR}${root.id}`;
|
|
299
|
+
const rootFileIds = root.files.map((file) =>
|
|
300
|
+
codeContextFileId(root.id, file),
|
|
301
|
+
);
|
|
302
|
+
const selectedCount = rootFileIds.filter((fileId) =>
|
|
303
|
+
selectedFiles.includes(fileId),
|
|
304
|
+
).length;
|
|
305
|
+
const rootMatches = filter ? root.name.toLowerCase().includes(filter) : false;
|
|
306
|
+
const effectiveFilter = rootMatches ? "" : filter;
|
|
307
|
+
const hasMatchingDescendants = filter
|
|
308
|
+
? rootMatches ||
|
|
309
|
+
root.files.some((file) => file.toLowerCase().includes(filter))
|
|
310
|
+
: true;
|
|
311
|
+
if (filter && !hasMatchingDescendants) return null;
|
|
312
|
+
|
|
313
|
+
const isExpanded = rootMatches || expandedFolders.has(rootKey);
|
|
314
|
+
const checkState: "none" | "some" | "all" =
|
|
315
|
+
selectedCount === 0 || rootFileIds.length === 0
|
|
316
|
+
? "none"
|
|
317
|
+
: selectedCount === rootFileIds.length
|
|
318
|
+
? "all"
|
|
319
|
+
: "some";
|
|
320
|
+
const CheckIcon =
|
|
321
|
+
checkState === "all"
|
|
322
|
+
? CheckSquare
|
|
323
|
+
: checkState === "some"
|
|
324
|
+
? MinusSquare
|
|
325
|
+
: Square;
|
|
326
|
+
const checkColor = checkState === "none" ? "text-slate-600" : "text-cyan-400";
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div className="border-b border-slate-800/60 last:border-b-0">
|
|
330
|
+
<div className="flex items-center gap-0.5 py-1 cursor-pointer group px-1">
|
|
331
|
+
<button
|
|
332
|
+
onClick={() => onToggleExpand(rootKey)}
|
|
333
|
+
className="shrink-0 text-slate-500 hover:text-slate-300"
|
|
334
|
+
>
|
|
335
|
+
{isExpanded ? (
|
|
336
|
+
<ChevronDown className="w-3 h-3" />
|
|
337
|
+
) : (
|
|
338
|
+
<ChevronRight className="w-3 h-3" />
|
|
339
|
+
)}
|
|
340
|
+
</button>
|
|
341
|
+
<button
|
|
342
|
+
onClick={() => onToggleFolder(rootFileIds)}
|
|
343
|
+
className={`shrink-0 ${checkColor} hover:text-cyan-300`}
|
|
344
|
+
title={`Select all files in ${root.name}`}
|
|
345
|
+
>
|
|
346
|
+
<CheckIcon className="w-3.5 h-3.5" />
|
|
347
|
+
</button>
|
|
348
|
+
{isExpanded ? (
|
|
349
|
+
<FolderOpen className="w-3 h-3 text-cyan-500/70 shrink-0" />
|
|
350
|
+
) : (
|
|
351
|
+
<Folder className="w-3 h-3 text-cyan-500/70 shrink-0" />
|
|
352
|
+
)}
|
|
353
|
+
<button
|
|
354
|
+
onClick={() => onToggleExpand(rootKey)}
|
|
355
|
+
className="text-xs font-semibold text-slate-300 hover:text-slate-100 truncate text-left flex-1"
|
|
356
|
+
title={root.name}
|
|
357
|
+
>
|
|
358
|
+
{root.name}
|
|
359
|
+
</button>
|
|
360
|
+
<span className="text-[10px] text-slate-600 shrink-0 mr-1">
|
|
361
|
+
{selectedCount > 0 ? `${selectedCount}/` : ""}
|
|
362
|
+
{root.files.length}
|
|
363
|
+
</span>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
{isExpanded && (
|
|
367
|
+
<div className="pb-1">
|
|
368
|
+
{tree.children
|
|
369
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
370
|
+
.map((child) => (
|
|
371
|
+
<FolderNode
|
|
372
|
+
key={`${root.id}:${child.path}`}
|
|
373
|
+
rootId={root.id}
|
|
374
|
+
node={child}
|
|
375
|
+
selectedFiles={selectedFiles}
|
|
376
|
+
onToggleFile={onToggleFile}
|
|
377
|
+
onToggleFolder={onToggleFolder}
|
|
378
|
+
expandedFolders={expandedFolders}
|
|
379
|
+
onToggleExpand={onToggleExpand}
|
|
380
|
+
depth={1}
|
|
381
|
+
filter={effectiveFilter}
|
|
382
|
+
onOpenFile={onOpenFile}
|
|
383
|
+
/>
|
|
384
|
+
))}
|
|
385
|
+
{tree.files
|
|
386
|
+
.filter(
|
|
387
|
+
(file) =>
|
|
388
|
+
!effectiveFilter ||
|
|
389
|
+
file.toLowerCase().includes(effectiveFilter),
|
|
390
|
+
)
|
|
391
|
+
.sort((a, b) => a.localeCompare(b))
|
|
392
|
+
.map((filePath) => {
|
|
393
|
+
const fileId = codeContextFileId(root.id, filePath);
|
|
394
|
+
const isSelected = selectedFiles.includes(fileId);
|
|
395
|
+
return (
|
|
396
|
+
<div
|
|
397
|
+
key={fileId}
|
|
398
|
+
className={`flex items-center py-0.5 group ${
|
|
399
|
+
isSelected ? "text-cyan-400" : "text-slate-500"
|
|
400
|
+
}`}
|
|
401
|
+
style={{ paddingLeft: 30 }}
|
|
402
|
+
>
|
|
403
|
+
<button
|
|
404
|
+
onClick={() => onToggleFile(fileId)}
|
|
405
|
+
className="flex items-center gap-1.5 flex-1 min-w-0 hover:text-slate-300 transition-colors text-xs text-left"
|
|
406
|
+
>
|
|
407
|
+
<File className="w-3 h-3 shrink-0" />
|
|
408
|
+
<span className="truncate">{filePath}</span>
|
|
409
|
+
</button>
|
|
410
|
+
<button
|
|
411
|
+
onClick={() => onOpenFile(fileId)}
|
|
206
412
|
className="shrink-0 mr-1 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
207
413
|
title="View file"
|
|
208
414
|
>
|
|
@@ -222,6 +428,7 @@ function FolderNode({
|
|
|
222
428
|
export default function CodeContextPanel() {
|
|
223
429
|
const {
|
|
224
430
|
availableFiles,
|
|
431
|
+
codeContextRoots,
|
|
225
432
|
currentQuestion,
|
|
226
433
|
fetchAvailableFiles,
|
|
227
434
|
updateCodeContext,
|
|
@@ -263,10 +470,17 @@ export default function CodeContextPanel() {
|
|
|
263
470
|
}, []);
|
|
264
471
|
|
|
265
472
|
useEffect(() => {
|
|
266
|
-
setSelectedFiles(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
473
|
+
setSelectedFiles(
|
|
474
|
+
normalizeSelectedCodeFiles(
|
|
475
|
+
currentQuestion?.codeContextFiles || [],
|
|
476
|
+
codeContextRoots,
|
|
477
|
+
),
|
|
478
|
+
);
|
|
479
|
+
}, [
|
|
480
|
+
currentQuestion?.id,
|
|
481
|
+
currentQuestion?.codeContextFiles,
|
|
482
|
+
codeContextRoots,
|
|
483
|
+
]);
|
|
270
484
|
|
|
271
485
|
const toggleFile = useCallback(
|
|
272
486
|
(filePath: string) => {
|
|
@@ -432,9 +646,9 @@ export default function CodeContextPanel() {
|
|
|
432
646
|
<button
|
|
433
647
|
onClick={() => setViewingFile(f)}
|
|
434
648
|
className="truncate flex-1 text-left hover:underline"
|
|
435
|
-
title=
|
|
649
|
+
title={codeContextDisplayPath(f, codeContextRoots)}
|
|
436
650
|
>
|
|
437
|
-
{f
|
|
651
|
+
{codeContextShortLabel(f, codeContextRoots)}
|
|
438
652
|
</button>
|
|
439
653
|
<button
|
|
440
654
|
onClick={() => toggleFile(f)}
|
|
@@ -457,59 +671,27 @@ export default function CodeContextPanel() {
|
|
|
457
671
|
) : availableFiles.length === 0 ? (
|
|
458
672
|
<div className="p-3 text-center">
|
|
459
673
|
<p className="text-xs text-slate-600">
|
|
460
|
-
Set CODE_CONTEXT_DIR in .env
|
|
674
|
+
Set CODE_CONTEXT_DIR or CODE_CONTEXT_DIRS in .env
|
|
461
675
|
</p>
|
|
462
676
|
<p className="text-xs text-slate-700 mt-1">
|
|
463
677
|
to browse project files
|
|
464
678
|
</p>
|
|
465
679
|
</div>
|
|
466
680
|
) : (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
onOpenFile={setViewingFile}
|
|
481
|
-
/>
|
|
482
|
-
))
|
|
681
|
+
codeContextRoots.map((root) => (
|
|
682
|
+
<CodeContextRootNode
|
|
683
|
+
key={root.id}
|
|
684
|
+
root={root}
|
|
685
|
+
selectedFiles={selectedFiles}
|
|
686
|
+
onToggleFile={toggleFile}
|
|
687
|
+
onToggleFolder={toggleFolder}
|
|
688
|
+
expandedFolders={expandedFolders}
|
|
689
|
+
onToggleExpand={toggleExpand}
|
|
690
|
+
filter={filter}
|
|
691
|
+
onOpenFile={setViewingFile}
|
|
692
|
+
/>
|
|
693
|
+
))
|
|
483
694
|
)}
|
|
484
|
-
{/* Root-level files (if any) */}
|
|
485
|
-
{tree.files
|
|
486
|
-
.filter((f) => !filter || f.toLowerCase().includes(filter))
|
|
487
|
-
.map((filePath) => {
|
|
488
|
-
const isSelected = selectedFiles.includes(filePath);
|
|
489
|
-
return (
|
|
490
|
-
<div
|
|
491
|
-
key={filePath}
|
|
492
|
-
className={`flex items-center px-4 py-0.5 group ${
|
|
493
|
-
isSelected ? "text-cyan-400" : "text-slate-500"
|
|
494
|
-
}`}
|
|
495
|
-
>
|
|
496
|
-
<button
|
|
497
|
-
onClick={() => toggleFile(filePath)}
|
|
498
|
-
className="flex items-center gap-1.5 flex-1 min-w-0 hover:text-slate-300 transition-colors text-xs text-left"
|
|
499
|
-
>
|
|
500
|
-
<File className="w-3 h-3 shrink-0" />
|
|
501
|
-
<span className="truncate">{filePath}</span>
|
|
502
|
-
</button>
|
|
503
|
-
<button
|
|
504
|
-
onClick={() => setViewingFile(filePath)}
|
|
505
|
-
className="shrink-0 mr-1 p-0.5 rounded opacity-0 group-hover:opacity-100 text-slate-500 hover:text-cyan-400 transition-all"
|
|
506
|
-
title="View file"
|
|
507
|
-
>
|
|
508
|
-
<Eye className="w-3 h-3" />
|
|
509
|
-
</button>
|
|
510
|
-
</div>
|
|
511
|
-
);
|
|
512
|
-
})}
|
|
513
695
|
</div>
|
|
514
696
|
|
|
515
697
|
{/* ── My Code section ─────────────────────────────────── */}
|