@aion0/forge 0.5.19 → 0.5.21
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/.forge/mcp.json +1 -1
- package/RELEASE_NOTES.md +9 -4
- package/app/api/code/route.ts +10 -4
- package/components/WorkspaceView.tsx +115 -11
- package/lib/workspace/orchestrator.ts +1 -7
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/qa/.forge/agent-context.json +6 -0
package/.forge/mcp.json
CHANGED
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.21
|
|
2
2
|
|
|
3
3
|
Released: 2026-04-01
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.20
|
|
6
6
|
|
|
7
7
|
### Bug Fixes
|
|
8
|
-
- fix:
|
|
8
|
+
- fix: code search excludes node_modules + handles grep exit code 1
|
|
9
|
+
- Revert "fix: hook only broadcasts/suppresses when transitioning from running"
|
|
10
|
+
- fix: hook only broadcasts/suppresses when transitioning from running
|
|
9
11
|
|
|
12
|
+
### Other
|
|
13
|
+
- Revert "fix: hook only broadcasts/suppresses when transitioning from running"
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
|
|
16
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.20...v0.5.21
|
package/app/api/code/route.ts
CHANGED
|
@@ -96,10 +96,16 @@ export async function GET(req: Request) {
|
|
|
96
96
|
const { execSync } = require('node:child_process');
|
|
97
97
|
const safeQuery = searchQuery.replace(/['"\\]/g, '\\$&');
|
|
98
98
|
// Use grep -rn with limits to prevent huge output
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
let result = '';
|
|
100
|
+
try {
|
|
101
|
+
result = execSync(
|
|
102
|
+
`grep -rn --exclude-dir=node_modules --exclude-dir=.next --exclude-dir=.git --exclude-dir=dist --exclude-dir=build --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}' . | head -100`,
|
|
103
|
+
{ cwd: resolvedDir, encoding: 'utf-8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
104
|
+
).trim();
|
|
105
|
+
} catch (e: any) {
|
|
106
|
+
// grep exit code 1 = no match (not an error)
|
|
107
|
+
result = e.stdout?.trim() || '';
|
|
108
|
+
}
|
|
103
109
|
const matches = result ? result.split('\n').map((line: string) => {
|
|
104
110
|
const match = line.match(/^\.\/(.+?):(\d+):(.*)$/);
|
|
105
111
|
if (!match) return null;
|
|
@@ -379,6 +379,109 @@ function SessionTargetSelector({ target, agents, projectPath, onChange }: {
|
|
|
379
379
|
);
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
+
// ─── Watch Path Picker (file/directory browser) ─────────
|
|
383
|
+
|
|
384
|
+
function WatchPathPicker({ value, projectPath, onChange }: { value: string; projectPath: string; onChange: (v: string) => void }) {
|
|
385
|
+
const [showBrowser, setShowBrowser] = useState(false);
|
|
386
|
+
const [tree, setTree] = useState<any[]>([]);
|
|
387
|
+
const [search, setSearch] = useState('');
|
|
388
|
+
const [flatFiles, setFlatFiles] = useState<string[]>([]);
|
|
389
|
+
|
|
390
|
+
const loadTree = useCallback(() => {
|
|
391
|
+
if (!projectPath) return;
|
|
392
|
+
fetch(`/api/code?dir=${encodeURIComponent(projectPath)}`)
|
|
393
|
+
.then(r => r.json())
|
|
394
|
+
.then(data => {
|
|
395
|
+
setTree(data.tree || []);
|
|
396
|
+
// Build flat list for search
|
|
397
|
+
const files: string[] = [];
|
|
398
|
+
const walk = (nodes: any[], prefix = '') => {
|
|
399
|
+
for (const n of nodes || []) {
|
|
400
|
+
const path = prefix ? `${prefix}/${n.name}` : n.name;
|
|
401
|
+
files.push(n.type === 'dir' ? path + '/' : path);
|
|
402
|
+
if (n.children) walk(n.children, path);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
walk(data.tree || []);
|
|
406
|
+
setFlatFiles(files);
|
|
407
|
+
})
|
|
408
|
+
.catch(() => {});
|
|
409
|
+
}, [projectPath]);
|
|
410
|
+
|
|
411
|
+
const filtered = search ? flatFiles.filter(f => f.toLowerCase().includes(search.toLowerCase())).slice(0, 30) : [];
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div className="flex-1 flex items-center gap-1 relative">
|
|
415
|
+
<input
|
|
416
|
+
value={value}
|
|
417
|
+
onChange={e => onChange(e.target.value)}
|
|
418
|
+
placeholder="./ (project root)"
|
|
419
|
+
className="text-[10px] bg-[#161b22] border border-[#30363d] rounded px-1 py-0.5 text-white flex-1"
|
|
420
|
+
/>
|
|
421
|
+
<button onClick={() => { setShowBrowser(!showBrowser); if (!showBrowser) loadTree(); }}
|
|
422
|
+
className="text-[9px] px-1 py-0.5 rounded bg-[#30363d] text-gray-400 hover:text-white shrink-0">📂</button>
|
|
423
|
+
|
|
424
|
+
{showBrowser && (
|
|
425
|
+
<div className="absolute left-0 right-0 top-full mt-1 z-50 bg-[#0d1117] border border-[#30363d] rounded-lg shadow-xl max-h-60 overflow-hidden flex flex-col" style={{ minWidth: 250 }}>
|
|
426
|
+
<input
|
|
427
|
+
value={search}
|
|
428
|
+
onChange={e => setSearch(e.target.value)}
|
|
429
|
+
placeholder="Search files & dirs..."
|
|
430
|
+
autoFocus
|
|
431
|
+
className="text-[10px] bg-[#161b22] border-b border-[#30363d] px-2 py-1 text-white focus:outline-none"
|
|
432
|
+
/>
|
|
433
|
+
<div className="overflow-y-auto flex-1">
|
|
434
|
+
{search ? (
|
|
435
|
+
// Search results
|
|
436
|
+
filtered.length > 0 ? filtered.map(f => (
|
|
437
|
+
<div key={f} onClick={() => { onChange(f); setShowBrowser(false); setSearch(''); }}
|
|
438
|
+
className="px-2 py-0.5 text-[9px] text-gray-300 hover:bg-[#161b22] cursor-pointer truncate font-mono">
|
|
439
|
+
{f.endsWith('/') ? `📁 ${f}` : `📄 ${f}`}
|
|
440
|
+
</div>
|
|
441
|
+
)) : <div className="px-2 py-1 text-[9px] text-gray-500">No matches</div>
|
|
442
|
+
) : (
|
|
443
|
+
// Tree view (first 2 levels)
|
|
444
|
+
tree.map(n => <PathTreeNode key={n.name} node={n} prefix="" onSelect={p => { onChange(p); setShowBrowser(false); }} />)
|
|
445
|
+
)}
|
|
446
|
+
</div>
|
|
447
|
+
<div className="flex items-center justify-between px-2 py-0.5 border-t border-[#30363d] bg-[#161b22]">
|
|
448
|
+
<span className="text-[8px] text-gray-600">{flatFiles.length} items</span>
|
|
449
|
+
<button onClick={() => setShowBrowser(false)} className="text-[8px] text-gray-500 hover:text-white">Close</button>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function PathTreeNode({ node, prefix, onSelect, depth = 0 }: { node: any; prefix: string; onSelect: (path: string) => void; depth?: number }) {
|
|
458
|
+
const [expanded, setExpanded] = useState(depth < 1);
|
|
459
|
+
const path = prefix ? `${prefix}/${node.name}` : node.name;
|
|
460
|
+
const isDir = node.type === 'dir';
|
|
461
|
+
|
|
462
|
+
if (!isDir && depth > 1) return null; // only show files at top 2 levels
|
|
463
|
+
|
|
464
|
+
return (
|
|
465
|
+
<div>
|
|
466
|
+
<div
|
|
467
|
+
onClick={() => isDir ? setExpanded(!expanded) : onSelect(path)}
|
|
468
|
+
className="flex items-center px-2 py-0.5 text-[9px] hover:bg-[#161b22] cursor-pointer"
|
|
469
|
+
style={{ paddingLeft: 8 + depth * 12 }}
|
|
470
|
+
>
|
|
471
|
+
<span className="text-gray-500 mr-1 w-3">{isDir ? (expanded ? '▼' : '▶') : ''}</span>
|
|
472
|
+
<span className={isDir ? 'text-[var(--accent)]' : 'text-gray-400'}>{isDir ? '📁' : '📄'} {node.name}</span>
|
|
473
|
+
{isDir && (
|
|
474
|
+
<button onClick={e => { e.stopPropagation(); onSelect(path + '/'); }}
|
|
475
|
+
className="ml-auto text-[8px] text-gray-600 hover:text-[var(--accent)]">select</button>
|
|
476
|
+
)}
|
|
477
|
+
</div>
|
|
478
|
+
{isDir && expanded && node.children && depth < 2 && (
|
|
479
|
+
node.children.map((c: any) => <PathTreeNode key={c.name} node={c} prefix={path} onSelect={onSelect} depth={depth + 1} />)
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
382
485
|
// ─── Fixed Session Picker ────────────────────────────────
|
|
383
486
|
|
|
384
487
|
function FixedSessionPicker({ projectPath, value, onChange }: { projectPath?: string; value: string; onChange: (v: string) => void }) {
|
|
@@ -487,14 +590,14 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
487
590
|
fetch(`/api/code?dir=${encodeURIComponent(projectPath)}`)
|
|
488
591
|
.then(r => r.json())
|
|
489
592
|
.then(data => {
|
|
490
|
-
//
|
|
593
|
+
// Collect directories with depth limit (max 2 levels for readability)
|
|
491
594
|
const dirs: string[] = [];
|
|
492
|
-
const walk = (nodes: any[], prefix = '') => {
|
|
595
|
+
const walk = (nodes: any[], prefix = '', depth = 0) => {
|
|
493
596
|
for (const n of nodes || []) {
|
|
494
597
|
if (n.type === 'dir') {
|
|
495
598
|
const path = prefix ? `${prefix}/${n.name}` : n.name;
|
|
496
599
|
dirs.push(path);
|
|
497
|
-
if (n.children) walk(n.children, path);
|
|
600
|
+
if (n.children && depth < 2) walk(n.children, path, depth + 1);
|
|
498
601
|
}
|
|
499
602
|
}
|
|
500
603
|
};
|
|
@@ -785,14 +888,15 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
785
888
|
<option value="agent_status">Agent Status</option>
|
|
786
889
|
</select>
|
|
787
890
|
{t.type === 'directory' && (
|
|
788
|
-
<
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
891
|
+
<WatchPathPicker
|
|
892
|
+
value={t.path || ''}
|
|
893
|
+
projectPath={projectPath || ''}
|
|
894
|
+
onChange={v => {
|
|
895
|
+
const next = [...watchTargets];
|
|
896
|
+
next[i] = { ...t, path: v };
|
|
897
|
+
setWatchTargets(next);
|
|
898
|
+
}}
|
|
899
|
+
/>
|
|
796
900
|
)}
|
|
797
901
|
{t.type === 'agent_status' && (<>
|
|
798
902
|
<select value={t.path || ''} onChange={e => {
|
|
@@ -1111,13 +1111,7 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1111
1111
|
if (!entry) return;
|
|
1112
1112
|
if (!this.daemonActive) return;
|
|
1113
1113
|
|
|
1114
|
-
|
|
1115
|
-
if (entry.state.taskStatus !== 'running') {
|
|
1116
|
-
console.log(`[hook] ${entry.config.label}: Stop hook fired but task=${entry.state.taskStatus}, ignoring`);
|
|
1117
|
-
return;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
console.log(`[hook] ${entry.config.label}: Stop hook → done`);
|
|
1114
|
+
console.log(`[hook] ${entry.config.label}: Stop hook → done (was ${entry.state.taskStatus})`);
|
|
1121
1115
|
entry.state.taskStatus = 'done';
|
|
1122
1116
|
entry.state.completedAt = Date.now();
|
|
1123
1117
|
this.emit('event', { type: 'task_status', agentId, taskStatus: 'done' } as any);
|
package/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/types/routes.d.ts";
|
|
3
|
+
import "./.next/dev/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/package.json
CHANGED