ideaco 1.1.5

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 (159) hide show
  1. package/.dockerignore +33 -0
  2. package/.nvmrc +1 -0
  3. package/ARCHITECTURE.md +394 -0
  4. package/Dockerfile +50 -0
  5. package/LICENSE +29 -0
  6. package/README.md +206 -0
  7. package/bin/i18n.js +46 -0
  8. package/bin/ideaco.js +494 -0
  9. package/deploy.sh +15 -0
  10. package/docker-compose.yml +30 -0
  11. package/electron/main.cjs +986 -0
  12. package/electron/preload.cjs +14 -0
  13. package/electron/web-backends.cjs +854 -0
  14. package/jsconfig.json +8 -0
  15. package/next.config.mjs +34 -0
  16. package/package.json +134 -0
  17. package/postcss.config.mjs +6 -0
  18. package/public/demo/dashboard.png +0 -0
  19. package/public/demo/employee.png +0 -0
  20. package/public/demo/messages.png +0 -0
  21. package/public/demo/office.png +0 -0
  22. package/public/demo/requirement.png +0 -0
  23. package/public/logo.jpeg +0 -0
  24. package/public/logo.png +0 -0
  25. package/scripts/prepare-electron.js +67 -0
  26. package/scripts/release.js +76 -0
  27. package/src/app/api/agents/[agentId]/chat/route.js +70 -0
  28. package/src/app/api/agents/[agentId]/conversations/route.js +35 -0
  29. package/src/app/api/agents/[agentId]/route.js +106 -0
  30. package/src/app/api/avatar/route.js +104 -0
  31. package/src/app/api/browse-dir/route.js +44 -0
  32. package/src/app/api/chat/route.js +265 -0
  33. package/src/app/api/company/factory-reset/route.js +43 -0
  34. package/src/app/api/company/route.js +82 -0
  35. package/src/app/api/departments/[deptId]/agents/[agentId]/dismiss/route.js +19 -0
  36. package/src/app/api/departments/route.js +92 -0
  37. package/src/app/api/group-chat-loop/events/route.js +70 -0
  38. package/src/app/api/group-chat-loop/route.js +94 -0
  39. package/src/app/api/mailbox/route.js +100 -0
  40. package/src/app/api/messages/route.js +14 -0
  41. package/src/app/api/providers/[id]/configure/route.js +21 -0
  42. package/src/app/api/providers/[id]/refresh-cookie/route.js +38 -0
  43. package/src/app/api/providers/[id]/test-cookie/route.js +28 -0
  44. package/src/app/api/providers/route.js +11 -0
  45. package/src/app/api/requirements/route.js +242 -0
  46. package/src/app/api/secretary/route.js +65 -0
  47. package/src/app/api/system/cli-backends/route.js +91 -0
  48. package/src/app/api/system/cron/route.js +110 -0
  49. package/src/app/api/system/knowledge/route.js +104 -0
  50. package/src/app/api/system/plugins/route.js +40 -0
  51. package/src/app/api/system/skills/route.js +46 -0
  52. package/src/app/api/system/status/route.js +46 -0
  53. package/src/app/api/talent-market/[profileId]/recall/route.js +22 -0
  54. package/src/app/api/talent-market/[profileId]/route.js +17 -0
  55. package/src/app/api/talent-market/route.js +26 -0
  56. package/src/app/api/teams/route.js +773 -0
  57. package/src/app/api/ws-files/[departmentId]/file/route.js +27 -0
  58. package/src/app/api/ws-files/[departmentId]/files/route.js +22 -0
  59. package/src/app/globals.css +130 -0
  60. package/src/app/layout.jsx +40 -0
  61. package/src/app/page.jsx +97 -0
  62. package/src/components/AgentChatModal.jsx +164 -0
  63. package/src/components/AgentDetailModal.jsx +425 -0
  64. package/src/components/AgentSpyModal.jsx +481 -0
  65. package/src/components/AvatarGrid.jsx +29 -0
  66. package/src/components/BossProfileModal.jsx +162 -0
  67. package/src/components/CachedAvatar.jsx +77 -0
  68. package/src/components/ChatPanel.jsx +219 -0
  69. package/src/components/ChatShared.jsx +255 -0
  70. package/src/components/DepartmentDetail.jsx +842 -0
  71. package/src/components/DepartmentView.jsx +367 -0
  72. package/src/components/FileReference.jsx +260 -0
  73. package/src/components/FilesView.jsx +465 -0
  74. package/src/components/GroupChatView.jsx +799 -0
  75. package/src/components/Mailbox.jsx +926 -0
  76. package/src/components/MessagesView.jsx +112 -0
  77. package/src/components/OnboardingGuide.jsx +209 -0
  78. package/src/components/OrgTree.jsx +151 -0
  79. package/src/components/Overview.jsx +391 -0
  80. package/src/components/PixelOffice.jsx +2281 -0
  81. package/src/components/ProviderGrid.jsx +551 -0
  82. package/src/components/ProvidersBoard.jsx +16 -0
  83. package/src/components/RequirementDetail.jsx +1279 -0
  84. package/src/components/RequirementsBoard.jsx +187 -0
  85. package/src/components/SecretarySettings.jsx +295 -0
  86. package/src/components/SetupWizard.jsx +388 -0
  87. package/src/components/Sidebar.jsx +169 -0
  88. package/src/components/SystemMonitor.jsx +808 -0
  89. package/src/components/TalentMarket.jsx +183 -0
  90. package/src/components/TeamDetail.jsx +697 -0
  91. package/src/core/agent/base-agent.js +104 -0
  92. package/src/core/agent/chat-store.js +602 -0
  93. package/src/core/agent/cli-agent/backends/claude-code/README.md +52 -0
  94. package/src/core/agent/cli-agent/backends/claude-code/config.js +27 -0
  95. package/src/core/agent/cli-agent/backends/codebuddy/README.md +236 -0
  96. package/src/core/agent/cli-agent/backends/codebuddy/config.js +27 -0
  97. package/src/core/agent/cli-agent/backends/codex/README.md +51 -0
  98. package/src/core/agent/cli-agent/backends/codex/config.js +27 -0
  99. package/src/core/agent/cli-agent/backends/index.js +27 -0
  100. package/src/core/agent/cli-agent/backends/registry.js +580 -0
  101. package/src/core/agent/cli-agent/index.js +154 -0
  102. package/src/core/agent/index.js +60 -0
  103. package/src/core/agent/llm-agent/client.js +320 -0
  104. package/src/core/agent/llm-agent/index.js +97 -0
  105. package/src/core/agent/message-bus.js +211 -0
  106. package/src/core/agent/session.js +608 -0
  107. package/src/core/agent/tools.js +596 -0
  108. package/src/core/agent/web-agent/backends/base-backend.js +180 -0
  109. package/src/core/agent/web-agent/backends/chatgpt/client.js +146 -0
  110. package/src/core/agent/web-agent/backends/chatgpt/config.js +148 -0
  111. package/src/core/agent/web-agent/backends/chatgpt/dom-scripts.js +303 -0
  112. package/src/core/agent/web-agent/backends/index.js +91 -0
  113. package/src/core/agent/web-agent/index.js +278 -0
  114. package/src/core/agent/web-agent/web-client.js +407 -0
  115. package/src/core/employee/base-employee.js +1088 -0
  116. package/src/core/employee/index.js +35 -0
  117. package/src/core/employee/knowledge.js +327 -0
  118. package/src/core/employee/lifecycle.js +990 -0
  119. package/src/core/employee/memory/index.js +642 -0
  120. package/src/core/employee/memory/store.js +143 -0
  121. package/src/core/employee/performance.js +224 -0
  122. package/src/core/employee/secretary.js +625 -0
  123. package/src/core/employee/skills.js +398 -0
  124. package/src/core/index.js +38 -0
  125. package/src/core/organization/company.js +2600 -0
  126. package/src/core/organization/department.js +737 -0
  127. package/src/core/organization/group-chat-loop.js +264 -0
  128. package/src/core/organization/index.js +8 -0
  129. package/src/core/organization/persistence.js +111 -0
  130. package/src/core/organization/team.js +267 -0
  131. package/src/core/organization/workforce/hr.js +377 -0
  132. package/src/core/organization/workforce/providers.js +468 -0
  133. package/src/core/organization/workforce/role-archetypes.js +805 -0
  134. package/src/core/organization/workforce/talent-market.js +205 -0
  135. package/src/core/prompts.js +532 -0
  136. package/src/core/requirement.js +1789 -0
  137. package/src/core/system/audit.js +483 -0
  138. package/src/core/system/cron.js +449 -0
  139. package/src/core/system/index.js +7 -0
  140. package/src/core/system/plugin.js +2183 -0
  141. package/src/core/utils/json-parse.js +188 -0
  142. package/src/core/workspace.js +239 -0
  143. package/src/lib/api-i18n.js +211 -0
  144. package/src/lib/avatar.js +268 -0
  145. package/src/lib/client-store.js +1025 -0
  146. package/src/lib/config-validator.js +483 -0
  147. package/src/lib/format-time.js +22 -0
  148. package/src/lib/hooks.js +414 -0
  149. package/src/lib/i18n.js +134 -0
  150. package/src/lib/paths.js +23 -0
  151. package/src/lib/store.js +72 -0
  152. package/src/locales/de.js +393 -0
  153. package/src/locales/en.js +1054 -0
  154. package/src/locales/es.js +393 -0
  155. package/src/locales/fr.js +393 -0
  156. package/src/locales/ja.js +501 -0
  157. package/src/locales/ko.js +513 -0
  158. package/src/locales/zh.js +828 -0
  159. package/tailwind.config.mjs +11 -0
