@aion0/forge 0.5.15 → 0.5.16
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/RELEASE_NOTES.md +5 -16
- package/app/api/code/route.ts +22 -0
- package/app/api/git/route.ts +16 -3
- package/components/ProjectDetail.tsx +122 -35
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,23 +1,12 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.16
|
|
2
2
|
|
|
3
3
|
Released: 2026-03-31
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.15
|
|
6
6
|
|
|
7
7
|
### Features
|
|
8
|
-
- feat:
|
|
9
|
-
- feat:
|
|
8
|
+
- feat: code search in project sidebar
|
|
9
|
+
- feat: git enhancements — branch switch, resizable changes, more log
|
|
10
10
|
|
|
11
|
-
### Bug Fixes
|
|
12
|
-
- fix: suppress session monitor for 10s after manual state change
|
|
13
|
-
- fix: reset session monitor state when task status manually changed
|
|
14
|
-
- fix: stop button resets task to idle for terminal agents, not smith down
|
|
15
|
-
- fix: session monitor fallback timeout to 60min
|
|
16
|
-
- fix: session monitor fallback timeout to 10min
|
|
17
|
-
- fix: session monitor done threshold to 5min (hook is primary detection)
|
|
18
|
-
- fix: add logging to agent-context.json write for debugging
|
|
19
|
-
- fix: hook uses correct Claude Code schema + date-stamped backup
|
|
20
|
-
- fix: hook reads agent context from file instead of env vars
|
|
21
11
|
|
|
22
|
-
|
|
23
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.14...v0.5.15
|
|
12
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.15...v0.5.16
|
package/app/api/code/route.ts
CHANGED
|
@@ -89,6 +89,28 @@ export async function GET(req: Request) {
|
|
|
89
89
|
return NextResponse.json({ error: 'Directory not under any project root' }, { status: 403 });
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// Code search (grep)
|
|
93
|
+
const searchQuery = searchParams.get('search');
|
|
94
|
+
if (searchQuery) {
|
|
95
|
+
try {
|
|
96
|
+
const { execSync } = require('node:child_process');
|
|
97
|
+
const safeQuery = searchQuery.replace(/['"\\]/g, '\\$&');
|
|
98
|
+
// Use grep -rn with limits to prevent huge output
|
|
99
|
+
const result = execSync(
|
|
100
|
+
`grep -rn --include='*.ts' --include='*.tsx' --include='*.js' --include='*.jsx' --include='*.py' --include='*.java' --include='*.go' --include='*.rs' --include='*.md' --include='*.json' --include='*.yaml' --include='*.yml' --include='*.css' --include='*.html' --include='*.vue' --include='*.svelte' -m 5 '${safeQuery}' . 2>/dev/null | head -100`,
|
|
101
|
+
{ cwd: resolvedDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
102
|
+
).trim();
|
|
103
|
+
const matches = result ? result.split('\n').map((line: string) => {
|
|
104
|
+
const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
|
|
105
|
+
if (!match) return null;
|
|
106
|
+
return { file: match[1], line: parseInt(match[2]), content: match[3].trim().slice(0, 200) };
|
|
107
|
+
}).filter(Boolean) : [];
|
|
108
|
+
return NextResponse.json({ matches });
|
|
109
|
+
} catch {
|
|
110
|
+
return NextResponse.json({ matches: [] });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
92
114
|
// Git diff for a specific file
|
|
93
115
|
const diffFile = searchParams.get('diff');
|
|
94
116
|
if (diffFile) {
|
package/app/api/git/route.ts
CHANGED
|
@@ -34,12 +34,13 @@ export async function GET(req: NextRequest) {
|
|
|
34
34
|
|
|
35
35
|
try {
|
|
36
36
|
// Run all git commands in parallel
|
|
37
|
-
const [branchOut, statusOut, remoteOut, lastCommitOut, logOut] = await Promise.all([
|
|
37
|
+
const [branchOut, statusOut, remoteOut, lastCommitOut, logOut, branchListOut] = await Promise.all([
|
|
38
38
|
gitAsync('rev-parse --abbrev-ref HEAD', dir),
|
|
39
39
|
gitAsync('status --porcelain -u', dir),
|
|
40
40
|
gitAsync('remote get-url origin', dir),
|
|
41
41
|
gitAsync('log -1 --format="%h %s"', dir),
|
|
42
|
-
gitAsync('log --format="%h||%s||%an||%ar" -
|
|
42
|
+
gitAsync('log --format="%h||%s||%an||%ar" -20', dir),
|
|
43
|
+
gitAsync('branch --format="%(refname:short)||%(upstream:short)||%(objectname:short)"', dir),
|
|
43
44
|
]);
|
|
44
45
|
|
|
45
46
|
const branch = branchOut;
|
|
@@ -59,7 +60,12 @@ export async function GET(req: NextRequest) {
|
|
|
59
60
|
return { hash, message, author, date };
|
|
60
61
|
}) : [];
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
const branches = branchListOut ? branchListOut.split('\n').filter(Boolean).map(line => {
|
|
64
|
+
const [name, upstream, hash] = line.split('||');
|
|
65
|
+
return { name, upstream: upstream || '', hash: hash || '', current: name === branch };
|
|
66
|
+
}) : [];
|
|
67
|
+
|
|
68
|
+
return NextResponse.json({ branch, branches, changes, remote: remoteOut, ahead, behind, lastCommit: lastCommitOut, log });
|
|
63
69
|
} catch (e: any) {
|
|
64
70
|
return NextResponse.json({ error: e.message }, { status: 500 });
|
|
65
71
|
}
|
|
@@ -120,6 +126,13 @@ export async function POST(req: NextRequest) {
|
|
|
120
126
|
return NextResponse.json({ ok: true, output });
|
|
121
127
|
}
|
|
122
128
|
|
|
129
|
+
if (action === 'checkout') {
|
|
130
|
+
const branch = body.branch;
|
|
131
|
+
if (!branch) return NextResponse.json({ error: 'branch required' }, { status: 400 });
|
|
132
|
+
const out = gitSync(`checkout ${branch}`, dir);
|
|
133
|
+
return NextResponse.json({ ok: true, output: out });
|
|
134
|
+
}
|
|
135
|
+
|
|
123
136
|
if (action === 'stage') {
|
|
124
137
|
if (files && files.length > 0) {
|
|
125
138
|
for (const f of files) gitSync(`add "${f}"`, dir);
|
|
@@ -49,6 +49,7 @@ function highlightLine(line: string): React.ReactNode {
|
|
|
49
49
|
|
|
50
50
|
interface GitInfo {
|
|
51
51
|
branch: string;
|
|
52
|
+
branches: { name: string; upstream: string; hash: string; current: boolean }[];
|
|
52
53
|
changes: { status: string; path: string }[];
|
|
53
54
|
remote: string;
|
|
54
55
|
ahead: number;
|
|
@@ -72,6 +73,12 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
72
73
|
const [fileLanguage, setFileLanguage] = useState('');
|
|
73
74
|
const [fileLoading, setFileLoading] = useState(false);
|
|
74
75
|
const [showLog, setShowLog] = useState(false);
|
|
76
|
+
const [changesExpanded, setChangesExpanded] = useState(false);
|
|
77
|
+
const [codeSearch, setCodeSearch] = useState('');
|
|
78
|
+
const [codeSearchResults, setCodeSearchResults] = useState<{ file: string; line: number; content: string }[]>([]);
|
|
79
|
+
const [codeSearching, setCodeSearching] = useState(false);
|
|
80
|
+
const [changesHeight, setChangesHeight] = useState(120);
|
|
81
|
+
const changesResizeRef = useRef<{ startY: number; origH: number } | null>(null);
|
|
75
82
|
const [diffContent, setDiffContent] = useState<string | null>(null);
|
|
76
83
|
const [diffFile, setDiffFile] = useState<string | null>(null);
|
|
77
84
|
const [projectSkills, setProjectSkills] = useState<{ name: string; displayName: string; type: string; scope: string; version: string; installedVersion: string; hasUpdate: boolean; source: 'registry' | 'local' }[]>([]);
|
|
@@ -448,7 +455,25 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
448
455
|
<div className="flex items-center gap-2">
|
|
449
456
|
<span className="text-sm font-semibold text-[var(--text-primary)]">{projectName}</span>
|
|
450
457
|
{gitInfo?.branch && (
|
|
451
|
-
<
|
|
458
|
+
<div className="relative">
|
|
459
|
+
<select
|
|
460
|
+
value={gitInfo.branch}
|
|
461
|
+
onChange={async (e) => {
|
|
462
|
+
const branch = e.target.value;
|
|
463
|
+
if (branch === gitInfo.branch) return;
|
|
464
|
+
try {
|
|
465
|
+
await fetch('/api/git', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'checkout', dir: projectPath, branch }) });
|
|
466
|
+
fetchGitInfo(); fetchTree();
|
|
467
|
+
} catch {}
|
|
468
|
+
}}
|
|
469
|
+
className="text-[9px] text-[var(--accent)] bg-[var(--accent)]/10 px-1.5 py-0.5 rounded border-none cursor-pointer appearance-none pr-4 focus:outline-none"
|
|
470
|
+
style={{ backgroundImage: 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'8\' height=\'8\' viewBox=\'0 0 8 8\'%3E%3Cpath d=\'M0 2l4 4 4-4z\' fill=\'%2358a6ff\'/%3E%3C/svg%3E")', backgroundRepeat: 'no-repeat', backgroundPosition: 'right 4px center' }}
|
|
471
|
+
>
|
|
472
|
+
{(gitInfo.branches || []).map(b => (
|
|
473
|
+
<option key={b.name} value={b.name}>{b.name}{b.current ? ' ●' : ''}</option>
|
|
474
|
+
))}
|
|
475
|
+
</select>
|
|
476
|
+
</div>
|
|
452
477
|
)}
|
|
453
478
|
{gitInfo?.ahead ? <span className="text-[9px] text-green-400">↑{gitInfo.ahead}</span> : null}
|
|
454
479
|
{gitInfo?.behind ? <span className="text-[9px] text-yellow-400">↓{gitInfo.behind}</span> : null}
|
|
@@ -645,11 +670,51 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
645
670
|
|
|
646
671
|
{/* Code content area */}
|
|
647
672
|
{projectTab === 'code' && <div className="flex-1 flex min-h-0 overflow-hidden">
|
|
648
|
-
{/* File tree */}
|
|
649
|
-
<div style={{ width: sidebarWidth }} className="
|
|
650
|
-
{
|
|
651
|
-
|
|
652
|
-
|
|
673
|
+
{/* File tree + search */}
|
|
674
|
+
<div style={{ width: sidebarWidth }} className="flex flex-col shrink-0">
|
|
675
|
+
{/* Search input */}
|
|
676
|
+
<div className="p-1 border-b border-[var(--border)]">
|
|
677
|
+
<input
|
|
678
|
+
value={codeSearch}
|
|
679
|
+
onChange={e => setCodeSearch(e.target.value)}
|
|
680
|
+
onKeyDown={async e => {
|
|
681
|
+
if (e.key === 'Enter' && codeSearch.trim()) {
|
|
682
|
+
setCodeSearching(true);
|
|
683
|
+
try {
|
|
684
|
+
const res = await fetch(`/api/code?dir=${encodeURIComponent(projectPath)}&search=${encodeURIComponent(codeSearch.trim())}`);
|
|
685
|
+
const data = await res.json();
|
|
686
|
+
setCodeSearchResults(data.matches || []);
|
|
687
|
+
} catch { setCodeSearchResults([]); }
|
|
688
|
+
setCodeSearching(false);
|
|
689
|
+
}
|
|
690
|
+
if (e.key === 'Escape') { setCodeSearch(''); setCodeSearchResults([]); }
|
|
691
|
+
}}
|
|
692
|
+
placeholder="Search code... (Enter)"
|
|
693
|
+
className="w-full text-[10px] bg-[var(--bg-tertiary)] border border-[var(--border)] rounded px-2 py-1 text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] placeholder-[var(--text-secondary)]"
|
|
694
|
+
/>
|
|
695
|
+
</div>
|
|
696
|
+
{/* Search results */}
|
|
697
|
+
{codeSearchResults.length > 0 && (
|
|
698
|
+
<div className="overflow-y-auto border-b border-[var(--border)] max-h-60">
|
|
699
|
+
<div className="px-2 py-0.5 text-[8px] text-[var(--text-secondary)] bg-[var(--bg-tertiary)] sticky top-0 flex items-center justify-between">
|
|
700
|
+
<span>{codeSearchResults.length} results for "{codeSearch}"</span>
|
|
701
|
+
<button onClick={() => { setCodeSearch(''); setCodeSearchResults([]); }} className="text-[var(--text-secondary)] hover:text-[var(--text-primary)]">✕</button>
|
|
702
|
+
</div>
|
|
703
|
+
{codeSearchResults.map((r, i) => (
|
|
704
|
+
<div key={i} onClick={() => openFile(r.file)} className="px-2 py-0.5 cursor-pointer hover:bg-[var(--bg-tertiary)] border-b border-[var(--border)]/30">
|
|
705
|
+
<div className="text-[9px] text-[var(--accent)] truncate">{r.file}:{r.line}</div>
|
|
706
|
+
<div className="text-[8px] text-[var(--text-secondary)] font-mono truncate">{r.content}</div>
|
|
707
|
+
</div>
|
|
708
|
+
))}
|
|
709
|
+
</div>
|
|
710
|
+
)}
|
|
711
|
+
{codeSearching && <div className="px-2 py-1 text-[9px] text-[var(--text-secondary)]">Searching...</div>}
|
|
712
|
+
{/* File tree */}
|
|
713
|
+
<div className="overflow-y-auto flex-1 p-1">
|
|
714
|
+
{fileTree.map((node: any) => (
|
|
715
|
+
<FileTreeNode key={node.path} node={node} depth={0} selected={selectedFile} onSelect={openFile} />
|
|
716
|
+
))}
|
|
717
|
+
</div>
|
|
653
718
|
</div>
|
|
654
719
|
|
|
655
720
|
{/* Sidebar resize handle */}
|
|
@@ -1299,36 +1364,58 @@ export default memo(function ProjectDetail({ projectPath, projectName, hasGit }:
|
|
|
1299
1364
|
<div className="border-t border-[var(--border)] shrink-0">
|
|
1300
1365
|
{/* Changes list */}
|
|
1301
1366
|
{gitInfo.changes.length > 0 && (
|
|
1302
|
-
|
|
1303
|
-
<div className="
|
|
1304
|
-
{
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
<span
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
g.status.includes('D') ? 'text-red-500' : 'text-[var(--text-secondary)]'
|
|
1312
|
-
}`}>
|
|
1313
|
-
{g.status.includes('?') ? '+' : g.status[0]}
|
|
1314
|
-
</span>
|
|
1315
|
-
<button
|
|
1316
|
-
onClick={() => openDiff(g.path)}
|
|
1317
|
-
className={`truncate flex-1 text-left ml-1 ${diffFile === g.path ? 'text-[var(--accent)]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'}`}
|
|
1318
|
-
title="View diff"
|
|
1319
|
-
>
|
|
1320
|
-
{g.path}
|
|
1321
|
-
</button>
|
|
1322
|
-
<button
|
|
1323
|
-
onClick={() => openFile(g.path)}
|
|
1324
|
-
className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--accent)] opacity-0 group-hover:opacity-100 shrink-0 ml-1"
|
|
1325
|
-
title="Open source file"
|
|
1326
|
-
>
|
|
1327
|
-
src
|
|
1328
|
-
</button>
|
|
1367
|
+
<>
|
|
1368
|
+
<div className="overflow-y-auto border-b border-[var(--border)]" style={{ height: changesHeight }}>
|
|
1369
|
+
<div className="px-3 py-1 text-[9px] text-[var(--text-secondary)] bg-[var(--bg-tertiary)] sticky top-0 flex items-center gap-1 cursor-pointer z-10" onClick={() => {
|
|
1370
|
+
setChangesExpanded(!changesExpanded);
|
|
1371
|
+
setChangesHeight(changesExpanded ? 120 : Math.min(400, gitInfo.changes.length * 22 + 24));
|
|
1372
|
+
}}>
|
|
1373
|
+
<span>{changesExpanded ? '▼' : '▶'}</span>
|
|
1374
|
+
<span>{gitInfo.changes.length} changes</span>
|
|
1375
|
+
<button onClick={(e) => { e.stopPropagation(); fetchGitInfo(); }} className="ml-auto text-[8px] hover:text-[var(--accent)]" title="Refresh">↻</button>
|
|
1329
1376
|
</div>
|
|
1330
|
-
|
|
1331
|
-
|
|
1377
|
+
{gitInfo.changes.map(g => (
|
|
1378
|
+
<div key={g.path} className="flex items-center px-3 py-0.5 text-xs hover:bg-[var(--bg-tertiary)] group">
|
|
1379
|
+
<span className={`text-[10px] font-mono w-4 shrink-0 ${
|
|
1380
|
+
g.status.includes('M') ? 'text-yellow-500' :
|
|
1381
|
+
g.status.includes('?') ? 'text-green-500' :
|
|
1382
|
+
g.status.includes('D') ? 'text-red-500' : 'text-[var(--text-secondary)]'
|
|
1383
|
+
}`}>
|
|
1384
|
+
{g.status.includes('?') ? '+' : g.status[0]}
|
|
1385
|
+
</span>
|
|
1386
|
+
<button
|
|
1387
|
+
onClick={() => openDiff(g.path)}
|
|
1388
|
+
className={`truncate flex-1 text-left ml-1 ${diffFile === g.path ? 'text-[var(--accent)]' : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)]'}`}
|
|
1389
|
+
title="View diff"
|
|
1390
|
+
>
|
|
1391
|
+
{g.path}
|
|
1392
|
+
</button>
|
|
1393
|
+
<button
|
|
1394
|
+
onClick={() => openFile(g.path)}
|
|
1395
|
+
className="text-[8px] text-[var(--text-secondary)] hover:text-[var(--accent)] opacity-0 group-hover:opacity-100 shrink-0 ml-1"
|
|
1396
|
+
title="Open source file"
|
|
1397
|
+
>
|
|
1398
|
+
src
|
|
1399
|
+
</button>
|
|
1400
|
+
</div>
|
|
1401
|
+
))}
|
|
1402
|
+
</div>
|
|
1403
|
+
{/* Drag handle to resize changes list */}
|
|
1404
|
+
<div
|
|
1405
|
+
className="h-1 cursor-ns-resize hover:bg-[var(--accent)]/30 border-b border-[var(--border)]"
|
|
1406
|
+
onMouseDown={(e) => {
|
|
1407
|
+
e.preventDefault();
|
|
1408
|
+
changesResizeRef.current = { startY: e.clientY, origH: changesHeight };
|
|
1409
|
+
const onMove = (ev: MouseEvent) => {
|
|
1410
|
+
if (!changesResizeRef.current) return;
|
|
1411
|
+
setChangesHeight(Math.max(60, Math.min(600, changesResizeRef.current.origH + ev.clientY - changesResizeRef.current.startY)));
|
|
1412
|
+
};
|
|
1413
|
+
const onUp = () => { changesResizeRef.current = null; window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); };
|
|
1414
|
+
window.addEventListener('mousemove', onMove);
|
|
1415
|
+
window.addEventListener('mouseup', onUp);
|
|
1416
|
+
}}
|
|
1417
|
+
/>
|
|
1418
|
+
</>
|
|
1332
1419
|
)}
|
|
1333
1420
|
|
|
1334
1421
|
{/* Git actions */}
|
package/package.json
CHANGED