agent-canvas 0.2.1 → 0.7.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.
Files changed (99) hide show
  1. package/README.md +42 -17
  2. package/bin/agent-canvas.js +7 -7
  3. package/bin/agent-canvas.ts +48 -37
  4. package/bin/lib/commands/daemon.ts +46 -43
  5. package/bin/lib/commands/exec.ts +100 -100
  6. package/bin/lib/commands/fetch.ts +15 -15
  7. package/bin/lib/commands/install.ts +36 -36
  8. package/bin/lib/commands/instructions.ts +51 -51
  9. package/bin/lib/commands/push.ts +115 -97
  10. package/bin/lib/commands/review.ts +190 -0
  11. package/bin/lib/commands/watch.ts +24 -24
  12. package/bin/lib/config.ts +12 -12
  13. package/bin/lib/daemon-lifecycle.ts +47 -51
  14. package/bin/lib/helpers.ts +82 -76
  15. package/bin/lib/instructions/app-review.md +88 -0
  16. package/bin/lib/instructions/component-checklist.md +7 -5
  17. package/bin/lib/instructions/component-custom.md +20 -14
  18. package/bin/lib/instructions/component-diff.md +4 -3
  19. package/bin/lib/instructions/component-item.md +18 -8
  20. package/bin/lib/instructions/component-markdown.md +4 -2
  21. package/bin/lib/instructions/component-mermaid.md +12 -6
  22. package/bin/lib/instructions/component-multichoice.md +6 -2
  23. package/bin/lib/instructions/component-note.md +1 -1
  24. package/bin/lib/instructions/component-priority.md +1 -1
  25. package/bin/lib/instructions/component-rangeinput.md +9 -2
  26. package/bin/lib/instructions/component-secretinput.md +12 -11
  27. package/bin/lib/instructions/component-svg.md +62 -16
  28. package/bin/lib/instructions/component-table.md +6 -6
  29. package/bin/lib/instructions/component-usefeedback.md +8 -8
  30. package/bin/lib/instructions/component-userinput.md +6 -2
  31. package/bin/lib/instructions/flow-feature.md +22 -5
  32. package/bin/lib/instructions/flow-plan.md +5 -5
  33. package/bin/lib/instructions/flow-runbook.md +28 -25
  34. package/bin/lib/instructions/overview.md +27 -18
  35. package/bin/lib/instructions/theming.md +5 -0
  36. package/daemon/build.ts +171 -152
  37. package/daemon/client/App.tsx +1205 -1077
  38. package/daemon/client/CompareView.tsx +478 -458
  39. package/daemon/client/FileBrowser.tsx +241 -203
  40. package/daemon/client/FileViewer.tsx +220 -211
  41. package/daemon/client/OverviewView.tsx +283 -267
  42. package/daemon/client/ReviewerIdentityDialog.tsx +72 -71
  43. package/daemon/client/RevisionSelect.tsx +111 -95
  44. package/daemon/client/SessionSwitcher.tsx +81 -81
  45. package/daemon/client/ShareDialog.tsx +211 -212
  46. package/daemon/client/apiSchemas.ts +78 -0
  47. package/daemon/client/canvas-runtime.ts +1 -1
  48. package/daemon/client/clientApi.ts +194 -206
  49. package/daemon/client/components/SecretInput.tsx +138 -138
  50. package/daemon/client/components/index.ts +2 -2
  51. package/daemon/client/domDiff.ts +341 -333
  52. package/daemon/client/errorMessage.ts +3 -0
  53. package/daemon/client/exportMarkdown.ts +222 -201
  54. package/daemon/client/lcs.ts +19 -14
  55. package/daemon/client/shareCrypto.ts +57 -57
  56. package/daemon/client/unifiedDiff.ts +660 -649
  57. package/daemon/dist/annotations-runtime.js +1 -1
  58. package/daemon/dist/client.css +2 -2
  59. package/daemon/dist/client.js +77 -80
  60. package/daemon/dist/components.js +59 -59
  61. package/daemon/dist/preact-compat.js +1 -1
  62. package/daemon/dist/review.js +361 -0
  63. package/daemon/dist/runtime.js +1 -1
  64. package/daemon/langMap.ts +24 -6
  65. package/daemon/review-widget/anchor.ts +187 -0
  66. package/daemon/review-widget/client.ts +95 -0
  67. package/daemon/review-widget/index.tsx +36 -0
  68. package/daemon/review-widget/markup.ts +71 -0
  69. package/daemon/review-widget/screenshot.ts +91 -0
  70. package/daemon/review-widget/source.ts +103 -0
  71. package/daemon/review-widget/styles.css +359 -0
  72. package/daemon/review-widget/text-modules.d.ts +4 -0
  73. package/daemon/review-widget/tsconfig.json +16 -0
  74. package/daemon/review-widget/widget.tsx +436 -0
  75. package/daemon/src/components.ts +3 -3
  76. package/daemon/src/handlers/api.ts +330 -328
  77. package/daemon/src/handlers/files.ts +112 -106
  78. package/daemon/src/handlers/static.ts +47 -46
  79. package/daemon/src/handlers/uploads.ts +61 -56
  80. package/daemon/src/paths.ts +13 -10
  81. package/daemon/src/remote-feedback.ts +192 -192
  82. package/daemon/src/review/markdown.ts +52 -0
  83. package/daemon/src/review/model.ts +132 -0
  84. package/daemon/src/review/proxy.ts +177 -0
  85. package/daemon/src/review/server.ts +199 -0
  86. package/daemon/src/review/store.ts +151 -0
  87. package/daemon/src/server.ts +127 -86
  88. package/daemon/src/share.ts +163 -169
  89. package/daemon/src/stubFetch.ts +7 -0
  90. package/daemon/tsconfig.json +19 -19
  91. package/package.json +47 -47
  92. package/skills/canvas/SKILL.md +5 -1
  93. package/bin/lib/commands/exec.test.ts +0 -27
  94. package/bin/lib/helpers.test.ts +0 -79
  95. package/daemon/src/files-api.test.ts +0 -78
  96. package/daemon/src/remote-feedback.test.ts +0 -165
  97. package/daemon/src/secrets-api.test.ts +0 -123
  98. package/daemon/src/share-e2e.test.ts +0 -277
  99. package/daemon/src/share.test.ts +0 -289
