@aion0/forge 0.2.0 → 0.2.1
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/api/code/route.ts +52 -18
- package/components/CodeViewer.tsx +69 -33
- package/components/Dashboard.tsx +1 -2
- package/components/WebTerminal.tsx +22 -12
- package/package.json +1 -1
package/app/api/code/route.ts
CHANGED
|
@@ -138,23 +138,57 @@ export async function GET(req: Request) {
|
|
|
138
138
|
const tree = scanDir(resolvedDir, resolvedDir);
|
|
139
139
|
const dirName = resolvedDir.split('/').pop() || resolvedDir;
|
|
140
140
|
|
|
141
|
-
// Git status:
|
|
142
|
-
|
|
143
|
-
|
|
141
|
+
// Git status: scan for git repos (could be root dir or subdirectories)
|
|
142
|
+
interface GitRepo {
|
|
143
|
+
name: string; // repo dir name (or '.' for root)
|
|
144
|
+
branch: string;
|
|
145
|
+
remote: string; // remote URL
|
|
146
|
+
changes: { path: string; status: string }[];
|
|
147
|
+
}
|
|
148
|
+
const gitRepos: GitRepo[] = [];
|
|
149
|
+
|
|
150
|
+
function scanGitStatus(dir: string, repoName: string, pathPrefix: string) {
|
|
151
|
+
try {
|
|
152
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: dir, encoding: 'utf-8', timeout: 3000 }).trim();
|
|
153
|
+
const statusOut = execSync('git status --porcelain -u', { cwd: dir, encoding: 'utf-8', timeout: 5000 });
|
|
154
|
+
const changes = statusOut.replace(/\n$/, '').split('\n').filter(Boolean)
|
|
155
|
+
.map(line => {
|
|
156
|
+
if (line.length < 4) return null;
|
|
157
|
+
return {
|
|
158
|
+
status: line.substring(0, 2).trim() || 'M',
|
|
159
|
+
path: pathPrefix ? `${pathPrefix}/${line.substring(3).replace(/\/$/, '')}` : line.substring(3).replace(/\/$/, ''),
|
|
160
|
+
};
|
|
161
|
+
})
|
|
162
|
+
.filter((g): g is { status: string; path: string } => g !== null && !!g.path && !g.path.includes(' -> '));
|
|
163
|
+
let remote = '';
|
|
164
|
+
try { remote = execSync('git remote get-url origin', { cwd: dir, encoding: 'utf-8', timeout: 2000 }).trim(); } catch {}
|
|
165
|
+
if (branch || changes.length > 0) {
|
|
166
|
+
gitRepos.push({ name: repoName, branch, remote, changes });
|
|
167
|
+
}
|
|
168
|
+
} catch {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Check if root is a git repo
|
|
144
172
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
execSync('git rev-parse --git-dir', { cwd: resolvedDir, encoding: 'utf-8', timeout: 2000 });
|
|
174
|
+
scanGitStatus(resolvedDir, '.', '');
|
|
175
|
+
} catch {
|
|
176
|
+
// Root is not a git repo — scan subdirectories
|
|
177
|
+
try {
|
|
178
|
+
for (const entry of readdirSync(resolvedDir, { withFileTypes: true })) {
|
|
179
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || IGNORE.has(entry.name)) continue;
|
|
180
|
+
const subDir = join(resolvedDir, entry.name);
|
|
181
|
+
try {
|
|
182
|
+
execSync('git rev-parse --git-dir', { cwd: subDir, encoding: 'utf-8', timeout: 2000 });
|
|
183
|
+
scanGitStatus(subDir, entry.name, entry.name);
|
|
184
|
+
} catch {}
|
|
185
|
+
}
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Flatten for backward compat
|
|
190
|
+
const gitChanges = gitRepos.flatMap(r => r.changes);
|
|
191
|
+
const gitBranch = gitRepos.length === 1 ? gitRepos[0].branch : '';
|
|
192
|
+
|
|
193
|
+
return NextResponse.json({ tree, dirName, dirPath: resolvedDir, gitBranch, gitChanges, gitRepos });
|
|
160
194
|
}
|
|
@@ -15,13 +15,15 @@ interface FileNode {
|
|
|
15
15
|
// ─── File Tree ───────────────────────────────────────────
|
|
16
16
|
|
|
17
17
|
type GitStatusMap = Map<string, string>; // path → status
|
|
18
|
+
type GitRepoMap = Map<string, { branch: string; remote: string }>; // dir name → repo info
|
|
18
19
|
|
|
19
|
-
function TreeNode({ node, depth, selected, onSelect, gitMap }: {
|
|
20
|
+
function TreeNode({ node, depth, selected, onSelect, gitMap, repoMap }: {
|
|
20
21
|
node: FileNode;
|
|
21
22
|
depth: number;
|
|
22
23
|
selected: string | null;
|
|
23
24
|
onSelect: (path: string) => void;
|
|
24
25
|
gitMap: GitStatusMap;
|
|
26
|
+
repoMap: GitRepoMap;
|
|
25
27
|
}) {
|
|
26
28
|
// Auto-expand if selected file is under this directory
|
|
27
29
|
const containsSelected = selected ? selected.startsWith(node.path + '/') : false;
|
|
@@ -30,18 +32,23 @@ function TreeNode({ node, depth, selected, onSelect, gitMap }: {
|
|
|
30
32
|
|
|
31
33
|
if (node.type === 'dir') {
|
|
32
34
|
const dirHasChanges = node.children?.some(c => hasGitChanges(c, gitMap));
|
|
35
|
+
const repo = repoMap.get(node.name);
|
|
33
36
|
return (
|
|
34
37
|
<div>
|
|
35
38
|
<button
|
|
36
39
|
onClick={() => setManualExpanded(v => v === null ? !expanded : !v)}
|
|
37
|
-
className="w-full text-left flex items-center gap-1 px-1 py-0.5 hover:bg-[var(--bg-tertiary)] rounded text-xs"
|
|
40
|
+
className="w-full text-left flex items-center gap-1 px-1 py-0.5 hover:bg-[var(--bg-tertiary)] rounded text-xs group"
|
|
38
41
|
style={{ paddingLeft: depth * 12 + 4 }}
|
|
42
|
+
title={repo ? `${repo.branch} · ${repo.remote.replace(/^https?:\/\//, '').replace(/^git@github\.com:/, 'github.com/').replace(/\.git$/, '')}` : undefined}
|
|
39
43
|
>
|
|
40
44
|
<span className="text-[10px] text-[var(--text-secondary)] w-3">{expanded ? '▾' : '▸'}</span>
|
|
41
45
|
<span className={dirHasChanges ? 'text-yellow-400' : 'text-[var(--text-primary)]'}>{node.name}</span>
|
|
46
|
+
{repo && (
|
|
47
|
+
<span className="text-[8px] text-[var(--accent)] opacity-60 group-hover:opacity-100 ml-auto shrink-0">{repo.branch}</span>
|
|
48
|
+
)}
|
|
42
49
|
</button>
|
|
43
50
|
{expanded && node.children?.map(child => (
|
|
44
|
-
<TreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} gitMap={gitMap} />
|
|
51
|
+
<TreeNode key={child.path} node={child} depth={depth + 1} selected={selected} onSelect={onSelect} gitMap={gitMap} repoMap={repoMap} />
|
|
45
52
|
))}
|
|
46
53
|
</div>
|
|
47
54
|
);
|
|
@@ -159,12 +166,13 @@ function highlightLine(line: string, lang: string): React.ReactNode {
|
|
|
159
166
|
|
|
160
167
|
// ─── Main Component ──────────────────────────────────────
|
|
161
168
|
|
|
162
|
-
export default function CodeViewer({ terminalRef
|
|
169
|
+
export default function CodeViewer({ terminalRef }: { terminalRef: React.RefObject<WebTerminalHandle | null> }) {
|
|
163
170
|
const [currentDir, setCurrentDir] = useState<string | null>(null);
|
|
164
171
|
const [dirName, setDirName] = useState('');
|
|
165
172
|
const [tree, setTree] = useState<FileNode[]>([]);
|
|
166
173
|
const [gitBranch, setGitBranch] = useState('');
|
|
167
174
|
const [gitChanges, setGitChanges] = useState<{ path: string; status: string }[]>([]);
|
|
175
|
+
const [gitRepos, setGitRepos] = useState<{ name: string; branch: string; remote: string; changes: { path: string; status: string }[] }[]>([]);
|
|
168
176
|
const [showGit, setShowGit] = useState(false);
|
|
169
177
|
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
|
170
178
|
const [content, setContent] = useState<string | null>(null);
|
|
@@ -176,6 +184,10 @@ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef:
|
|
|
176
184
|
const [viewMode, setViewMode] = useState<'file' | 'diff'>('file');
|
|
177
185
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
178
186
|
const [codeOpen, setCodeOpen] = useState(false);
|
|
187
|
+
|
|
188
|
+
const handleCodeOpenChange = useCallback((open: boolean) => {
|
|
189
|
+
setCodeOpen(open);
|
|
190
|
+
}, []);
|
|
179
191
|
const [terminalHeight, setTerminalHeight] = useState(300);
|
|
180
192
|
const [activeSession, setActiveSession] = useState<string | null>(null);
|
|
181
193
|
const dragRef = useRef<{ startY: number; startH: number } | null>(null);
|
|
@@ -216,6 +228,7 @@ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef:
|
|
|
216
228
|
setDirName(data.dirName || currentDir.split('/').pop() || '');
|
|
217
229
|
setGitBranch(data.gitBranch || '');
|
|
218
230
|
setGitChanges(data.gitChanges || []);
|
|
231
|
+
setGitRepos(data.gitRepos || []);
|
|
219
232
|
})
|
|
220
233
|
.catch(() => setTree([]));
|
|
221
234
|
};
|
|
@@ -224,6 +237,7 @@ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef:
|
|
|
224
237
|
|
|
225
238
|
// Build git status map for tree coloring
|
|
226
239
|
const gitMap: GitStatusMap = new Map(gitChanges.map(g => [g.path, g.status]));
|
|
240
|
+
const repoMap: GitRepoMap = new Map(gitRepos.filter(r => r.name !== '.').map(r => [r.name, { branch: r.branch, remote: r.remote }]));
|
|
227
241
|
|
|
228
242
|
const openFile = useCallback(async (path: string) => {
|
|
229
243
|
if (!currentDir) return;
|
|
@@ -285,7 +299,7 @@ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef:
|
|
|
285
299
|
{/* Terminal — top */}
|
|
286
300
|
<div className={codeOpen ? 'shrink-0' : 'flex-1'} style={codeOpen ? { height: terminalHeight } : undefined}>
|
|
287
301
|
<Suspense fallback={<div className="h-full flex items-center justify-center text-[var(--text-secondary)] text-xs">Loading...</div>}>
|
|
288
|
-
<WebTerminal ref={terminalRef} onActiveSession={handleActiveSession}
|
|
302
|
+
<WebTerminal ref={terminalRef} onActiveSession={handleActiveSession} onCodeOpenChange={handleCodeOpenChange} />
|
|
289
303
|
</Suspense>
|
|
290
304
|
</div>
|
|
291
305
|
|
|
@@ -314,6 +328,11 @@ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef:
|
|
|
314
328
|
</span>
|
|
315
329
|
)}
|
|
316
330
|
</div>
|
|
331
|
+
{gitRepos.find(r => r.name === '.')?.remote && (
|
|
332
|
+
<div className="text-[9px] text-[var(--text-secondary)] truncate mt-0.5" title={gitRepos.find(r => r.name === '.')!.remote}>
|
|
333
|
+
{gitRepos.find(r => r.name === '.')!.remote.replace(/^https?:\/\//, '').replace(/^git@github\.com:/, 'github.com/').replace(/\.git$/, '')}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
317
336
|
{gitChanges.length > 0 && (
|
|
318
337
|
<button
|
|
319
338
|
onClick={() => setShowGit(v => !v)}
|
|
@@ -324,38 +343,55 @@ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef:
|
|
|
324
343
|
)}
|
|
325
344
|
</div>
|
|
326
345
|
|
|
327
|
-
{/* Git changes */}
|
|
346
|
+
{/* Git changes — grouped by repo */}
|
|
328
347
|
{showGit && gitChanges.length > 0 && (
|
|
329
348
|
<div className="border-b border-[var(--border)] max-h-48 overflow-y-auto">
|
|
330
|
-
{
|
|
331
|
-
<div
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
349
|
+
{gitRepos.map(repo => (
|
|
350
|
+
<div key={repo.name}>
|
|
351
|
+
{/* Repo header — only show if multiple repos */}
|
|
352
|
+
{gitRepos.length > 1 && (
|
|
353
|
+
<div className="px-2 py-1 text-[9px] text-[var(--text-secondary)] bg-[var(--bg-tertiary)] sticky top-0" title={repo.remote}>
|
|
354
|
+
<div className="flex items-center gap-1.5">
|
|
355
|
+
<span className="font-semibold text-[var(--text-primary)]">{repo.name}</span>
|
|
356
|
+
<span className="text-[var(--accent)]">{repo.branch}</span>
|
|
357
|
+
<span className="ml-auto">{repo.changes.length}</span>
|
|
358
|
+
</div>
|
|
359
|
+
{repo.remote && (
|
|
360
|
+
<div className="text-[8px] truncate mt-0.5">{repo.remote.replace(/^https?:\/\//, '').replace(/\.git$/, '')}</div>
|
|
361
|
+
)}
|
|
362
|
+
</div>
|
|
363
|
+
)}
|
|
364
|
+
{repo.changes.map(g => (
|
|
365
|
+
<div
|
|
366
|
+
key={g.path}
|
|
367
|
+
className={`flex items-center px-2 py-1 text-xs hover:bg-[var(--bg-tertiary)] ${
|
|
368
|
+
diffFile === g.path && viewMode === 'diff' ? 'bg-[var(--accent)]/10' : ''
|
|
369
|
+
}`}
|
|
370
|
+
>
|
|
371
|
+
<span className={`text-[10px] font-mono w-4 shrink-0 ${
|
|
372
|
+
g.status.includes('M') ? 'text-yellow-500' :
|
|
373
|
+
g.status.includes('A') || g.status.includes('?') ? 'text-green-500' :
|
|
374
|
+
g.status.includes('D') ? 'text-red-500' :
|
|
375
|
+
'text-[var(--text-secondary)]'
|
|
376
|
+
}`}>
|
|
377
|
+
{g.status.includes('?') ? '+' : g.status[0]}
|
|
378
|
+
</span>
|
|
379
|
+
<button
|
|
380
|
+
onClick={() => openDiff(g.path)}
|
|
381
|
+
className="flex-1 text-left truncate text-[var(--text-secondary)] hover:text-[var(--text-primary)] ml-1 group relative"
|
|
382
|
+
title={`${g.path}${gitRepos.length > 1 ? ` (${repo.name} · ${repo.branch})` : ''}`}
|
|
383
|
+
>
|
|
384
|
+
{gitRepos.length > 1 ? g.path.replace(repo.name + '/', '') : g.path}
|
|
385
|
+
</button>
|
|
386
|
+
<button
|
|
387
|
+
onClick={(e) => { e.stopPropagation(); locateFile(g.path); }}
|
|
388
|
+
className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--accent)] hover:bg-[var(--accent)]/10 px-1.5 py-0.5 rounded shrink-0"
|
|
389
|
+
title="Locate in file tree"
|
|
356
390
|
>
|
|
357
391
|
file
|
|
358
392
|
</button>
|
|
393
|
+
</div>
|
|
394
|
+
))}
|
|
359
395
|
</div>
|
|
360
396
|
))}
|
|
361
397
|
</div>
|
|
@@ -396,7 +432,7 @@ export default function CodeViewer({ terminalRef, onToggleCode }: { terminalRef:
|
|
|
396
432
|
)
|
|
397
433
|
) : (
|
|
398
434
|
tree.map(node => (
|
|
399
|
-
<TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} gitMap={gitMap} />
|
|
435
|
+
<TreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} gitMap={gitMap} repoMap={repoMap} />
|
|
400
436
|
))
|
|
401
437
|
)}
|
|
402
438
|
</div>
|
package/components/Dashboard.tsx
CHANGED
|
@@ -40,7 +40,6 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
40
40
|
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
|
41
41
|
const [showNewTask, setShowNewTask] = useState(false);
|
|
42
42
|
const [showSettings, setShowSettings] = useState(false);
|
|
43
|
-
const [showCode, setShowCode] = useState(true);
|
|
44
43
|
const [usage, setUsage] = useState<UsageSummary[]>([]);
|
|
45
44
|
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
46
45
|
const [projects, setProjects] = useState<ProjectInfo[]>([]);
|
|
@@ -262,7 +261,7 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
262
261
|
{/* Code — terminal + file browser, always mounted to keep terminal sessions alive */}
|
|
263
262
|
<div className={`flex-1 min-h-0 flex ${viewMode === 'terminal' ? '' : 'hidden'}`}>
|
|
264
263
|
<Suspense fallback={<div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">Loading...</div>}>
|
|
265
|
-
<CodeViewer terminalRef={terminalRef}
|
|
264
|
+
<CodeViewer terminalRef={terminalRef} />
|
|
266
265
|
</Suspense>
|
|
267
266
|
</div>
|
|
268
267
|
</div>
|
|
@@ -13,8 +13,7 @@ export interface WebTerminalHandle {
|
|
|
13
13
|
|
|
14
14
|
export interface WebTerminalProps {
|
|
15
15
|
onActiveSession?: (sessionName: string | null) => void;
|
|
16
|
-
|
|
17
|
-
onToggleCode?: () => void;
|
|
16
|
+
onCodeOpenChange?: (open: boolean) => void;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
// ─── Types ───────────────────────────────────────────────────
|
|
@@ -162,7 +161,7 @@ let globalDragging = false;
|
|
|
162
161
|
|
|
163
162
|
// ─── Main component ─────────────────────────────────────────
|
|
164
163
|
|
|
165
|
-
const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function WebTerminal({ onActiveSession,
|
|
164
|
+
const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function WebTerminal({ onActiveSession, onCodeOpenChange }, ref) {
|
|
166
165
|
const [tabs, setTabs] = useState<TabState[]>(() => {
|
|
167
166
|
const tree = makeTerminal();
|
|
168
167
|
return [{ id: nextId++, label: 'Terminal 1', tree, ratios: {}, activeId: firstTerminalId(tree) }];
|
|
@@ -178,6 +177,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
178
177
|
const sessionLabelsRef = useRef<Record<string, string>>({});
|
|
179
178
|
const dragTabRef = useRef<number | null>(null);
|
|
180
179
|
const [refreshKeys, setRefreshKeys] = useState<Record<number, number>>({});
|
|
180
|
+
const [tabCodeOpen, setTabCodeOpen] = useState<Record<number, boolean>>({});
|
|
181
181
|
|
|
182
182
|
// Restore shared state from server after mount
|
|
183
183
|
useEffect(() => {
|
|
@@ -214,12 +214,17 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
214
214
|
|
|
215
215
|
const activeTab = tabs.find(t => t.id === activeTabId) || tabs[0];
|
|
216
216
|
|
|
217
|
-
// Notify parent when active terminal session changes
|
|
217
|
+
// Notify parent when active terminal session or code state changes
|
|
218
218
|
useEffect(() => {
|
|
219
|
-
if (!
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
219
|
+
if (!activeTab) return;
|
|
220
|
+
if (onActiveSession) {
|
|
221
|
+
const sessions = collectSessionNames(activeTab.tree);
|
|
222
|
+
onActiveSession(sessions[0] || null);
|
|
223
|
+
}
|
|
224
|
+
if (onCodeOpenChange) {
|
|
225
|
+
onCodeOpenChange(tabCodeOpen[activeTab.id] ?? false);
|
|
226
|
+
}
|
|
227
|
+
}, [activeTabId, activeTab, onActiveSession, onCodeOpenChange, tabCodeOpen]);
|
|
223
228
|
|
|
224
229
|
// ─── Imperative handle for parent ─────────────────────
|
|
225
230
|
|
|
@@ -519,11 +524,16 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
519
524
|
>
|
|
520
525
|
Refresh
|
|
521
526
|
</button>
|
|
522
|
-
{
|
|
527
|
+
{onCodeOpenChange && activeTab && (
|
|
523
528
|
<button
|
|
524
|
-
onClick={
|
|
525
|
-
|
|
526
|
-
|
|
529
|
+
onClick={() => {
|
|
530
|
+
const current = tabCodeOpen[activeTab.id] ?? false;
|
|
531
|
+
const next = !current;
|
|
532
|
+
setTabCodeOpen(prev => ({ ...prev, [activeTab.id]: next }));
|
|
533
|
+
onCodeOpenChange(next);
|
|
534
|
+
}}
|
|
535
|
+
className={`text-[11px] px-3 py-1 rounded font-bold ${(tabCodeOpen[activeTab.id] ?? false) ? 'text-white bg-red-500 hover:bg-red-400' : 'text-red-400 border border-red-500 hover:bg-red-500 hover:text-white'}`}
|
|
536
|
+
title={(tabCodeOpen[activeTab.id] ?? false) ? 'Hide code panel' : 'Show code panel'}
|
|
527
537
|
>
|
|
528
538
|
Code
|
|
529
539
|
</button>
|