@@ -0,0 +1,465 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
4
+ import { useStore } from '@/lib/client-store';
5
+ import { useI18n } from '@/lib/i18n';
6
+ import dynamic from 'next/dynamic';
7
+
8
+ const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { ssr: false });
9
+
10
+ const CUSTOM_THEME_NAME = 'vscode-dark-plus';
11
+ const defineCustomTheme = (monaco) => {
12
+ monaco.editor.defineTheme(CUSTOM_THEME_NAME, {
13
+ base: 'vs-dark',
14
+ inherit: true,
15
+ rules: [
16
+ { token: '', foreground: 'd4d4d4', background: '1e1e1e' },
17
+ { token: 'comment', foreground: '6a9955', fontStyle: 'italic' },
18
+ { token: 'keyword', foreground: '569cd6' },
19
+ { token: 'keyword.control', foreground: 'c586c0' },
20
+ { token: 'string', foreground: 'ce9178' },
21
+ { token: 'string.escape', foreground: 'd7ba7d' },
22
+ { token: 'number', foreground: 'b5cea8' },
23
+ { token: 'type', foreground: '4ec9b0' },
24
+ { token: 'type.identifier', foreground: '4ec9b0' },
25
+ { token: 'variable', foreground: '9cdcfe' },
26
+ { token: 'variable.predefined', foreground: '4fc1ff' },
27
+ { token: 'constant', foreground: '4fc1ff' },
28
+ { token: 'delimiter', foreground: 'd4d4d4' },
29
+ { token: 'delimiter.bracket', foreground: 'ffd700' },
30
+ { token: 'tag', foreground: '569cd6' },
31
+ { token: 'tag.id.pug', foreground: '4ec9b0' },
32
+ { token: 'attribute.name', foreground: '9cdcfe' },
33
+ { token: 'attribute.value', foreground: 'ce9178' },
34
+ { token: 'string.key.json', foreground: '9cdcfe' },
35
+ { token: 'string.value.json', foreground: 'ce9178' },
36
+ { token: 'metatag', foreground: 'ce9178' },
37
+ { token: 'metatag.content.html', foreground: 'ce9178' },
38
+ { token: 'regexp', foreground: 'd16969' },
39
+ { token: 'annotation', foreground: 'dcdcaa' },
40
+ { token: 'function', foreground: 'dcdcaa' },
41
+ { token: 'function.declaration', foreground: 'dcdcaa' },
42
+ { token: 'operator', foreground: 'd4d4d4' },
43
+ { token: 'namespace', foreground: '4ec9b0' },
44
+ ],
45
+ colors: {
46
+ 'editor.background': '#1e1e1e',
47
+ 'editor.foreground': '#d4d4d4',
48
+ 'editor.lineHighlightBackground': '#2a2d2e',
49
+ 'editor.selectionBackground': '#264f78',
50
+ 'editor.inactiveSelectionBackground': '#3a3d41',
51
+ 'editorCursor.foreground': '#aeafad',
52
+ 'editorLineNumber.foreground': '#858585',
53
+ 'editorLineNumber.activeForeground': '#c6c6c6',
54
+ 'editorIndentGuide.background': '#404040',
55
+ 'editorIndentGuide.activeBackground': '#707070',
56
+ 'editor.selectionHighlightBackground': '#add6ff26',
57
+ 'editorBracketMatch.background': '#0064001a',
58
+ 'editorBracketMatch.border': '#888888',
59
+ 'editorGutter.background': '#1e1e1e',
60
+ 'editorWidget.background': '#252526',
61
+ 'editorWidget.border': '#454545',
62
+ 'editorSuggestWidget.background': '#252526',
63
+ 'editorSuggestWidget.border': '#454545',
64
+ 'editorSuggestWidget.selectedBackground': '#04395e',
65
+ 'input.background': '#3c3c3c',
66
+ 'input.border': '#3c3c3c',
67
+ 'scrollbar.shadow': '#000000',
68
+ 'scrollbarSlider.background': '#79797966',
69
+ 'scrollbarSlider.hoverBackground': '#646464b3',
70
+ 'scrollbarSlider.activeBackground': '#bfbfbf66',
71
+ 'minimap.background': '#1e1e1e',
72
+ 'minimapSlider.background': '#79797933',
73
+ 'minimapSlider.hoverBackground': '#64646459',
74
+ }
75
+ });
76
+ };
77
+
78
+ /**
79
+ * Shared FilesView component - VSCode style left-right layout
80
+ * Left: file explorer (tree structure showing actual workspace files)
81
+ * Right: Monaco Editor code preview
82
+ *
83
+ * Props:
84
+ * - fileChanges: array of recent file changes from agents (used to trigger workspace reload)
85
+ * - departmentId: department ID for workspace API calls
86
+ * - previewFile: { path, content, loading } or null
87
+ * - onPreview: (filePath) => void
88
+ * - onClosePreview: () => void
89
+ */
90
+ export default function FilesView({ fileChanges, departmentId, previewFile, onPreview, onClosePreview }) {
91
+ const { t } = useI18n();
92
+ const { fetchWorkspaceFiles } = useStore();
93
+ const [sidebarWidth, setSidebarWidth] = useState(260);
94
+ const [isResizing, setIsResizing] = useState(false);
95
+ const [openTabs, setOpenTabs] = useState([]);
96
+ const [expandedDirs, setExpandedDirs] = useState(new Set());
97
+ const [dirChildren, setDirChildren] = useState({});
98
+ const [dirLoading, setDirLoading] = useState(new Set());
99
+ const resizeRef = useRef(null);
100
+ const expandedDirsRef = useRef(expandedDirs);
101
+ expandedDirsRef.current = expandedDirs;
102
+ const [rootEntries, setRootEntries] = useState([]);
103
+ const [wsLoading, setWsLoading] = useState(false);
104
+
105
+ // Reload workspace file tree when fileChanges updates (e.g. after CLI agent creates files)
106
+ const fileChangesLen = (fileChanges || []).length;
107
+ useEffect(() => {
108
+ if (!departmentId) return;
109
+ let cancelled = false;
110
+ setWsLoading(true);
111
+ // Clear cached sub-directory data so expanded dirs also refresh
112
+ setDirChildren({});
113
+ (async () => {
114
+ try {
115
+ const files = await fetchWorkspaceFiles(departmentId);
116
+ if (!cancelled && Array.isArray(files)) {
117
+ setRootEntries(files);
118
+ }
119
+ } catch {
120
+ // silently fail
121
+ }
122
+ if (!cancelled) setWsLoading(false);
123
+ // Re-load children for currently expanded directories
124
+ if (!cancelled) {
125
+ for (const dirPath of expandedDirsRef.current) {
126
+ try {
127
+ const children = await fetchWorkspaceFiles(departmentId, dirPath);
128
+ if (!cancelled && Array.isArray(children)) {
129
+ setDirChildren(prev => ({ ...prev, [dirPath]: children }));
130
+ }
131
+ } catch { /* ignore */ }
132
+ }
133
+ }
134
+ })();
135
+ return () => { cancelled = true; };
136
+ }, [departmentId, fetchWorkspaceFiles, fileChangesLen]);
137
+
138
+ const loadDirChildren = useCallback(async (dirPath) => {
139
+ if (dirChildren[dirPath] || dirLoading.has(dirPath)) return;
140
+ setDirLoading(prev => new Set(prev).add(dirPath));
141
+ try {
142
+ const children = await fetchWorkspaceFiles(departmentId, dirPath);
143
+ if (Array.isArray(children)) {
144
+ setDirChildren(prev => ({ ...prev, [dirPath]: children }));
145
+ }
146
+ } catch { /* ignore */ }
147
+ setDirLoading(prev => {
148
+ const next = new Set(prev);
149
+ next.delete(dirPath);
150
+ return next;
151
+ });
152
+ }, [departmentId, dirChildren, dirLoading, fetchWorkspaceFiles]);
153
+
154
+ const toggleDir = useCallback((dirPath) => {
155
+ setExpandedDirs(prev => {
156
+ const next = new Set(prev);
157
+ if (next.has(dirPath)) {
158
+ next.delete(dirPath);
159
+ } else {
160
+ next.add(dirPath);
161
+ loadDirChildren(dirPath);
162
+ }
163
+ return next;
164
+ });
165
+ }, [loadDirChildren]);
166
+
167
+ const totalCount = useMemo(() => {
168
+ return rootEntries.length;
169
+ }, [rootEntries]);
170
+
171
+ useEffect(() => {
172
+ if (!isResizing) return;
173
+ const handleMouseMove = (e) => {
174
+ const container = resizeRef.current?.parentElement;
175
+ if (!container) return;
176
+ const rect = container.getBoundingClientRect();
177
+ const newWidth = Math.max(180, Math.min(500, e.clientX - rect.left));
178
+ setSidebarWidth(newWidth);
179
+ };
180
+ const handleMouseUp = () => setIsResizing(false);
181
+ document.addEventListener('mousemove', handleMouseMove);
182
+ document.addEventListener('mouseup', handleMouseUp);
183
+ return () => {
184
+ document.removeEventListener('mousemove', handleMouseMove);
185
+ document.removeEventListener('mouseup', handleMouseUp);
186
+ };
187
+ }, [isResizing]);
188
+
189
+ const handleFileClick = (filePath) => {
190
+ const name = filePath?.split('/').pop() || filePath;
191
+ if (!openTabs.find(t => t.path === filePath)) {
192
+ setOpenTabs(prev => [...prev, { path: filePath, name }]);
193
+ }
194
+ onPreview(filePath);
195
+ };
196
+
197
+ const handleCloseTab = (e, tabPath) => {
198
+ e.stopPropagation();
199
+ setOpenTabs(prev => prev.filter(t => t.path !== tabPath));
200
+ if (previewFile?.path === tabPath) {
201
+ const remaining = openTabs.filter(t => t.path !== tabPath);
202
+ if (remaining.length > 0) {
203
+ onPreview(remaining[remaining.length - 1].path);
204
+ } else {
205
+ onClosePreview();
206
+ }
207
+ }
208
+ };
209
+
210
+ const getLanguage = (path) => {
211
+ const ext = path?.split('.').pop()?.toLowerCase();
212
+ const langMap = {
213
+ js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript',
214
+ py: 'python', json: 'json', md: 'markdown', css: 'css', scss: 'scss',
215
+ html: 'html', xml: 'xml', yaml: 'yaml', yml: 'yaml', sh: 'shell',
216
+ sql: 'sql', java: 'java', go: 'go', rs: 'rust', cpp: 'cpp', c: 'c',
217
+ txt: 'plaintext',
218
+ };
219
+ return langMap[ext] || 'plaintext';
220
+ };
221
+
222
+ const getFileIcon = (name) => {
223
+ const ext = name?.split('.').pop()?.toLowerCase();
224
+ const iconMap = {
225
+ js: '🟨', jsx: '⚛️', ts: '🔷', tsx: '⚛️', py: '🐍', json: '📋',
226
+ md: '📝', css: '🎨', html: '🌐', yaml: '⚙️', yml: '⚙️', txt: '📄',
227
+ sh: '🖥️', sql: '🗃️',
228
+ };
229
+ return iconMap[ext] || '📄';
230
+ };
231
+
232
+ // Sort helper: directories first
233
+ const sortEntries = (entries) =>
234
+ [...entries].sort((a, b) => {
235
+ if (a.type === 'directory' && b.type !== 'directory') return -1;
236
+ if (a.type !== 'directory' && b.type === 'directory') return 1;
237
+ return a.name.localeCompare(b.name);
238
+ });
239
+
240
+ const sortedRootEntries = useMemo(() => sortEntries(rootEntries), [rootEntries]);
241
+
242
+ // Empty state
243
+ if (rootEntries.length === 0 && !previewFile && !wsLoading) {
244
+ return (
245
+ <div className="flex items-center justify-center py-16 text-[var(--muted)]">
246
+ <div className="text-center">
247
+ <div className="text-4xl mb-2">📁</div>
248
+ <p>{t('reqDetail.files.noChanges')}</p>
249
+ <p className="text-xs mt-1">{t('reqDetail.files.noChangesHint')}</p>
250
+ </div>
251
+ </div>
252
+ );
253
+ }
254
+ if (rootEntries.length === 0 && wsLoading) {
255
+ return (
256
+ <div className="flex items-center justify-center py-16 text-[var(--muted)]">
257
+ <div className="text-center">
258
+ <div className="text-2xl mb-2 animate-pulse">⏳</div>
259
+ <p>{t('reqDetail.files.loading')}</p>
260
+ </div>
261
+ </div>
262
+ );
263
+ }
264
+
265
+ const renderFileEntry = (entry, depth = 0) => {
266
+ if (entry.type === 'file') {
267
+ const filePath = entry.path || entry.name;
268
+ const isActive = previewFile?.path === filePath;
269
+ return (
270
+ <div
271
+ key={filePath}
272
+ className={`flex items-center gap-1.5 px-2 py-[3px] cursor-pointer text-xs transition-colors group ${
273
+ isActive
274
+ ? 'bg-[var(--accent)]/15 text-[var(--accent)]'
275
+ : 'text-[var(--foreground)]/80 hover:bg-white/[0.06]'
276
+ }`}
277
+ style={{ paddingLeft: `${depth * 16 + 16}px` }}
278
+ onClick={() => handleFileClick(filePath)}
279
+ title={filePath}
280
+ >
281
+ <span className="text-[11px] shrink-0">{getFileIcon(entry.name)}</span>
282
+ <span className="truncate">{entry.name}</span>
283
+ </div>
284
+ );
285
+ }
286
+
287
+ // Directory entry
288
+ const dirPath = entry.path || entry.name;
289
+ const isExpanded = expandedDirs.has(dirPath);
290
+ const isLoading = dirLoading.has(dirPath);
291
+ const children = dirChildren[dirPath] || [];
292
+ const sortedChildren = sortEntries(children);
293
+
294
+ return (
295
+ <div key={dirPath}>
296
+ <div
297
+ className="flex items-center gap-1.5 px-2 py-[3px] cursor-pointer text-xs text-[var(--foreground)]/80 hover:bg-white/[0.06] transition-colors"
298
+ style={{ paddingLeft: `${depth * 16 + 16}px` }}
299
+ onClick={() => toggleDir(dirPath)}
300
+ >
301
+ <span className="text-[10px] text-[var(--muted)] w-3 text-center shrink-0">
302
+ {isLoading ? <span className="animate-pulse">⏳</span> : isExpanded ? '▼' : '▶'}
303
+ </span>
304
+ <span className="text-[11px] shrink-0">📁</span>
305
+ <span className="truncate font-medium">{entry.name}</span>
306
+ </div>
307
+ {isExpanded && sortedChildren.map(child =>
308
+ renderFileEntry(child, depth + 1)
309
+ )}
310
+ </div>
311
+ );
312
+ };
313
+
314
+ return (
315
+ <div className="flex h-full min-h-[400px] overflow-hidden">
316
+ {/* Left: file explorer */}
317
+ <div
318
+ className="shrink-0 bg-[var(--card)] flex flex-col"
319
+ style={{ width: sidebarWidth }}
320
+ >
321
+ <div className="px-3 py-2 text-[10px] font-semibold tracking-wider text-[var(--muted)] uppercase flex items-center justify-between">
322
+ <span>{t('reqDetail.files.explorer')}</span>
323
+ <span className="text-[10px] normal-case font-normal text-[var(--muted)]/60">
324
+ {totalCount}
325
+ </span>
326
+ </div>
327
+
328
+ <div className="flex-1 overflow-auto py-1 select-none">
329
+ {sortedRootEntries.map(entry => renderFileEntry(entry, 0))}
330
+ </div>
331
+ </div>
332
+
333
+ {/* Drag resize handle */}
334
+ <div
335
+ ref={resizeRef}
336
+ className={`w-[3px] cursor-col-resize hover:bg-[var(--accent)]/40 transition-colors shrink-0 ${
337
+ isResizing ? 'bg-[var(--accent)]/50' : 'bg-transparent'
338
+ }`}
339
+ onMouseDown={() => setIsResizing(true)}
340
+ />
341
+
342
+ {/* Right: editor area */}
343
+ <div className="flex-1 flex flex-col min-w-0 min-h-0 bg-[var(--background)]">
344
+ {openTabs.length > 0 && (
345
+ <div className="flex bg-[var(--card)] overflow-x-auto shrink-0">
346
+ {openTabs.map(tab => {
347
+ const isActive = previewFile?.path === tab.path;
348
+ return (
349
+ <div
350
+ key={tab.path}
351
+ className={`flex items-center gap-1.5 px-3 py-1.5 text-xs cursor-pointer min-w-0 group transition-colors ${
352
+ isActive
353
+ ? 'bg-[var(--background)] text-[var(--foreground)]'
354
+ : 'text-[var(--muted)] hover:bg-[var(--card-hover)]'
355
+ }`}
356
+ onClick={() => onPreview(tab.path)}
357
+ >
358
+ <span className="text-[11px] shrink-0">{getFileIcon(tab.name)}</span>
359
+ <span className="truncate max-w-[120px]">{tab.name}</span>
360
+ {previewFile?.loading && isActive && (
361
+ <span className="w-2 h-2 rounded-full bg-yellow-400 animate-pulse shrink-0" />
362
+ )}
363
+ <button
364
+ onClick={(e) => handleCloseTab(e, tab.path)}
365
+ className="ml-1 text-[var(--muted)] hover:text-white opacity-0 group-hover:opacity-100 transition-opacity shrink-0 text-[10px] w-4 h-4 flex items-center justify-center rounded hover:bg-white/10"
366
+ >
367
+
368
+ </button>
369
+ </div>
370
+ );
371
+ })}
372
+ </div>
373
+ )}
374
+
375
+ {previewFile ? (
376
+ <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
377
+ <div className="flex items-center justify-between px-3 py-1 bg-[var(--card)] text-[10px] text-[var(--muted)]">
378
+ <div className="flex items-center gap-1 truncate">
379
+ {previewFile.path?.split('/').filter(Boolean).map((seg, i, arr) => (
380
+ <span key={i} className="flex items-center gap-1">
381
+ {i > 0 && <span className="text-[var(--muted)]/50">›</span>}
382
+ <span className={i === arr.length - 1 ? 'text-[var(--foreground)]' : ''}>{seg}</span>
383
+ </span>
384
+ ))}
385
+ </div>
386
+ <div className="flex items-center gap-2 shrink-0">
387
+ {previewFile.loading && (
388
+ <span className="text-yellow-400 animate-pulse">{t('reqDetail.files.syncingShort')}</span>
389
+ )}
390
+ <button
391
+ onClick={() => onPreview(previewFile.path)}
392
+ className="text-[var(--muted)] hover:text-white transition-colors px-1"
393
+ title={t('common.refresh')}
394
+ >
395
+
396
+ </button>
397
+ </div>
398
+ </div>
399
+
400
+ <div className="flex-1 min-h-0 overflow-hidden">
401
+ {previewFile.loading ? (
402
+ <div className="flex items-center justify-center h-full text-[var(--muted)] animate-pulse">
403
+ <div className="text-center">
404
+ <div className="text-2xl mb-2">⏳</div>
405
+ <p className="text-sm">{t('common.loading')}</p>
406
+ </div>
407
+ </div>
408
+ ) : (
409
+ <MonacoEditor
410
+ height="100%"
411
+ language={getLanguage(previewFile.path)}
412
+ value={previewFile.content != null ? previewFile.content : t('reqDetail.files.emptyFile')}
413
+ theme={CUSTOM_THEME_NAME}
414
+ beforeMount={defineCustomTheme}
415
+ options={{
416
+ readOnly: true,
417
+ minimap: { enabled: true },
418
+ fontSize: 13,
419
+ lineNumbers: 'on',
420
+ scrollBeyondLastLine: false,
421
+ wordWrap: 'on',
422
+ automaticLayout: true,
423
+ renderWhitespace: 'selection',
424
+ smoothScrolling: true,
425
+ cursorBlinking: 'smooth',
426
+ padding: { top: 8 },
427
+ scrollbar: {
428
+ verticalScrollbarSize: 8,
429
+ horizontalScrollbarSize: 8,
430
+ },
431
+ }}
432
+ />
433
+ )}
434
+ </div>
435
+
436
+ <div className="flex items-center justify-between px-3 py-1 bg-[var(--card)] text-[10px] text-[var(--muted)]">
437
+ <div className="flex items-center gap-3">
438
+ <span>{getLanguage(previewFile.path).toUpperCase()}</span>
439
+ <span>UTF-8</span>
440
+ {previewFile.content && (
441
+ <span>{previewFile.content.split('\n').length} {t('reqDetail.files.lines', { n: '' }).trim()}</span>
442
+ )}
443
+ </div>
444
+ <div className="flex items-center gap-3">
445
+ <span>{t('reqDetail.files.readOnly')}</span>
446
+ <span className="w-2 h-2 rounded-full bg-green-500 inline-block" />
447
+ </div>
448
+ </div>
449
+ </div>
450
+ ) : (
451
+ <div className="flex-1 flex items-center justify-center text-[var(--muted)]">
452
+ <div className="text-center">
453
+ <div className="text-6xl mb-4 opacity-20">{ }</div>
454
+ <div className="w-16 h-16 mx-auto mb-4 rounded-xl bg-white/[0.03] flex items-center justify-center">
455
+ <span className="text-3xl opacity-40">📝</span>
456
+ </div>
457
+ <p className="text-sm">{t('reqDetail.files.clickToView')}</p>
458
+ <p className="text-[10px] mt-1 text-[var(--muted)]/60">{t('reqDetail.files.syntaxHighlight')}</p>
459
+ </div>
460
+ </div>
461
+ )}
462
+ </div>
463
+ </div>
464
+ );
465
+ }