@aion0/forge 0.5.8 → 0.5.12
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/CLAUDE.md +7 -0
- package/RELEASE_NOTES.md +7 -128
- package/components/WorkspaceView.tsx +191 -70
- package/lib/agents/index.ts +2 -2
- package/lib/workspace/agent-worker.ts +2 -11
- package/lib/workspace/backends/cli-backend.ts +11 -2
- package/lib/workspace/orchestrator.ts +206 -18
- package/lib/workspace/session-monitor.ts +210 -0
- package/lib/workspace/types.ts +1 -1
- package/lib/workspace/watch-manager.ts +23 -0
- package/lib/workspace-standalone.ts +9 -1
- package/package.json +4 -4
- package/qa/.forge/mcp.json +8 -0
|
@@ -446,13 +446,15 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
446
446
|
const [role, setRole] = useState(initial.role || '');
|
|
447
447
|
const [backend, setBackend] = useState<'api' | 'cli'>(initial.backend === 'api' ? 'api' : 'cli');
|
|
448
448
|
const [agentId, setAgentId] = useState(initial.agentId || 'claude');
|
|
449
|
-
const [availableAgents, setAvailableAgents] = useState<{ id: string; name: string; isProfile?: boolean; backendType?: string }[]>([]);
|
|
449
|
+
const [availableAgents, setAvailableAgents] = useState<{ id: string; name: string; isProfile?: boolean; backendType?: string; base?: string; cliType?: string }[]>([]);
|
|
450
450
|
|
|
451
451
|
useEffect(() => {
|
|
452
452
|
fetch('/api/agents').then(r => r.json()).then(data => {
|
|
453
453
|
const list = (data.agents || data || []).map((a: any) => ({
|
|
454
454
|
id: a.id, name: a.name || a.id,
|
|
455
455
|
isProfile: a.isProfile || a.base,
|
|
456
|
+
base: a.base,
|
|
457
|
+
cliType: a.cliType,
|
|
456
458
|
backendType: a.backendType || 'cli',
|
|
457
459
|
}));
|
|
458
460
|
setAvailableAgents(list);
|
|
@@ -681,24 +683,38 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
681
683
|
<label htmlFor="requiresApproval" className="text-[9px] text-gray-400">Require approval before processing inbox messages</label>
|
|
682
684
|
</div>
|
|
683
685
|
|
|
684
|
-
{/* Persistent Session */}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
686
|
+
{/* Persistent Session — only for claude-code based agents */}
|
|
687
|
+
{(() => {
|
|
688
|
+
// Check if selected agent supports terminal mode (claude-code or its profiles)
|
|
689
|
+
const selectedAgent = availableAgents.find(a => a.id === agentId);
|
|
690
|
+
const isClaude = agentId === 'claude' || selectedAgent?.base === 'claude' || selectedAgent?.cliType === 'claude-code' || !selectedAgent;
|
|
691
|
+
const canTerminal = isClaude || isPrimary;
|
|
692
|
+
return canTerminal ? (
|
|
693
|
+
<>
|
|
694
|
+
<div className="flex items-center gap-2">
|
|
695
|
+
<input type="checkbox" id="persistentSession" checked={persistentSession} onChange={e => !isPrimary && setPersistentSession(e.target.checked)}
|
|
696
|
+
disabled={isPrimary}
|
|
697
|
+
className={`accent-[#3fb950] ${isPrimary ? 'opacity-50 cursor-not-allowed' : ''}`} />
|
|
698
|
+
<label htmlFor="persistentSession" className={`text-[9px] text-gray-400 ${isPrimary ? 'opacity-50' : ''}`}>
|
|
699
|
+
Terminal mode {isPrimary ? '(required for primary)' : '— run in terminal instead of headless'}
|
|
700
|
+
</label>
|
|
701
|
+
</div>
|
|
702
|
+
{persistentSession && (
|
|
703
|
+
<div className="flex flex-col gap-1.5 ml-4">
|
|
704
|
+
<div className="flex items-center gap-2">
|
|
705
|
+
<input type="checkbox" id="skipPermissions" checked={skipPermissions} onChange={e => setSkipPermissions(e.target.checked)}
|
|
706
|
+
className="accent-[#f0883e]" />
|
|
707
|
+
<label htmlFor="skipPermissions" className="text-[9px] text-gray-400">Skip permissions (auto-approve all tool calls)</label>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
)}
|
|
711
|
+
</>
|
|
712
|
+
) : (
|
|
713
|
+
<div className="text-[8px] text-gray-500 bg-gray-500/10 px-2 py-1 rounded">
|
|
714
|
+
Headless mode only — {agentId} does not support terminal mode
|
|
699
715
|
</div>
|
|
700
|
-
|
|
701
|
-
)}
|
|
716
|
+
);
|
|
717
|
+
})()}
|
|
702
718
|
|
|
703
719
|
{/* Steps */}
|
|
704
720
|
<div className="flex flex-col gap-1">
|
|
@@ -766,6 +782,7 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
766
782
|
<option value="agent_log">Agent Log</option>
|
|
767
783
|
<option value="session">Session Output</option>
|
|
768
784
|
<option value="command">Command</option>
|
|
785
|
+
<option value="agent_status">Agent Status</option>
|
|
769
786
|
</select>
|
|
770
787
|
{t.type === 'directory' && (
|
|
771
788
|
<select value={t.path || ''} onChange={e => {
|
|
@@ -777,6 +794,29 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
777
794
|
{projectDirs.map(d => <option key={d} value={d + '/'}>{d}/</option>)}
|
|
778
795
|
</select>
|
|
779
796
|
)}
|
|
797
|
+
{t.type === 'agent_status' && (<>
|
|
798
|
+
<select value={t.path || ''} onChange={e => {
|
|
799
|
+
const next = [...watchTargets];
|
|
800
|
+
next[i] = { ...t, path: e.target.value };
|
|
801
|
+
setWatchTargets(next);
|
|
802
|
+
}} className="text-[10px] bg-[#161b22] border border-[#30363d] rounded px-1 py-0.5 text-white flex-1">
|
|
803
|
+
<option value="">Select agent...</option>
|
|
804
|
+
{existingAgents.filter(a => a.id !== initial.id).map(a =>
|
|
805
|
+
<option key={a.id} value={a.id}>{a.icon} {a.label}</option>
|
|
806
|
+
)}
|
|
807
|
+
</select>
|
|
808
|
+
<select value={t.pattern || ''} onChange={e => {
|
|
809
|
+
const next = [...watchTargets];
|
|
810
|
+
next[i] = { ...t, pattern: e.target.value };
|
|
811
|
+
setWatchTargets(next);
|
|
812
|
+
}} className="text-[10px] bg-[#161b22] border border-[#30363d] rounded px-1 py-0.5 text-white w-20">
|
|
813
|
+
<option value="">Any change</option>
|
|
814
|
+
<option value="done">done</option>
|
|
815
|
+
<option value="failed">failed</option>
|
|
816
|
+
<option value="running">running</option>
|
|
817
|
+
<option value="idle">idle</option>
|
|
818
|
+
</select>
|
|
819
|
+
</>)}
|
|
780
820
|
{t.type === 'agent_output' && (
|
|
781
821
|
<select value={t.path || ''} onChange={e => {
|
|
782
822
|
const next = [...watchTargets];
|
|
@@ -810,7 +850,7 @@ function AgentConfigModal({ initial, mode, existingAgents, projectPath, onConfir
|
|
|
810
850
|
{t.type === 'session' && (
|
|
811
851
|
<SessionTargetSelector
|
|
812
852
|
target={t}
|
|
813
|
-
agents={existingAgents.filter(a => a.id !== initial.id
|
|
853
|
+
agents={existingAgents.filter(a => a.id !== initial.id)}
|
|
814
854
|
projectPath={projectPath}
|
|
815
855
|
onChange={(updated) => {
|
|
816
856
|
const next = [...watchTargets];
|
|
@@ -1357,11 +1397,17 @@ function InboxPanel({ agentId, agentLabel, busLog, agents, workspaceId, onClose
|
|
|
1357
1397
|
</button>
|
|
1358
1398
|
</div>
|
|
1359
1399
|
)}
|
|
1360
|
-
{msg.status === 'pending' && msg.type !== 'ack' && (
|
|
1361
|
-
<
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1400
|
+
{(msg.status === 'pending' || msg.status === 'running') && msg.type !== 'ack' && (
|
|
1401
|
+
<div className="flex gap-1 ml-auto">
|
|
1402
|
+
<button onClick={() => wsApi(workspaceId, 'message_done', { messageId: msg.id })}
|
|
1403
|
+
className="text-[7px] px-1.5 py-0.5 rounded bg-green-600/20 text-green-400 hover:bg-green-600/30">
|
|
1404
|
+
✓ Done
|
|
1405
|
+
</button>
|
|
1406
|
+
<button onClick={() => wsApi(workspaceId, 'abort_message', { messageId: msg.id })}
|
|
1407
|
+
className="text-[7px] px-1.5 py-0.5 rounded bg-red-600/20 text-red-400 hover:bg-red-600/30">
|
|
1408
|
+
✕ Abort
|
|
1409
|
+
</button>
|
|
1410
|
+
</div>
|
|
1365
1411
|
)}
|
|
1366
1412
|
{(msg.status === 'done' || msg.status === 'failed') && msg.type !== 'ack' && (
|
|
1367
1413
|
<div className="flex gap-1 ml-auto">
|
|
@@ -1735,13 +1781,18 @@ function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId
|
|
|
1735
1781
|
const envWithoutModel = profileEnv ? Object.fromEntries(
|
|
1736
1782
|
Object.entries(profileEnv).filter(([k]) => k !== 'CLAUDE_MODEL')
|
|
1737
1783
|
) : {};
|
|
1738
|
-
//
|
|
1784
|
+
// Build commands as separate short lines
|
|
1785
|
+
const commands: string[] = [];
|
|
1739
1786
|
const profileVarsToReset = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_SMALL_FAST_MODEL', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', 'DISABLE_TELEMETRY', 'DISABLE_ERROR_REPORTING', 'DISABLE_AUTOUPDATER', 'DISABLE_NON_ESSENTIAL_MODEL_CALLS', 'CLAUDE_MODEL'];
|
|
1740
|
-
|
|
1741
|
-
const
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1787
|
+
commands.push(profileVarsToReset.map(v => `unset ${v}`).join('; '));
|
|
1788
|
+
const envWithoutForge = Object.entries(envWithoutModel).filter(([k]) => !k.startsWith('FORGE_'));
|
|
1789
|
+
if (envWithoutForge.length > 0) {
|
|
1790
|
+
commands.push(envWithoutForge.map(([k, v]) => `export ${k}="${v}"`).join('; '));
|
|
1791
|
+
}
|
|
1792
|
+
const forgeVars = Object.entries(envWithoutModel).filter(([k]) => k.startsWith('FORGE_'));
|
|
1793
|
+
if (forgeVars.length > 0) {
|
|
1794
|
+
commands.push(forgeVars.map(([k, v]) => `export ${k}="${v}"`).join('; '));
|
|
1795
|
+
}
|
|
1745
1796
|
let resumeId = resumeSessionId || boundSessionId;
|
|
1746
1797
|
if (isClaude && !resumeId && isPrimary) {
|
|
1747
1798
|
try {
|
|
@@ -1753,10 +1804,12 @@ function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId
|
|
|
1753
1804
|
let mcpFlag = '';
|
|
1754
1805
|
if (isClaude) { try { const { getMcpFlag } = await import('@/lib/session-utils'); mcpFlag = await getMcpFlag(projectPath); } catch {} }
|
|
1755
1806
|
const sf = skipPermissions ? (cliType === 'codex' ? ' --full-auto' : cliType === 'aider' ? ' --yes' : ' --dangerously-skip-permissions') : '';
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1807
|
+
commands.push(`${cdCmd} && ${cli}${resumeFlag}${modelFlag}${sf}${mcpFlag}`);
|
|
1808
|
+
commands.forEach((cmd, i) => {
|
|
1809
|
+
setTimeout(() => {
|
|
1810
|
+
if (!disposed && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'input', data: cmd + '\n' }));
|
|
1811
|
+
}, 300 + i * 300);
|
|
1812
|
+
});
|
|
1760
1813
|
}
|
|
1761
1814
|
}
|
|
1762
1815
|
} catch {}
|
|
@@ -1779,7 +1832,7 @@ function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId
|
|
|
1779
1832
|
return <div ref={containerRef} className="w-full h-full" style={{ background: '#0d1117' }} />;
|
|
1780
1833
|
}
|
|
1781
1834
|
|
|
1782
|
-
function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, persistentSession, boundSessionId, onSessionReady, onClose }: {
|
|
1835
|
+
function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, persistentSession, boundSessionId, initialPos, onSessionReady, onClose }: {
|
|
1783
1836
|
agentLabel: string;
|
|
1784
1837
|
agentIcon: string;
|
|
1785
1838
|
projectPath: string;
|
|
@@ -1796,14 +1849,20 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1796
1849
|
skipPermissions?: boolean;
|
|
1797
1850
|
persistentSession?: boolean;
|
|
1798
1851
|
boundSessionId?: string;
|
|
1852
|
+
initialPos?: { x: number; y: number };
|
|
1799
1853
|
onSessionReady?: (name: string) => void;
|
|
1800
1854
|
onClose: (killSession: boolean) => void;
|
|
1801
1855
|
}) {
|
|
1802
1856
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
1803
1857
|
const wsRef = useRef<WebSocket | null>(null);
|
|
1804
1858
|
const sessionNameRef = useRef('');
|
|
1805
|
-
const [pos, setPos] = useState({ x: 80, y: 60 });
|
|
1806
|
-
const [
|
|
1859
|
+
const [pos, setPos] = useState(initialPos || { x: 80, y: 60 });
|
|
1860
|
+
const [userDragged, setUserDragged] = useState(false);
|
|
1861
|
+
// Follow node position unless user manually dragged the terminal
|
|
1862
|
+
useEffect(() => {
|
|
1863
|
+
if (initialPos && !userDragged) setPos(initialPos);
|
|
1864
|
+
}, [initialPos?.x, initialPos?.y]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1865
|
+
const [size, setSize] = useState({ w: 500, h: 300 });
|
|
1807
1866
|
const [showCloseDialog, setShowCloseDialog] = useState(false);
|
|
1808
1867
|
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
|
1809
1868
|
const resizeRef = useRef<{ startX: number; startY: number; origW: number; origH: number } | null>(null);
|
|
@@ -1821,7 +1880,7 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1821
1880
|
if (disposed) return;
|
|
1822
1881
|
|
|
1823
1882
|
const term = new Terminal({
|
|
1824
|
-
cursorBlink: true, fontSize:
|
|
1883
|
+
cursorBlink: true, fontSize: 10,
|
|
1825
1884
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
1826
1885
|
scrollback: 5000,
|
|
1827
1886
|
theme: { background: '#0d1117', foreground: '#c9d1d9', cursor: '#58a6ff' },
|
|
@@ -1831,7 +1890,15 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1831
1890
|
term.open(el);
|
|
1832
1891
|
setTimeout(() => { try { fitAddon.fit(); } catch {} }, 100);
|
|
1833
1892
|
|
|
1834
|
-
|
|
1893
|
+
// Scale font: min 10 at small size, max 13 at large size
|
|
1894
|
+
const ro = new ResizeObserver(() => {
|
|
1895
|
+
try {
|
|
1896
|
+
const w = el.clientWidth;
|
|
1897
|
+
const newSize = Math.min(13, Math.max(10, Math.floor(w / 60)));
|
|
1898
|
+
if (term.options.fontSize !== newSize) term.options.fontSize = newSize;
|
|
1899
|
+
fitAddon.fit();
|
|
1900
|
+
} catch {}
|
|
1901
|
+
});
|
|
1835
1902
|
ro.observe(el);
|
|
1836
1903
|
|
|
1837
1904
|
// Connect WebSocket — attach to existing or create new
|
|
@@ -1900,14 +1967,26 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1900
1967
|
const envWithoutModel = profileEnv ? Object.fromEntries(
|
|
1901
1968
|
Object.entries(profileEnv).filter(([k]) => k !== 'CLAUDE_MODEL')
|
|
1902
1969
|
) : {};
|
|
1903
|
-
//
|
|
1970
|
+
// Build commands as separate short lines to avoid terminal truncation
|
|
1971
|
+
const commands: string[] = [];
|
|
1972
|
+
|
|
1973
|
+
// 1. Unset old profile vars
|
|
1904
1974
|
const profileVarsToReset = ['ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_SMALL_FAST_MODEL', 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', 'DISABLE_TELEMETRY', 'DISABLE_ERROR_REPORTING', 'DISABLE_AUTOUPDATER', 'DISABLE_NON_ESSENTIAL_MODEL_CALLS', 'CLAUDE_MODEL'];
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1975
|
+
commands.push(profileVarsToReset.map(v => `unset ${v}`).join('; '));
|
|
1976
|
+
|
|
1977
|
+
// 2. Export new profile vars (if any)
|
|
1978
|
+
const envWithoutForge = Object.entries(envWithoutModel).filter(([k]) => !k.startsWith('FORGE_'));
|
|
1979
|
+
if (envWithoutForge.length > 0) {
|
|
1980
|
+
commands.push(envWithoutForge.map(([k, v]) => `export ${k}="${v}"`).join('; '));
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// 3. Export FORGE vars
|
|
1984
|
+
const forgeVars = Object.entries(envWithoutModel).filter(([k]) => k.startsWith('FORGE_'));
|
|
1985
|
+
if (forgeVars.length > 0) {
|
|
1986
|
+
commands.push(forgeVars.map(([k, v]) => `export ${k}="${v}"`).join('; '));
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// 4. CLI command
|
|
1911
1990
|
let resumeId = resumeSessionId || boundSessionId;
|
|
1912
1991
|
if (isClaude && !resumeId && isPrimary) {
|
|
1913
1992
|
try {
|
|
@@ -1918,11 +1997,15 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1918
1997
|
const resumeFlag = isClaude && resumeId ? ` --resume ${resumeId}` : '';
|
|
1919
1998
|
let mcpFlag = '';
|
|
1920
1999
|
if (isClaude) { try { const { getMcpFlag } = await import('@/lib/session-utils'); mcpFlag = await getMcpFlag(projectPath); } catch {} }
|
|
1921
|
-
const sf = skipPermissions ? ' --dangerously-skip-permissions' : '';
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2000
|
+
const sf = skipPermissions ? (cliType === 'codex' ? ' --full-auto' : cliType === 'aider' ? ' --yes' : ' --dangerously-skip-permissions') : '';
|
|
2001
|
+
commands.push(`${cdCmd} && ${cli}${resumeFlag}${modelFlag}${sf}${mcpFlag}`);
|
|
2002
|
+
|
|
2003
|
+
// Send each command with delay between them
|
|
2004
|
+
commands.forEach((cmd, i) => {
|
|
2005
|
+
setTimeout(() => {
|
|
2006
|
+
if (!disposed && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'input', data: cmd + '\n' }));
|
|
2007
|
+
}, 300 + i * 300);
|
|
2008
|
+
});
|
|
1926
2009
|
}
|
|
1927
2010
|
} catch {}
|
|
1928
2011
|
};
|
|
@@ -1956,6 +2039,7 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1956
2039
|
onMouseDown={(e) => {
|
|
1957
2040
|
e.preventDefault();
|
|
1958
2041
|
dragRef.current = { startX: e.clientX, startY: e.clientY, origX: pos.x, origY: pos.y };
|
|
2042
|
+
setUserDragged(true);
|
|
1959
2043
|
const onMove = (ev: MouseEvent) => {
|
|
1960
2044
|
if (!dragRef.current) return;
|
|
1961
2045
|
setPos({ x: Math.max(0, dragRef.current.origX + ev.clientX - dragRef.current.startX), y: Math.max(0, dragRef.current.origY + ev.clientY - dragRef.current.startY) });
|
|
@@ -2113,6 +2197,8 @@ interface AgentNodeData {
|
|
|
2113
2197
|
state: AgentState;
|
|
2114
2198
|
colorIdx: number;
|
|
2115
2199
|
previewLines: string[];
|
|
2200
|
+
projectPath: string;
|
|
2201
|
+
workspaceId: string | null;
|
|
2116
2202
|
onRun: () => void;
|
|
2117
2203
|
onPause: () => void;
|
|
2118
2204
|
onStop: () => void;
|
|
@@ -2131,8 +2217,11 @@ interface AgentNodeData {
|
|
|
2131
2217
|
[key: string]: unknown;
|
|
2132
2218
|
}
|
|
2133
2219
|
|
|
2220
|
+
// PortalTerminal/NodeTerminal removed — xterm cannot render inside React Flow nodes
|
|
2221
|
+
// and createPortal causes event routing issues. Using FloatingTerminal instead.
|
|
2222
|
+
|
|
2134
2223
|
function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
2135
|
-
const { config, state, colorIdx, previewLines, onRun, onPause, onStop, onRetry, onEdit, onRemove, onMessage, onApprove, onShowLog, onShowMemory, onShowInbox, onOpenTerminal, onSwitchSession, inboxPending = 0, inboxFailed = 0 } = data;
|
|
2224
|
+
const { config, state, colorIdx, previewLines, projectPath, workspaceId, onRun, onPause, onStop, onRetry, onEdit, onRemove, onMessage, onApprove, onShowLog, onShowMemory, onShowInbox, onOpenTerminal, onSwitchSession, inboxPending = 0, inboxFailed = 0 } = data;
|
|
2136
2225
|
const c = COLORS[colorIdx % COLORS.length];
|
|
2137
2226
|
const smithStatus = state?.smithStatus || 'down';
|
|
2138
2227
|
const taskStatus = state?.taskStatus || 'idle';
|
|
@@ -2141,7 +2230,7 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
|
2141
2230
|
const taskInfo = TASK_STATUS[taskStatus] || TASK_STATUS.idle;
|
|
2142
2231
|
const currentStep = state?.currentStep;
|
|
2143
2232
|
const step = currentStep !== undefined ? config.steps[currentStep] : undefined;
|
|
2144
|
-
const isApprovalPending = taskStatus === 'idle' && smithStatus === 'active';
|
|
2233
|
+
const isApprovalPending = taskStatus === 'idle' && smithStatus === 'active';
|
|
2145
2234
|
|
|
2146
2235
|
return (
|
|
2147
2236
|
<div className="w-52 flex flex-col rounded-lg select-none"
|
|
@@ -2170,8 +2259,13 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
|
2170
2259
|
{(() => {
|
|
2171
2260
|
// Execution mode is determined by config, not tmux state
|
|
2172
2261
|
const isTerminalMode = config.persistentSession;
|
|
2173
|
-
const
|
|
2174
|
-
const
|
|
2262
|
+
const isActive = smithStatus === 'active';
|
|
2263
|
+
const color = isTerminalMode
|
|
2264
|
+
? (hasTmux ? '#3fb950' : '#f0883e') // terminal: green (up) / orange (down)
|
|
2265
|
+
: (isActive ? '#58a6ff' : '#484f58'); // headless: blue (active) / gray (down)
|
|
2266
|
+
const label = isTerminalMode
|
|
2267
|
+
? (hasTmux ? 'terminal' : 'terminal (down)')
|
|
2268
|
+
: (isActive ? 'headless' : 'headless (down)');
|
|
2175
2269
|
return (<>
|
|
2176
2270
|
<div className="w-1.5 h-1.5 rounded-full" style={{ background: color }} />
|
|
2177
2271
|
<span className="text-[7px] font-medium" style={{ color }}>{label}</span>
|
|
@@ -2242,7 +2336,8 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
|
2242
2336
|
<div className="flex-1" />
|
|
2243
2337
|
<span className="flex items-center">
|
|
2244
2338
|
<button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); onOpenTerminal(); }}
|
|
2245
|
-
className={`text-[9px] px-1 ${hasTmux && taskStatus === 'running' ? 'text-green-400 animate-pulse' : 'text-gray-600 hover:text-green-400'}`}
|
|
2339
|
+
className={`text-[9px] px-1 ${hasTmux && taskStatus === 'running' ? 'text-green-400 animate-pulse' : 'text-gray-600 hover:text-green-400'}`}
|
|
2340
|
+
title="Open terminal">⌨️</button>
|
|
2246
2341
|
{hasTmux && !config.primary && (
|
|
2247
2342
|
<button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); onSwitchSession(); }}
|
|
2248
2343
|
className="text-[10px] text-gray-600 hover:text-yellow-400 px-0.5 py-0.5" title="Switch session">▾</button>
|
|
@@ -2287,8 +2382,8 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2287
2382
|
const [memoryTarget, setMemoryTarget] = useState<{ id: string; label: string } | null>(null);
|
|
2288
2383
|
const [inboxTarget, setInboxTarget] = useState<{ id: string; label: string } | null>(null);
|
|
2289
2384
|
const [showBusPanel, setShowBusPanel] = useState(false);
|
|
2290
|
-
const [floatingTerminals, setFloatingTerminals] = useState<{ agentId: string; label: string; icon: string; cliId: string; cliCmd?: string; cliType?: string; workDir?: string; tmuxSession?: string; sessionName: string; resumeMode?: boolean; resumeSessionId?: string; profileEnv?: Record<string, string>; isPrimary?: boolean; skipPermissions?: boolean; persistentSession?: boolean; boundSessionId?: string }[]>([]);
|
|
2291
|
-
const [termLaunchDialog, setTermLaunchDialog] = useState<{ agent: AgentConfig; sessName: string; workDir?: string; sessions: string[]; supportsSession?: boolean } | null>(null);
|
|
2385
|
+
const [floatingTerminals, setFloatingTerminals] = useState<{ agentId: string; label: string; icon: string; cliId: string; cliCmd?: string; cliType?: string; workDir?: string; tmuxSession?: string; sessionName: string; resumeMode?: boolean; resumeSessionId?: string; profileEnv?: Record<string, string>; isPrimary?: boolean; skipPermissions?: boolean; persistentSession?: boolean; boundSessionId?: string; initialPos?: { x: number; y: number } }[]>([]);
|
|
2386
|
+
const [termLaunchDialog, setTermLaunchDialog] = useState<{ agent: AgentConfig; sessName: string; workDir?: string; sessions: string[]; supportsSession?: boolean; initialPos?: { x: number; y: number } } | null>(null);
|
|
2292
2387
|
|
|
2293
2388
|
// Expose focusAgent to parent
|
|
2294
2389
|
useImperativeHandle(ref, () => ({
|
|
@@ -2367,6 +2462,8 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2367
2462
|
state: states[agent.id] || { smithStatus: 'down', taskStatus: 'idle', artifacts: [] },
|
|
2368
2463
|
colorIdx: i,
|
|
2369
2464
|
previewLines: logPreview[agent.id] || [],
|
|
2465
|
+
projectPath,
|
|
2466
|
+
workspaceId,
|
|
2370
2467
|
onRun: () => {
|
|
2371
2468
|
wsApi(workspaceId!, 'run', { agentId: agent.id });
|
|
2372
2469
|
},
|
|
@@ -2387,13 +2484,19 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2387
2484
|
inboxFailed: busLog.filter(m => m.to === agent.id && m.status === 'failed' && m.type !== 'ack').length,
|
|
2388
2485
|
onOpenTerminal: async () => {
|
|
2389
2486
|
if (!workspaceId) return;
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
}
|
|
2487
|
+
// Sync stale daemonActiveFromStream from agent states
|
|
2488
|
+
const anyActive = Object.values(states).some(s => s?.smithStatus === 'active');
|
|
2489
|
+
if (anyActive && !daemonActiveFromStream) setDaemonActiveFromStream(true);
|
|
2394
2490
|
// Close existing terminal (config may have changed)
|
|
2395
2491
|
setFloatingTerminals(prev => prev.filter(t => t.agentId !== agent.id));
|
|
2396
2492
|
|
|
2493
|
+
// Get node screen position for initial terminal placement
|
|
2494
|
+
const nodeEl = document.querySelector(`[data-id="${agent.id}"]`);
|
|
2495
|
+
const nodeRect = nodeEl?.getBoundingClientRect();
|
|
2496
|
+
const initialPos = nodeRect
|
|
2497
|
+
? { x: nodeRect.left, y: nodeRect.bottom + 4 }
|
|
2498
|
+
: { x: 80, y: 60 };
|
|
2499
|
+
|
|
2397
2500
|
const agentState = states[agent.id];
|
|
2398
2501
|
const existingTmux = agentState?.tmuxSession;
|
|
2399
2502
|
const safeName = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 20);
|
|
@@ -2421,7 +2524,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2421
2524
|
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2422
2525
|
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2423
2526
|
tmuxSession: existingTmux, sessionName: sessName,
|
|
2424
|
-
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId,
|
|
2527
|
+
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2425
2528
|
}]);
|
|
2426
2529
|
return;
|
|
2427
2530
|
}
|
|
@@ -2433,7 +2536,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2433
2536
|
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2434
2537
|
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2435
2538
|
tmuxSession: res?.tmuxSession || sessName, sessionName: sessName,
|
|
2436
|
-
isPrimary: true, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId,
|
|
2539
|
+
isPrimary: true, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2437
2540
|
}]);
|
|
2438
2541
|
return;
|
|
2439
2542
|
}
|
|
@@ -2446,24 +2549,25 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2446
2549
|
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2447
2550
|
tmuxSession: res?.tmuxSession || sessName, sessionName: sessName,
|
|
2448
2551
|
resumeSessionId: agent.boundSessionId,
|
|
2449
|
-
isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId,
|
|
2552
|
+
isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2450
2553
|
}]);
|
|
2451
2554
|
return;
|
|
2452
2555
|
}
|
|
2453
2556
|
// No bound session → show launch dialog (New / Resume / Select)
|
|
2454
|
-
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true });
|
|
2557
|
+
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true, initialPos });
|
|
2455
2558
|
},
|
|
2456
2559
|
onSwitchSession: async () => {
|
|
2457
2560
|
if (!workspaceId) return;
|
|
2458
|
-
// Close existing terminal
|
|
2459
2561
|
setFloatingTerminals(prev => prev.filter(t => t.agentId !== agent.id));
|
|
2460
2562
|
if (agent.id) wsApi(workspaceId, 'close_terminal', { agentId: agent.id });
|
|
2461
|
-
|
|
2563
|
+
const nodeEl = document.querySelector(`[data-id="${agent.id}"]`);
|
|
2564
|
+
const nodeRect = nodeEl?.getBoundingClientRect();
|
|
2565
|
+
const switchPos = nodeRect ? { x: nodeRect.left, y: nodeRect.bottom + 4 } : { x: 80, y: 60 };
|
|
2462
2566
|
const safeName = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 20);
|
|
2463
2567
|
const sessName = `mw-forge-${safeName(projectName)}-${safeName(agent.label)}`;
|
|
2464
2568
|
const workDir = agent.workDir && agent.workDir !== './' && agent.workDir !== '.' ? agent.workDir : undefined;
|
|
2465
2569
|
const resolveRes = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id, resolveOnly: true }).catch(() => ({})) as any;
|
|
2466
|
-
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true });
|
|
2570
|
+
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true, initialPos: switchPos });
|
|
2467
2571
|
},
|
|
2468
2572
|
} satisfies AgentNodeData,
|
|
2469
2573
|
};
|
|
@@ -2716,6 +2820,22 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2716
2820
|
nodes={rfNodes}
|
|
2717
2821
|
edges={rfEdges}
|
|
2718
2822
|
onNodesChange={onNodesChange}
|
|
2823
|
+
onNodeDragStop={() => {
|
|
2824
|
+
// Reposition terminals to follow their nodes
|
|
2825
|
+
setFloatingTerminals(prev => prev.map(ft => {
|
|
2826
|
+
const nodeEl = document.querySelector(`[data-id="${ft.agentId}"]`);
|
|
2827
|
+
const rect = nodeEl?.getBoundingClientRect();
|
|
2828
|
+
return rect ? { ...ft, initialPos: { x: rect.left, y: rect.bottom + 4 } } : ft;
|
|
2829
|
+
}));
|
|
2830
|
+
}}
|
|
2831
|
+
onMoveEnd={() => {
|
|
2832
|
+
// Reposition after pan/zoom
|
|
2833
|
+
setFloatingTerminals(prev => prev.map(ft => {
|
|
2834
|
+
const nodeEl = document.querySelector(`[data-id="${ft.agentId}"]`);
|
|
2835
|
+
const rect = nodeEl?.getBoundingClientRect();
|
|
2836
|
+
return rect ? { ...ft, initialPos: { x: rect.left, y: rect.bottom + 4 } } : ft;
|
|
2837
|
+
}));
|
|
2838
|
+
}}
|
|
2719
2839
|
nodeTypes={nodeTypes}
|
|
2720
2840
|
fitView
|
|
2721
2841
|
fitViewOptions={{ padding: 0.3 }}
|
|
@@ -2826,7 +2946,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2826
2946
|
cliCmd: res.cliCmd || 'claude',
|
|
2827
2947
|
cliType: res.cliType || 'claude-code',
|
|
2828
2948
|
workDir,
|
|
2829
|
-
sessionName: sessName, resumeMode, resumeSessionId: sessionId, isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId,
|
|
2949
|
+
sessionName: sessName, resumeMode, resumeSessionId: sessionId, isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId, initialPos: termLaunchDialog.initialPos,
|
|
2830
2950
|
profileEnv: {
|
|
2831
2951
|
...(res.env || {}),
|
|
2832
2952
|
...(res.model ? { CLAUDE_MODEL: res.model } : {}),
|
|
@@ -2861,6 +2981,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2861
2981
|
skipPermissions={ft.skipPermissions}
|
|
2862
2982
|
persistentSession={ft.persistentSession}
|
|
2863
2983
|
boundSessionId={ft.boundSessionId}
|
|
2984
|
+
initialPos={ft.initialPos}
|
|
2864
2985
|
onSessionReady={(name) => {
|
|
2865
2986
|
if (workspaceId) wsApi(workspaceId, 'set_tmux_session', { agentId: ft.agentId, sessionName: name });
|
|
2866
2987
|
setFloatingTerminals(prev => prev.map(t => t.agentId === ft.agentId ? { ...t, tmuxSession: name } : t));
|
package/lib/agents/index.ts
CHANGED
|
@@ -27,9 +27,9 @@ export function listAgents(): AgentConfig[] {
|
|
|
27
27
|
|
|
28
28
|
// Codex
|
|
29
29
|
const codexConfig = settings.agents?.codex;
|
|
30
|
-
const codex = detectAgent('codex', 'OpenAI Codex', codexConfig?.path || 'codex');
|
|
30
|
+
const codex = detectAgent('codex', 'OpenAI Codex', codexConfig?.path || 'codex', ['exec']);
|
|
31
31
|
if (codex) {
|
|
32
|
-
codex.capabilities.requiresTTY =
|
|
32
|
+
codex.capabilities.requiresTTY = false; // exec subcommand is non-interactive
|
|
33
33
|
agents.push({ ...codex, enabled: codexConfig?.enabled !== false, detected: true, skipPermissionsFlag: codexConfig?.skipPermissionsFlag || '--full-auto', cliType: 'codex' } as any);
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -444,20 +444,11 @@ export class AgentWorker extends EventEmitter {
|
|
|
444
444
|
this.state.history.push(msg);
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
-
// Execute using
|
|
447
|
+
// Execute using step template, or create one from the prompt if no steps defined
|
|
448
448
|
const stepTemplate = this.config.steps[this.config.steps.length - 1] || this.config.steps[0];
|
|
449
|
-
if (!stepTemplate) {
|
|
450
|
-
// No steps defined — just log
|
|
451
|
-
this.state.history.push({
|
|
452
|
-
type: 'system', subtype: 'daemon',
|
|
453
|
-
content: `[Daemon] Wake: ${reason.type} — no steps defined to execute`,
|
|
454
|
-
timestamp: new Date().toISOString(),
|
|
455
|
-
});
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
449
|
|
|
459
450
|
const step = {
|
|
460
|
-
...stepTemplate,
|
|
451
|
+
...(stepTemplate || { id: '', label: '', prompt: '' }),
|
|
461
452
|
id: `daemon-${this.state.daemonIteration}`,
|
|
462
453
|
label: `Daemon iteration ${this.state.daemonIteration}`,
|
|
463
454
|
prompt,
|
|
@@ -91,6 +91,7 @@ function detectArtifacts(parsed: any): Artifact[] {
|
|
|
91
91
|
export class CliBackend implements AgentBackend {
|
|
92
92
|
private child: ChildProcess | null = null;
|
|
93
93
|
private sessionId: string | undefined;
|
|
94
|
+
headlessSessionId: string | undefined; // exposed for session file monitoring
|
|
94
95
|
/** Callback to persist sessionId back to agent state */
|
|
95
96
|
onSessionId?: (id: string) => void;
|
|
96
97
|
|
|
@@ -118,6 +119,12 @@ export class CliBackend implements AgentBackend {
|
|
|
118
119
|
// Note: if no sessionId, each execution starts a new session (no resume).
|
|
119
120
|
// To maintain context, user can enable persistent terminal session per agent.
|
|
120
121
|
|
|
122
|
+
// Generate a session ID for headless execution so we can monitor the .jsonl file
|
|
123
|
+
const isClaude = adapter.config.type === 'claude-code';
|
|
124
|
+
if (isClaude && !this.sessionId) {
|
|
125
|
+
this.headlessSessionId = crypto.randomUUID();
|
|
126
|
+
}
|
|
127
|
+
|
|
121
128
|
const spawnOpts = adapter.buildTaskSpawn({
|
|
122
129
|
projectPath,
|
|
123
130
|
prompt,
|
|
@@ -125,6 +132,7 @@ export class CliBackend implements AgentBackend {
|
|
|
125
132
|
conversationId: this.sessionId,
|
|
126
133
|
skipPermissions: true,
|
|
127
134
|
outputFormat: adapter.config.capabilities?.supportsStreamJson ? 'stream-json' : undefined,
|
|
135
|
+
extraFlags: this.headlessSessionId && !this.sessionId ? ['--session-id', this.headlessSessionId] : undefined,
|
|
128
136
|
});
|
|
129
137
|
|
|
130
138
|
onLog?.({
|
|
@@ -148,9 +156,10 @@ export class CliBackend implements AgentBackend {
|
|
|
148
156
|
};
|
|
149
157
|
delete env.CLAUDECODE;
|
|
150
158
|
|
|
151
|
-
// Check if agent needs TTY
|
|
159
|
+
// Check if agent needs TTY — use cliType not agentId
|
|
160
|
+
const cliType = (adapter.config as any).cliType || adapter.config.type;
|
|
152
161
|
const needsTTY = adapter.config.capabilities?.requiresTTY
|
|
153
|
-
||
|
|
162
|
+
|| cliType === 'codex';
|
|
154
163
|
|
|
155
164
|
if (needsTTY) {
|
|
156
165
|
this.executePTY(spawnOpts, projectPath, env, onLog, abortSignal, resolve, reject);
|