@aion0/forge 0.5.8 → 0.5.9
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 +6 -125
- package/components/WorkspaceView.tsx +66 -18
- package/lib/workspace/orchestrator.ts +37 -5
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/qa/.forge/mcp.json +8 -0
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,134 +1,15 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.9
|
|
2
2
|
|
|
3
3
|
Released: 2026-03-30
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.8
|
|
6
6
|
|
|
7
7
|
### Features
|
|
8
|
-
- feat:
|
|
9
|
-
- feat: boundSessionId drives non-primary terminal open flow
|
|
10
|
-
- feat: boundSessionId per agent — each agent has its own last session
|
|
11
|
-
- feat: execution mode determined by config, not runtime tmux state
|
|
12
|
-
- feat: /resolve API for workspace lookup + skills use env var → API fallback
|
|
13
|
-
- feat: switch session button (▾) for non-primary agents with active terminal
|
|
14
|
-
- feat: auto-fallback to headless when terminal startup fails
|
|
15
|
-
- feat: Forge agent monitors all inbox states
|
|
16
|
-
- feat: Forge agent — autonomous bus monitor with periodic scanning
|
|
17
|
-
- feat: Forge agent auto-requests summary when agent completes without reply
|
|
18
|
-
- feat: nudge sender when target agent completes — inject hint into terminal
|
|
19
|
-
- feat: restore auto-reply on completion + keep check_outbox as supplement
|
|
20
|
-
- feat: auto-reply to message sender on completion + check_outbox tool
|
|
21
|
-
- feat: add MCP Server to monitor panel and status script
|
|
22
|
-
- feat: get_agents tool + fuzzy send_message matching
|
|
23
|
-
- feat: inject FORGE env vars at all terminal launch points
|
|
24
|
-
- feat: --mcp-config injected at all claude terminal launch points
|
|
25
|
-
- feat: Forge MCP Server — replace HTTP skills with native tool calls
|
|
26
|
-
- feat: open terminal button on session list items
|
|
27
|
-
- feat: detect CLI startup failures in persistent sessions
|
|
28
|
-
- feat: all terminal entry points use fixedSessionId via --resume
|
|
29
|
-
- feat: bind button on sessions list to set fixedSessionId
|
|
30
|
-
- feat: click-to-change session binding in project header
|
|
31
|
-
- feat: show bound session in project header and session list
|
|
32
|
-
- feat: session picker dropdown for fixedSessionId binding
|
|
33
|
-
- feat: session list shows full ID on expand with copy button
|
|
34
|
-
- feat: show and edit fixedSessionId in agent config modal
|
|
35
|
-
- feat: primary agent setup prompt for new and existing workspaces
|
|
36
|
-
- feat: primary agent checkbox in agent config modal
|
|
37
|
-
- feat: terminal dock — inline right panel with tabs and resizable width
|
|
38
|
-
- feat: VibeCoding uses workspace primary agent's fixed session
|
|
39
|
-
- feat: fixed CLI session binding for primary agent
|
|
40
|
-
- feat: primary agent — terminal-only, fixed session, root directory
|
|
41
|
-
- feat: persistent terminal session per agent
|
|
42
|
-
- feat: watch send_message auto-detects tmux sessions (workspace + VibeCoding)
|
|
43
|
-
- feat: watch send_message injects directly into terminal session
|
|
8
|
+
- feat: terminal floats below agent node, follows on drag/zoom
|
|
44
9
|
|
|
45
10
|
### Bug Fixes
|
|
46
|
-
- fix:
|
|
47
|
-
- fix:
|
|
48
|
-
- fix: session list uses agent's workDir, not project root
|
|
49
|
-
- fix: clear boundSessionId on agent change even without tmux session
|
|
50
|
-
- fix: kill terminal and clear boundSessionId when agent CLI changes
|
|
51
|
-
- fix: rename "Keep terminal session alive" to "Terminal mode"
|
|
52
|
-
- fix: codex skipPermissionsFlag back to --full-auto
|
|
53
|
-
- fix: codex skip flag + -c only when sessions exist
|
|
54
|
-
- fix: set FORGE env vars as separate tmux command before CLI start
|
|
55
|
-
- fix: set FORGE env vars in tmux, skills use env vars not curl lookup
|
|
56
|
-
- fix: mark forge skills as FALLBACK so MCP tools take priority
|
|
57
|
-
- fix: nudge explicitly says use MCP tool, not forge-send skill
|
|
58
|
-
- fix: skills prefer MCP tools, fix subdirectory workspace lookup
|
|
59
|
-
- fix: show headless after Kill, terminal (pending) only before daemon start
|
|
60
|
-
- fix: Suspend vs Kill terminal behavior
|
|
61
|
-
- fix: Forge agent nudge uses stronger language to trigger send_message
|
|
62
|
-
- fix: always resolve agent launch info before opening terminal
|
|
63
|
-
- fix: bus log updates immediately after message delete (no refresh needed)
|
|
64
|
-
- fix: stop daemon kills tmux sessions + Forge agent only scans new messages
|
|
65
|
-
- fix: enlarge session switch button (▾) for easier clicking
|
|
66
|
-
- fix: terminal button visible during running state (was hidden)
|
|
67
|
-
- fix: daemon persistent session uses -c for non-primary agents
|
|
68
|
-
- fix: restore -c flag for non-primary agents to resume latest session
|
|
69
|
-
- fix: Forge agent processes historical pending/running, skips done/failed
|
|
70
|
-
- fix: Forge agent only scans messages after daemon start, ignores history
|
|
71
|
-
- fix: Forge agent restarts message loop for stuck pending messages
|
|
72
|
-
- fix: add skipPermissions to FloatingTerminalInline (was missing)
|
|
73
|
-
- fix: pass skipPermissions to FloatingTerminal, add --dangerously-skip-permissions
|
|
74
|
-
- fix: block terminal open when daemon not started
|
|
75
|
-
- fix: re-open terminal when agent config changes (close old first)
|
|
76
|
-
- fix: don't use claude -c for workspace agents (subdirs may have no session)
|
|
77
|
-
- fix: MCP monitor shows port not pid, add separator after Tunnel
|
|
78
|
-
- fix: add type:'sse' to MCP config (required by Claude Code schema)
|
|
79
|
-
- fix: MCP server port 7830 → 8406 (follows 8403/8404/8405 sequence)
|
|
80
|
-
- fix: MCP agentId resolved by server, not hardcoded in all URLs
|
|
81
|
-
- fix: MAX_ACTIVE limits daemon start, not workspace loading
|
|
82
|
-
- fix: increase MAX_ACTIVE workspaces from 2 to 5
|
|
83
|
-
- fix: move all session action buttons to second row (right-aligned)
|
|
84
|
-
- fix: skip message loop when persistent session fails to start
|
|
85
|
-
- fix: only primary agent uses fixedSession, others use -c or session dialog
|
|
86
|
-
- fix: refresh fixed session display after bind via window event
|
|
87
|
-
- fix: resolveFixedSession auto-binds latest session when not set
|
|
88
|
-
- fix: add error logging to session bind button for debugging
|
|
89
|
-
- fix: session list uses disk files as source of truth, not stale index
|
|
90
|
-
- fix: bind button always visible, set session when none bound
|
|
91
|
-
- fix: limit fixedSessionId auto-bind to only 3 entry points
|
|
92
|
-
- fix: show bind button even when no session is bound yet
|
|
93
|
-
- fix: auto-bind fixedSessionId at Next.js API layer
|
|
94
|
-
- fix: ensure primary session binding at all entry points
|
|
95
|
-
- fix: auto-bind fixedSessionId on agent add and config update
|
|
96
|
-
- fix: show terminal (pending) for persistentSession agents without tmux yet
|
|
97
|
-
- fix: replace nested button with div+span in terminal dock tabs
|
|
98
|
-
- fix: don't auto-open terminal UI on workspace load
|
|
99
|
-
- fix: session binding edge cases
|
|
100
|
-
- fix: auto-bind latest CLI session on upgrade instead of creating new
|
|
101
|
-
- fix: persistent session starts claude with -c (resume last session)
|
|
102
|
-
- fix: verify stored tmuxSession is alive before using, fallback to find
|
|
103
|
-
- fix: findTmuxSession reads terminal-state.json for VibeCoding sessions
|
|
104
|
-
- fix: session watch detects file switch (new session created)
|
|
105
|
-
- fix: getAllAgentStates preserves tmuxSession + currentMessageId
|
|
106
|
-
- fix: persistent session emits state update so frontend knows tmuxSession
|
|
107
|
-
- fix: watch send_message sends configured prompt only + auto-Enter
|
|
108
|
-
- fix: open terminal attaches to existing tmux session regardless of mode
|
|
109
|
-
- fix: session watch only sends last matching entry, not all content
|
|
110
|
-
- fix: remove .js extension from dynamic import for Next.js compat
|
|
111
|
-
- fix: workspace terminal defaults to resume last session (-c)
|
|
112
|
-
- fix: updateAgentConfig rebuilds worker + emits status update
|
|
113
|
-
- fix: CLI backend auto-finds latest session when no sessionId
|
|
11
|
+
- fix: revise dedup to not block legitimate messages
|
|
12
|
+
- fix: reduce duplicate messages in workspace agent communication
|
|
114
13
|
|
|
115
|
-
### Performance
|
|
116
|
-
- perf: cache session binding check, defer SessionView loading
|
|
117
14
|
|
|
118
|
-
|
|
119
|
-
- refactor: MCP context via URL params, remove FORGE env var injection
|
|
120
|
-
- refactor: fixedSessionId is project-level, not workspace/agent-level
|
|
121
|
-
- refactor: remove manual/auto mode, unify message execution via tmuxSession
|
|
122
|
-
|
|
123
|
-
### Documentation
|
|
124
|
-
- revert auto-resume + document persistent terminal session plan
|
|
125
|
-
|
|
126
|
-
### Other
|
|
127
|
-
- revert: remove orchestrator auto-reply, let agent decide via MCP
|
|
128
|
-
- cleanup: remove fixedSessionId from agent config and workspace state
|
|
129
|
-
- simplify: remove auto-detect session binding complexity
|
|
130
|
-
- revert: restore floating terminal windows, remove dock panel
|
|
131
|
-
- revert auto-resume + document persistent terminal session plan
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.7...v0.5.8
|
|
15
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.8...v0.5.9
|
|
@@ -1779,7 +1779,7 @@ function FloatingTerminalInline({ agentLabel, agentIcon, projectPath, agentCliId
|
|
|
1779
1779
|
return <div ref={containerRef} className="w-full h-full" style={{ background: '#0d1117' }} />;
|
|
1780
1780
|
}
|
|
1781
1781
|
|
|
1782
|
-
function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, persistentSession, boundSessionId, onSessionReady, onClose }: {
|
|
1782
|
+
function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliCmd: cliCmdProp, cliType, workDir, preferredSessionName, existingSession, resumeMode, resumeSessionId, profileEnv, isPrimary, skipPermissions, persistentSession, boundSessionId, initialPos, onSessionReady, onClose }: {
|
|
1783
1783
|
agentLabel: string;
|
|
1784
1784
|
agentIcon: string;
|
|
1785
1785
|
projectPath: string;
|
|
@@ -1796,14 +1796,20 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1796
1796
|
skipPermissions?: boolean;
|
|
1797
1797
|
persistentSession?: boolean;
|
|
1798
1798
|
boundSessionId?: string;
|
|
1799
|
+
initialPos?: { x: number; y: number };
|
|
1799
1800
|
onSessionReady?: (name: string) => void;
|
|
1800
1801
|
onClose: (killSession: boolean) => void;
|
|
1801
1802
|
}) {
|
|
1802
1803
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
1803
1804
|
const wsRef = useRef<WebSocket | null>(null);
|
|
1804
1805
|
const sessionNameRef = useRef('');
|
|
1805
|
-
const [pos, setPos] = useState({ x: 80, y: 60 });
|
|
1806
|
-
const [
|
|
1806
|
+
const [pos, setPos] = useState(initialPos || { x: 80, y: 60 });
|
|
1807
|
+
const [userDragged, setUserDragged] = useState(false);
|
|
1808
|
+
// Follow node position unless user manually dragged the terminal
|
|
1809
|
+
useEffect(() => {
|
|
1810
|
+
if (initialPos && !userDragged) setPos(initialPos);
|
|
1811
|
+
}, [initialPos?.x, initialPos?.y]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
1812
|
+
const [size, setSize] = useState({ w: 500, h: 300 });
|
|
1807
1813
|
const [showCloseDialog, setShowCloseDialog] = useState(false);
|
|
1808
1814
|
const dragRef = useRef<{ startX: number; startY: number; origX: number; origY: number } | null>(null);
|
|
1809
1815
|
const resizeRef = useRef<{ startX: number; startY: number; origW: number; origH: number } | null>(null);
|
|
@@ -1821,7 +1827,7 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1821
1827
|
if (disposed) return;
|
|
1822
1828
|
|
|
1823
1829
|
const term = new Terminal({
|
|
1824
|
-
cursorBlink: true, fontSize:
|
|
1830
|
+
cursorBlink: true, fontSize: 10,
|
|
1825
1831
|
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
|
1826
1832
|
scrollback: 5000,
|
|
1827
1833
|
theme: { background: '#0d1117', foreground: '#c9d1d9', cursor: '#58a6ff' },
|
|
@@ -1831,7 +1837,15 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1831
1837
|
term.open(el);
|
|
1832
1838
|
setTimeout(() => { try { fitAddon.fit(); } catch {} }, 100);
|
|
1833
1839
|
|
|
1834
|
-
|
|
1840
|
+
// Scale font: min 10 at small size, max 13 at large size
|
|
1841
|
+
const ro = new ResizeObserver(() => {
|
|
1842
|
+
try {
|
|
1843
|
+
const w = el.clientWidth;
|
|
1844
|
+
const newSize = Math.min(13, Math.max(10, Math.floor(w / 60)));
|
|
1845
|
+
if (term.options.fontSize !== newSize) term.options.fontSize = newSize;
|
|
1846
|
+
fitAddon.fit();
|
|
1847
|
+
} catch {}
|
|
1848
|
+
});
|
|
1835
1849
|
ro.observe(el);
|
|
1836
1850
|
|
|
1837
1851
|
// Connect WebSocket — attach to existing or create new
|
|
@@ -1956,6 +1970,7 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
|
|
|
1956
1970
|
onMouseDown={(e) => {
|
|
1957
1971
|
e.preventDefault();
|
|
1958
1972
|
dragRef.current = { startX: e.clientX, startY: e.clientY, origX: pos.x, origY: pos.y };
|
|
1973
|
+
setUserDragged(true);
|
|
1959
1974
|
const onMove = (ev: MouseEvent) => {
|
|
1960
1975
|
if (!dragRef.current) return;
|
|
1961
1976
|
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 +2128,8 @@ interface AgentNodeData {
|
|
|
2113
2128
|
state: AgentState;
|
|
2114
2129
|
colorIdx: number;
|
|
2115
2130
|
previewLines: string[];
|
|
2131
|
+
projectPath: string;
|
|
2132
|
+
workspaceId: string | null;
|
|
2116
2133
|
onRun: () => void;
|
|
2117
2134
|
onPause: () => void;
|
|
2118
2135
|
onStop: () => void;
|
|
@@ -2131,8 +2148,11 @@ interface AgentNodeData {
|
|
|
2131
2148
|
[key: string]: unknown;
|
|
2132
2149
|
}
|
|
2133
2150
|
|
|
2151
|
+
// PortalTerminal/NodeTerminal removed — xterm cannot render inside React Flow nodes
|
|
2152
|
+
// and createPortal causes event routing issues. Using FloatingTerminal instead.
|
|
2153
|
+
|
|
2134
2154
|
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;
|
|
2155
|
+
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
2156
|
const c = COLORS[colorIdx % COLORS.length];
|
|
2137
2157
|
const smithStatus = state?.smithStatus || 'down';
|
|
2138
2158
|
const taskStatus = state?.taskStatus || 'idle';
|
|
@@ -2141,7 +2161,7 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
|
2141
2161
|
const taskInfo = TASK_STATUS[taskStatus] || TASK_STATUS.idle;
|
|
2142
2162
|
const currentStep = state?.currentStep;
|
|
2143
2163
|
const step = currentStep !== undefined ? config.steps[currentStep] : undefined;
|
|
2144
|
-
const isApprovalPending = taskStatus === 'idle' && smithStatus === 'active';
|
|
2164
|
+
const isApprovalPending = taskStatus === 'idle' && smithStatus === 'active';
|
|
2145
2165
|
|
|
2146
2166
|
return (
|
|
2147
2167
|
<div className="w-52 flex flex-col rounded-lg select-none"
|
|
@@ -2242,7 +2262,8 @@ function AgentFlowNode({ data }: NodeProps<Node<AgentNodeData>>) {
|
|
|
2242
2262
|
<div className="flex-1" />
|
|
2243
2263
|
<span className="flex items-center">
|
|
2244
2264
|
<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'}`}
|
|
2265
|
+
className={`text-[9px] px-1 ${hasTmux && taskStatus === 'running' ? 'text-green-400 animate-pulse' : 'text-gray-600 hover:text-green-400'}`}
|
|
2266
|
+
title="Open terminal">⌨️</button>
|
|
2246
2267
|
{hasTmux && !config.primary && (
|
|
2247
2268
|
<button onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); onSwitchSession(); }}
|
|
2248
2269
|
className="text-[10px] text-gray-600 hover:text-yellow-400 px-0.5 py-0.5" title="Switch session">▾</button>
|
|
@@ -2287,8 +2308,8 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2287
2308
|
const [memoryTarget, setMemoryTarget] = useState<{ id: string; label: string } | null>(null);
|
|
2288
2309
|
const [inboxTarget, setInboxTarget] = useState<{ id: string; label: string } | null>(null);
|
|
2289
2310
|
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);
|
|
2311
|
+
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 } }[]>([]);
|
|
2312
|
+
const [termLaunchDialog, setTermLaunchDialog] = useState<{ agent: AgentConfig; sessName: string; workDir?: string; sessions: string[]; supportsSession?: boolean; initialPos?: { x: number; y: number } } | null>(null);
|
|
2292
2313
|
|
|
2293
2314
|
// Expose focusAgent to parent
|
|
2294
2315
|
useImperativeHandle(ref, () => ({
|
|
@@ -2367,6 +2388,8 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2367
2388
|
state: states[agent.id] || { smithStatus: 'down', taskStatus: 'idle', artifacts: [] },
|
|
2368
2389
|
colorIdx: i,
|
|
2369
2390
|
previewLines: logPreview[agent.id] || [],
|
|
2391
|
+
projectPath,
|
|
2392
|
+
workspaceId,
|
|
2370
2393
|
onRun: () => {
|
|
2371
2394
|
wsApi(workspaceId!, 'run', { agentId: agent.id });
|
|
2372
2395
|
},
|
|
@@ -2394,6 +2417,13 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2394
2417
|
// Close existing terminal (config may have changed)
|
|
2395
2418
|
setFloatingTerminals(prev => prev.filter(t => t.agentId !== agent.id));
|
|
2396
2419
|
|
|
2420
|
+
// Get node screen position for initial terminal placement
|
|
2421
|
+
const nodeEl = document.querySelector(`[data-id="${agent.id}"]`);
|
|
2422
|
+
const nodeRect = nodeEl?.getBoundingClientRect();
|
|
2423
|
+
const initialPos = nodeRect
|
|
2424
|
+
? { x: nodeRect.left, y: nodeRect.bottom + 4 }
|
|
2425
|
+
: { x: 80, y: 60 };
|
|
2426
|
+
|
|
2397
2427
|
const agentState = states[agent.id];
|
|
2398
2428
|
const existingTmux = agentState?.tmuxSession;
|
|
2399
2429
|
const safeName = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 20);
|
|
@@ -2421,7 +2451,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2421
2451
|
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2422
2452
|
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2423
2453
|
tmuxSession: existingTmux, sessionName: sessName,
|
|
2424
|
-
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId,
|
|
2454
|
+
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2425
2455
|
}]);
|
|
2426
2456
|
return;
|
|
2427
2457
|
}
|
|
@@ -2433,7 +2463,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2433
2463
|
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2434
2464
|
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2435
2465
|
tmuxSession: res?.tmuxSession || sessName, sessionName: sessName,
|
|
2436
|
-
isPrimary: true, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId,
|
|
2466
|
+
isPrimary: true, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2437
2467
|
}]);
|
|
2438
2468
|
return;
|
|
2439
2469
|
}
|
|
@@ -2446,24 +2476,25 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2446
2476
|
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2447
2477
|
tmuxSession: res?.tmuxSession || sessName, sessionName: sessName,
|
|
2448
2478
|
resumeSessionId: agent.boundSessionId,
|
|
2449
|
-
isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId,
|
|
2479
|
+
isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2450
2480
|
}]);
|
|
2451
2481
|
return;
|
|
2452
2482
|
}
|
|
2453
2483
|
// No bound session → show launch dialog (New / Resume / Select)
|
|
2454
|
-
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true });
|
|
2484
|
+
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true, initialPos });
|
|
2455
2485
|
},
|
|
2456
2486
|
onSwitchSession: async () => {
|
|
2457
2487
|
if (!workspaceId) return;
|
|
2458
|
-
// Close existing terminal
|
|
2459
2488
|
setFloatingTerminals(prev => prev.filter(t => t.agentId !== agent.id));
|
|
2460
2489
|
if (agent.id) wsApi(workspaceId, 'close_terminal', { agentId: agent.id });
|
|
2461
|
-
|
|
2490
|
+
const nodeEl = document.querySelector(`[data-id="${agent.id}"]`);
|
|
2491
|
+
const nodeRect = nodeEl?.getBoundingClientRect();
|
|
2492
|
+
const switchPos = nodeRect ? { x: nodeRect.left, y: nodeRect.bottom + 4 } : { x: 80, y: 60 };
|
|
2462
2493
|
const safeName = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').slice(0, 20);
|
|
2463
2494
|
const sessName = `mw-forge-${safeName(projectName)}-${safeName(agent.label)}`;
|
|
2464
2495
|
const workDir = agent.workDir && agent.workDir !== './' && agent.workDir !== '.' ? agent.workDir : undefined;
|
|
2465
2496
|
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 });
|
|
2497
|
+
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true, initialPos: switchPos });
|
|
2467
2498
|
},
|
|
2468
2499
|
} satisfies AgentNodeData,
|
|
2469
2500
|
};
|
|
@@ -2716,6 +2747,22 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2716
2747
|
nodes={rfNodes}
|
|
2717
2748
|
edges={rfEdges}
|
|
2718
2749
|
onNodesChange={onNodesChange}
|
|
2750
|
+
onNodeDragStop={() => {
|
|
2751
|
+
// Reposition terminals to follow their nodes
|
|
2752
|
+
setFloatingTerminals(prev => prev.map(ft => {
|
|
2753
|
+
const nodeEl = document.querySelector(`[data-id="${ft.agentId}"]`);
|
|
2754
|
+
const rect = nodeEl?.getBoundingClientRect();
|
|
2755
|
+
return rect ? { ...ft, initialPos: { x: rect.left, y: rect.bottom + 4 } } : ft;
|
|
2756
|
+
}));
|
|
2757
|
+
}}
|
|
2758
|
+
onMoveEnd={() => {
|
|
2759
|
+
// Reposition after pan/zoom
|
|
2760
|
+
setFloatingTerminals(prev => prev.map(ft => {
|
|
2761
|
+
const nodeEl = document.querySelector(`[data-id="${ft.agentId}"]`);
|
|
2762
|
+
const rect = nodeEl?.getBoundingClientRect();
|
|
2763
|
+
return rect ? { ...ft, initialPos: { x: rect.left, y: rect.bottom + 4 } } : ft;
|
|
2764
|
+
}));
|
|
2765
|
+
}}
|
|
2719
2766
|
nodeTypes={nodeTypes}
|
|
2720
2767
|
fitView
|
|
2721
2768
|
fitViewOptions={{ padding: 0.3 }}
|
|
@@ -2826,7 +2873,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2826
2873
|
cliCmd: res.cliCmd || 'claude',
|
|
2827
2874
|
cliType: res.cliType || 'claude-code',
|
|
2828
2875
|
workDir,
|
|
2829
|
-
sessionName: sessName, resumeMode, resumeSessionId: sessionId, isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId,
|
|
2876
|
+
sessionName: sessName, resumeMode, resumeSessionId: sessionId, isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId, initialPos: termLaunchDialog.initialPos,
|
|
2830
2877
|
profileEnv: {
|
|
2831
2878
|
...(res.env || {}),
|
|
2832
2879
|
...(res.model ? { CLAUDE_MODEL: res.model } : {}),
|
|
@@ -2861,6 +2908,7 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2861
2908
|
skipPermissions={ft.skipPermissions}
|
|
2862
2909
|
persistentSession={ft.persistentSession}
|
|
2863
2910
|
boundSessionId={ft.boundSessionId}
|
|
2911
|
+
initialPos={ft.initialPos}
|
|
2864
2912
|
onSessionReady={(name) => {
|
|
2865
2913
|
if (workspaceId) wsApi(workspaceId, 'set_tmux_session', { agentId: ft.agentId, sessionName: name });
|
|
2866
2914
|
setFloatingTerminals(prev => prev.map(t => t.agentId === ft.agentId ? { ...t, tmuxSession: name } : t));
|
|
@@ -1171,10 +1171,14 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1171
1171
|
if (msg.type === 'ack' || msg.from === '_forge') continue;
|
|
1172
1172
|
if (this.forgeActedMessages.has(msg.id)) continue;
|
|
1173
1173
|
|
|
1174
|
-
// Case 1: Message done but no reply from target → ask target to send summary
|
|
1174
|
+
// Case 1: Message done but no reply from target → ask target to send summary (once per pair)
|
|
1175
1175
|
if (msg.status === 'done') {
|
|
1176
1176
|
const age = now - msg.timestamp;
|
|
1177
|
-
if (age < 30_000) continue;
|
|
1177
|
+
if (age < 30_000) continue;
|
|
1178
|
+
|
|
1179
|
+
// Dedup by target→sender pair (only nudge once per relationship)
|
|
1180
|
+
const nudgeKey = `nudge-${msg.to}->${msg.from}`;
|
|
1181
|
+
if (this.forgeActedMessages.has(nudgeKey)) { this.forgeActedMessages.add(msg.id); continue; }
|
|
1178
1182
|
|
|
1179
1183
|
const hasReply = log.some(r =>
|
|
1180
1184
|
r.from === msg.to && r.to === msg.from &&
|
|
@@ -1189,7 +1193,8 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1189
1193
|
content: `[IMPORTANT] You finished a task requested by ${senderLabel} but did not send them the results. You MUST call the MCP tool "send_message" (NOT the forge-send skill) with to="${senderLabel}" and include a summary of what you did and the outcome. Do not do any other work until you have sent this reply.`,
|
|
1190
1194
|
});
|
|
1191
1195
|
this.forgeActedMessages.add(msg.id);
|
|
1192
|
-
|
|
1196
|
+
this.forgeActedMessages.add(nudgeKey);
|
|
1197
|
+
console.log(`[forge-agent] Nudged ${targetEntry.config.label} to reply to ${senderLabel} (once)`);
|
|
1193
1198
|
}
|
|
1194
1199
|
}
|
|
1195
1200
|
}
|
|
@@ -1755,13 +1760,25 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1755
1760
|
? `${completedLabel} completed: ${files.length} files changed. ${summary.slice(0, 200)}`
|
|
1756
1761
|
: `${completedLabel} completed. ${summary.slice(0, 300) || 'Check upstream outputs for updates.'}`;
|
|
1757
1762
|
|
|
1758
|
-
// Find all downstream agents
|
|
1763
|
+
// Find all downstream agents — skip if already sent upstream_complete recently (60s)
|
|
1764
|
+
const now = Date.now();
|
|
1759
1765
|
let sent = 0;
|
|
1760
1766
|
for (const [id, entry] of this.agents) {
|
|
1761
1767
|
if (id === completedAgentId) continue;
|
|
1762
1768
|
if (entry.config.type === 'input') continue;
|
|
1763
1769
|
if (!entry.config.dependsOn.includes(completedAgentId)) continue;
|
|
1764
1770
|
|
|
1771
|
+
// Dedup: skip if upstream_complete was sent to this target within last 60s
|
|
1772
|
+
const recentDup = this.bus.getLog().some(m =>
|
|
1773
|
+
m.from === completedAgentId && m.to === id &&
|
|
1774
|
+
m.payload?.action === 'upstream_complete' &&
|
|
1775
|
+
now - m.timestamp < 60_000
|
|
1776
|
+
);
|
|
1777
|
+
if (recentDup) {
|
|
1778
|
+
console.log(`[bus] ${completedLabel} → ${entry.config.label}: upstream_complete skipped (sent <60s ago)`);
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1765
1782
|
this.bus.send(completedAgentId, id, 'notify', {
|
|
1766
1783
|
action: 'upstream_complete',
|
|
1767
1784
|
content,
|
|
@@ -2209,8 +2226,23 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
2209
2226
|
// requiresApproval is handled at message arrival time (routeMessageToAgent),
|
|
2210
2227
|
// not in the message loop. Approved messages come through as normal 'pending'.
|
|
2211
2228
|
|
|
2229
|
+
// Dedup: if multiple upstream_complete from same sender are pending, keep only latest
|
|
2230
|
+
const allRaw = this.bus.getPendingMessagesFor(agentId).filter(m => m.from !== agentId && m.type !== 'ack');
|
|
2231
|
+
const upstreamSeen = new Set<string>();
|
|
2232
|
+
for (let i = allRaw.length - 1; i >= 0; i--) {
|
|
2233
|
+
const m = allRaw[i];
|
|
2234
|
+
if (m.payload?.action === 'upstream_complete') {
|
|
2235
|
+
const key = `upstream-${m.from}`;
|
|
2236
|
+
if (upstreamSeen.has(key)) {
|
|
2237
|
+
m.status = 'done' as any;
|
|
2238
|
+
this.emit('event', { type: 'bus_message_status', messageId: m.id, status: 'done' } as any);
|
|
2239
|
+
}
|
|
2240
|
+
upstreamSeen.add(key);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2212
2244
|
// Find next pending message, applying causedBy rules
|
|
2213
|
-
const allPending =
|
|
2245
|
+
const allPending = allRaw.filter(m => m.status === 'pending');
|
|
2214
2246
|
const pending = allPending.filter(m => {
|
|
2215
2247
|
// Tickets: accepted but check retry limit
|
|
2216
2248
|
if (m.category === 'ticket') {
|
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/
|
|
3
|
+
import "./.next/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