@@ -1,206 +1,244 @@
1
- import React, { useEffect, useState, useContext, useCallback, useMemo } from "react";
2
- import { SessionContext } from "#canvas/runtime";
3
- import { useAnnotations } from "@fabrika/canvas-kernel/client";
4
- import { ActiveViewContext } from "@fabrika/canvas-kernel/client";
5
- import { FileIcon } from "@fabrika/canvas-kernel/client";
6
- import { RESPONSE_ANNOTATION_PATH } from "@fabrika/annotations";
7
- import { FS_AVAILABLE } from "./clientApi";
8
-
9
- interface TreeEntry { name: string; type: "file" | "dir"; }
10
- interface DirState { entries: TreeEntry[]; expanded: boolean; loaded: boolean; }
1
+ import { SessionContext } from '#canvas/runtime'
2
+ import { RESPONSE_ANNOTATION_PATH } from '@fabrika/annotations'
3
+ import { filePreviewResponseSchema, useAnnotations } from '@fabrika/canvas-kernel/client'
4
+ import { ActiveViewContext } from '@fabrika/canvas-kernel/client'
5
+ import { FileIcon } from '@fabrika/canvas-kernel/client'
6
+ import React, { useContext, useEffect, useMemo, useState } from 'react'
7
+ import { treeResponseSchema } from './apiSchemas'
8
+ import { FS_AVAILABLE } from './clientApi'
9
+
10
+ interface TreeEntry {
11
+ name: string
12
+ type: 'file' | 'dir'
13
+ }
14
+ interface DirState {
15
+ entries: TreeEntry[]
16
+ expanded: boolean
17
+ loaded: boolean
18
+ }
11
19
 
12
20
  export function FileBrowser({ embedded }: { embedded?: boolean } = {}) {
13
- const sessionId = useContext(SessionContext);
14
- const { annotations, addAnnotation } = useAnnotations();
15
- const { setActiveView } = useContext(ActiveViewContext);
16
- const [collapsed, setCollapsed] = useState(false);
17
- const [dirs, setDirs] = useState<Record<string, DirState>>({});
18
-
19
- // Shared-mode canvases have no filesystem backing — the daemon's /api/tree
20
- // endpoint isn't reachable. Render a simple placeholder so reviewers see
21
- // something informative instead of a broken tree.
22
- if (!FS_AVAILABLE) {
23
- return (
24
- <div className={embedded ? "px-4 py-3" : "p-4"}>
25
- <div className="text-[11px] uppercase tracking-widest text-text-tertiary font-body mb-2">Files</div>
26
- <p className="text-[11px] text-text-tertiary font-body leading-relaxed">
27
- File browsing is not available in shared view. File previews embedded
28
- in the canvas still work.
29
- </p>
30
- </div>
31
- );
32
- }
33
-
34
- const { annCountMap, annotatedFiles } = useMemo(() => {
35
- const map = new Map<string, number>();
36
- for (const a of annotations) {
37
- if (a.filePath && a.filePath !== RESPONSE_ANNOTATION_PATH) map.set(a.filePath, (map.get(a.filePath) || 0) + 1);
38
- }
39
- return { annCountMap: map, annotatedFiles: [...map.keys()] };
40
- }, [annotations]);
41
-
42
- const loadDir = async (path: string) => {
43
- const res = await fetch(`/api/tree?session=${sessionId}&path=${encodeURIComponent(path)}`);
44
- const data = await res.json() as any;
45
- if (data.entries) setDirs((prev) => ({ ...prev, [path]: { entries: data.entries, expanded: true, loaded: true } }));
46
- };
47
-
48
- useEffect(() => { if (!collapsed && !dirs[""]) loadDir(""); }, [collapsed]);
49
-
50
- const toggleDir = (path: string) => {
51
- if (dirs[path]?.loaded) setDirs((prev) => ({ ...prev, [path]: { ...prev[path], expanded: !prev[path].expanded } }));
52
- else loadDir(path);
53
- };
54
-
55
- const openFile = (path: string) => {
56
- setActiveView({ type: "file", path });
57
- };
58
-
59
- const addWholeFile = async (path: string) => {
60
- const res = await fetch(`/api/file?session=${sessionId}&path=${encodeURIComponent(path)}`);
61
- const data = await res.json() as any;
62
- if (!data.error) {
63
- const preview = data.content.split("\n").slice(0, 3).join("\n") + (data.content.split("\n").length > 3 ? "\n..." : "");
64
- addAnnotation(preview, "Include this file for context", path);
65
- }
66
- };
67
-
68
- const addWholeDir = async (path: string) => {
69
- // Load dir if needed, then add all files
70
- let entries: TreeEntry[] = [];
71
- if (dirs[path]?.loaded) {
72
- entries = dirs[path].entries;
73
- } else {
74
- const res = await fetch(`/api/tree?session=${sessionId}&path=${encodeURIComponent(path)}`);
75
- const data = await res.json() as any;
76
- if (data.entries) entries = data.entries;
77
- }
78
- await Promise.all(
79
- entries
80
- .filter((entry) => entry.type === "file")
81
- .map((entry) => addWholeFile(path ? `${path}/${entry.name}` : entry.name))
82
- );
83
- };
84
-
85
- const getAnnCount = (path: string) => annCountMap.get(path) || 0;
86
-
87
- const renderTree = (basePath: string, depth: number = 0): React.ReactNode => {
88
- const dir = dirs[basePath];
89
- if (!dir?.expanded) return null;
90
- return dir.entries.map((entry) => {
91
- const fullPath = basePath ? `${basePath}/${entry.name}` : entry.name;
92
- const annCount = getAnnCount(fullPath);
93
-
94
- return (
95
- <div key={fullPath}>
96
- <div
97
- className="group/item flex items-center py-[3px] hover:bg-bg-elevated text-[13px] font-body cursor-default transition-colors duration-100"
98
- style={{ paddingLeft: `${16 + depth * 16}px`, paddingRight: "8px" }}
99
- >
100
- {entry.type === "dir" ? (
101
- <>
102
- <span onClick={() => toggleDir(fullPath)} className="flex items-center gap-1.5 flex-1 text-text-secondary hover:text-text-primary cursor-pointer truncate">
103
- <FileIcon name={entry.name} type="dir" expanded={dirs[fullPath]?.expanded} />
104
- <span className="truncate">{entry.name}</span>
105
- </span>
106
- <button
107
- onClick={(e) => { e.stopPropagation(); addWholeDir(fullPath); }}
108
- className="text-[11px] text-text-tertiary hover:text-text-secondary opacity-0 group-hover/item:opacity-100 transition-opacity px-1 flex-shrink-0"
109
- title="Add all files in folder"
110
- >
111
- +
112
- </button>
113
- </>
114
- ) : (
115
- <>
116
- <span
117
- onClick={() => openFile(fullPath)}
118
- className={`flex-1 truncate cursor-pointer flex items-center gap-1.5 ${annCount > 0 ? "text-text-primary" : "text-text-secondary hover:text-text-primary"}`}
119
- >
120
- <FileIcon name={entry.name} type="file" />
121
- <span className="truncate">{entry.name}</span>
122
- </span>
123
- {/* Right side: annotation badge + add button */}
124
- <div className="flex items-center gap-1 flex-shrink-0 ml-1">
125
- {annCount > 0 && (
126
- <span className="text-[10px] font-medium bg-badge-bg text-accent-amber px-1.5 py-px rounded-full min-w-[18px] text-center">
127
- {annCount}
128
- </span>
129
- )}
130
- <button
131
- onClick={(e) => { e.stopPropagation(); addWholeFile(fullPath); }}
132
- className="text-[11px] text-text-tertiary hover:text-text-secondary opacity-0 group-hover/item:opacity-100 transition-opacity px-1"
133
- title="Add file to context"
134
- >
135
- +
136
- </button>
137
- </div>
138
- </>
139
- )}
140
- </div>
141
- {entry.type === "dir" && renderTree(fullPath, depth + 1)}
142
- </div>
143
- );
144
- });
145
- };
146
-
147
- if (embedded) {
148
- return (
149
- <>
150
- <div className="flex items-center justify-between px-4 pt-3 pb-2 flex-shrink-0">
151
- <span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body">Files</span>
152
- </div>
153
- <div className="flex-1 overflow-y-auto">{renderTree("")}</div>
154
- {annotatedFiles.length > 0 && (
155
- <div className="border-t border-border-subtle px-4 py-3 flex-shrink-0">
156
- <div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body mb-1.5">Referenced</div>
157
- {annotatedFiles.map((f) => (
158
- <div key={f} onClick={() => openFile(f)} className="flex items-center justify-between text-[12px] font-body text-text-secondary hover:text-text-primary cursor-pointer py-0.5">
159
- <span className="truncate">{f.split("/").pop()}</span>
160
- <span className="text-[10px] font-medium bg-badge-bg text-accent-amber px-1.5 py-px rounded-full min-w-[18px] text-center flex-shrink-0 ml-1">
161
- {getAnnCount(f)}
162
- </span>
163
- </div>
164
- ))}
165
- </div>
166
- )}
167
- </>
168
- );
169
- }
170
-
171
- if (collapsed) {
172
- return (
173
- <div className="hidden lg:flex w-8 border-r border-border-subtle bg-bg-surface flex-col items-center flex-shrink-0">
174
- <button onClick={() => setCollapsed(false)} className="mt-3 p-1 text-text-tertiary hover:text-text-secondary transition-colors">
175
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M5 3l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
176
- </button>
177
- </div>
178
- );
179
- }
180
-
181
- return (
182
- <div className="hidden lg:flex w-60 border-r border-border-subtle bg-bg-surface flex-col flex-shrink-0 overflow-hidden">
183
- <div className="flex items-center justify-between px-4 pt-4 pb-3 flex-shrink-0">
184
- <span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body">Files</span>
185
- <button onClick={() => setCollapsed(true)} className="text-text-tertiary hover:text-text-secondary transition-colors p-0.5">
186
- <svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M9 3l-4 4 4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>
187
- </button>
188
- </div>
189
- <div className="flex-1 overflow-y-auto">{renderTree("")}</div>
190
-
191
- {annotatedFiles.length > 0 && (
192
- <div className="border-t border-border-subtle px-4 py-3 flex-shrink-0">
193
- <div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body mb-1.5">Referenced</div>
194
- {annotatedFiles.map((f) => (
195
- <div key={f} onClick={() => openFile(f)} className="flex items-center justify-between text-[12px] font-body text-text-secondary hover:text-text-primary cursor-pointer py-0.5">
196
- <span className="truncate">{f.split("/").pop()}</span>
197
- <span className="text-[10px] font-medium bg-badge-bg text-accent-amber px-1.5 py-px rounded-full min-w-[18px] text-center flex-shrink-0 ml-1">
198
- {getAnnCount(f)}
199
- </span>
200
- </div>
201
- ))}
202
- </div>
203
- )}
204
- </div>
205
- );
21
+ // Shared-mode canvases have no filesystem backing — the daemon's /api/tree
22
+ // endpoint isn't reachable. Render a simple placeholder so reviewers see
23
+ // something informative instead of a broken tree.
24
+ if (!FS_AVAILABLE) {
25
+ return (
26
+ <div className={embedded ? 'px-4 py-3' : 'p-4'}>
27
+ <div className="text-[11px] uppercase tracking-widest text-text-tertiary font-body mb-2">Files</div>
28
+ <p className="text-[11px] text-text-tertiary font-body leading-relaxed">
29
+ File browsing is not available in shared view. File previews embedded in the canvas still work.
30
+ </p>
31
+ </div>
32
+ )
33
+ }
34
+ return <LocalFileBrowser embedded={embedded} />
35
+ }
36
+
37
+ function LocalFileBrowser({ embedded }: { embedded?: boolean }) {
38
+ const sessionId = useContext(SessionContext)
39
+ const { annotations, addAnnotation } = useAnnotations()
40
+ const { setActiveView } = useContext(ActiveViewContext)
41
+ const [collapsed, setCollapsed] = useState(false)
42
+ const [dirs, setDirs] = useState<Record<string, DirState>>({})
43
+
44
+ const { annCountMap, annotatedFiles } = useMemo(() => {
45
+ const map = new Map<string, number>()
46
+ for (const a of annotations) {
47
+ if (a.filePath && a.filePath !== RESPONSE_ANNOTATION_PATH) map.set(a.filePath, (map.get(a.filePath) || 0) + 1)
48
+ }
49
+ return { annCountMap: map, annotatedFiles: [...map.keys()] }
50
+ }, [annotations])
51
+
52
+ const loadDir = async (path: string) => {
53
+ const res = await fetch(`/api/tree?session=${sessionId}&path=${encodeURIComponent(path)}`)
54
+ const data = treeResponseSchema.safeParse(await res.json())
55
+ if (data.success) setDirs((prev) => ({ ...prev, [path]: { entries: data.data.entries, expanded: true, loaded: true } }))
56
+ }
57
+
58
+ useEffect(() => {
59
+ if (!collapsed && !dirs['']) loadDir('')
60
+ }, [collapsed])
61
+
62
+ const toggleDir = (path: string) => {
63
+ if (dirs[path]?.loaded) setDirs((prev) => ({ ...prev, [path]: { ...prev[path], expanded: !prev[path].expanded } }))
64
+ else loadDir(path)
65
+ }
66
+
67
+ const openFile = (path: string) => {
68
+ setActiveView({ type: 'file', path })
69
+ }
70
+
71
+ const addWholeFile = async (path: string) => {
72
+ const res = await fetch(`/api/file?session=${sessionId}&path=${encodeURIComponent(path)}`)
73
+ const data = filePreviewResponseSchema.safeParse(await res.json())
74
+ if (data.success && 'content' in data.data) {
75
+ const { content } = data.data
76
+ const preview = content.split('\n').slice(0, 3).join('\n') + (content.split('\n').length > 3 ? '\n...' : '')
77
+ addAnnotation(preview, 'Include this file for context', path)
78
+ }
79
+ }
80
+
81
+ const addWholeDir = async (path: string) => {
82
+ // Load dir if needed, then add all files
83
+ let entries: TreeEntry[] = []
84
+ if (dirs[path]?.loaded) {
85
+ entries = dirs[path].entries
86
+ } else {
87
+ const res = await fetch(`/api/tree?session=${sessionId}&path=${encodeURIComponent(path)}`)
88
+ const data = treeResponseSchema.safeParse(await res.json())
89
+ if (data.success) entries = data.data.entries
90
+ }
91
+ await Promise.all(
92
+ entries
93
+ .filter((entry) => entry.type === 'file')
94
+ .map((entry) => addWholeFile(path ? `${path}/${entry.name}` : entry.name)),
95
+ )
96
+ }
97
+
98
+ const getAnnCount = (path: string) => annCountMap.get(path) || 0
99
+
100
+ const renderTree = (basePath: string, depth: number = 0): React.ReactNode => {
101
+ const dir = dirs[basePath]
102
+ if (!dir?.expanded) return null
103
+ return dir.entries.map((entry) => {
104
+ const fullPath = basePath ? `${basePath}/${entry.name}` : entry.name
105
+ const annCount = getAnnCount(fullPath)
106
+
107
+ return (
108
+ <div key={fullPath}>
109
+ <div
110
+ className="group/item flex items-center py-[3px] hover:bg-bg-elevated text-[13px] font-body cursor-default transition-colors duration-100"
111
+ style={{ paddingLeft: `${16 + depth * 16}px`, paddingRight: '8px' }}
112
+ >
113
+ {entry.type === 'dir'
114
+ ? (
115
+ <>
116
+ <span
117
+ onClick={() => toggleDir(fullPath)}
118
+ className="flex items-center gap-1.5 flex-1 text-text-secondary hover:text-text-primary cursor-pointer truncate"
119
+ >
120
+ <FileIcon name={entry.name} type="dir" expanded={dirs[fullPath]?.expanded} />
121
+ <span className="truncate">{entry.name}</span>
122
+ </span>
123
+ <button
124
+ onClick={(e) => {
125
+ e.stopPropagation()
126
+ addWholeDir(fullPath)
127
+ }}
128
+ className="text-[11px] text-text-tertiary hover:text-text-secondary opacity-0 group-hover/item:opacity-100 transition-opacity px-1 flex-shrink-0"
129
+ title="Add all files in folder"
130
+ >
131
+ +
132
+ </button>
133
+ </>
134
+ )
135
+ : (
136
+ <>
137
+ <span
138
+ onClick={() => openFile(fullPath)}
139
+ className={`flex-1 truncate cursor-pointer flex items-center gap-1.5 ${
140
+ annCount > 0 ? 'text-text-primary' : 'text-text-secondary hover:text-text-primary'
141
+ }`}
142
+ >
143
+ <FileIcon name={entry.name} type="file" />
144
+ <span className="truncate">{entry.name}</span>
145
+ </span>
146
+ {/* Right side: annotation badge + add button */}
147
+ <div className="flex items-center gap-1 flex-shrink-0 ml-1">
148
+ {annCount > 0 && (
149
+ <span className="text-[10px] font-medium bg-badge-bg text-accent-amber px-1.5 py-px rounded-full min-w-[18px] text-center">
150
+ {annCount}
151
+ </span>
152
+ )}
153
+ <button
154
+ onClick={(e) => {
155
+ e.stopPropagation()
156
+ addWholeFile(fullPath)
157
+ }}
158
+ className="text-[11px] text-text-tertiary hover:text-text-secondary opacity-0 group-hover/item:opacity-100 transition-opacity px-1"
159
+ title="Add file to context"
160
+ >
161
+ +
162
+ </button>
163
+ </div>
164
+ </>
165
+ )}
166
+ </div>
167
+ {entry.type === 'dir' && renderTree(fullPath, depth + 1)}
168
+ </div>
169
+ )
170
+ })
171
+ }
172
+
173
+ if (embedded) {
174
+ return (
175
+ <>
176
+ <div className="flex items-center justify-between px-4 pt-3 pb-2 flex-shrink-0">
177
+ <span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body">Files</span>
178
+ </div>
179
+ <div className="flex-1 overflow-y-auto">{renderTree('')}</div>
180
+ {annotatedFiles.length > 0 && (
181
+ <div className="border-t border-border-subtle px-4 py-3 flex-shrink-0">
182
+ <div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body mb-1.5">Referenced</div>
183
+ {annotatedFiles.map((f) => (
184
+ <div
185
+ key={f}
186
+ onClick={() => openFile(f)}
187
+ className="flex items-center justify-between text-[12px] font-body text-text-secondary hover:text-text-primary cursor-pointer py-0.5"
188
+ >
189
+ <span className="truncate">{f.split('/').pop()}</span>
190
+ <span className="text-[10px] font-medium bg-badge-bg text-accent-amber px-1.5 py-px rounded-full min-w-[18px] text-center flex-shrink-0 ml-1">
191
+ {getAnnCount(f)}
192
+ </span>
193
+ </div>
194
+ ))}
195
+ </div>
196
+ )}
197
+ </>
198
+ )
199
+ }
200
+
201
+ if (collapsed) {
202
+ return (
203
+ <div className="hidden lg:flex w-8 border-r border-border-subtle bg-bg-surface flex-col items-center flex-shrink-0">
204
+ <button onClick={() => setCollapsed(false)} className="mt-3 p-1 text-text-tertiary hover:text-text-secondary transition-colors">
205
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
206
+ <path d="M5 3l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
207
+ </svg>
208
+ </button>
209
+ </div>
210
+ )
211
+ }
212
+
213
+ return (
214
+ <div className="hidden lg:flex w-60 border-r border-border-subtle bg-bg-surface flex-col flex-shrink-0 overflow-hidden">
215
+ <div className="flex items-center justify-between px-4 pt-4 pb-3 flex-shrink-0">
216
+ <span className="text-[11px] font-medium uppercase tracking-widest text-text-tertiary font-body">Files</span>
217
+ <button onClick={() => setCollapsed(true)} className="text-text-tertiary hover:text-text-secondary transition-colors p-0.5">
218
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
219
+ <path d="M9 3l-4 4 4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
220
+ </svg>
221
+ </button>
222
+ </div>
223
+ <div className="flex-1 overflow-y-auto">{renderTree('')}</div>
224
+
225
+ {annotatedFiles.length > 0 && (
226
+ <div className="border-t border-border-subtle px-4 py-3 flex-shrink-0">
227
+ <div className="text-[10px] uppercase tracking-widest text-text-tertiary font-body mb-1.5">Referenced</div>
228
+ {annotatedFiles.map((f) => (
229
+ <div
230
+ key={f}
231
+ onClick={() => openFile(f)}
232
+ className="flex items-center justify-between text-[12px] font-body text-text-secondary hover:text-text-primary cursor-pointer py-0.5"
233
+ >
234
+ <span className="truncate">{f.split('/').pop()}</span>
235
+ <span className="text-[10px] font-medium bg-badge-bg text-accent-amber px-1.5 py-px rounded-full min-w-[18px] text-center flex-shrink-0 ml-1">
236
+ {getAnnCount(f)}
237
+ </span>
238
+ </div>
239
+ ))}
240
+ </div>
241
+ )}
242
+ </div>
243
+ )
206
244
  }