@aion0/forge 0.5.12 → 0.5.14
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 +7 -6
- package/components/WorkspaceView.tsx +20 -52
- package/lib/workspace/orchestrator.ts +29 -28
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.14
|
|
2
2
|
|
|
3
3
|
Released: 2026-03-31
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.13
|
|
6
6
|
|
|
7
7
|
### Bug Fixes
|
|
8
|
-
- fix:
|
|
9
|
-
- fix:
|
|
10
|
-
- fix:
|
|
8
|
+
- fix: dedup forge failed notifications by sender→target pair
|
|
9
|
+
- fix: TerminalLaunchDialog also uses daemon to create session
|
|
10
|
+
- fix: FloatingTerminal only attaches, daemon creates all sessions
|
|
11
|
+
- fix: write launch script to file to avoid tmux send-keys truncation
|
|
11
12
|
|
|
12
13
|
|
|
13
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.
|
|
14
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.13...v0.5.14
|
|
@@ -2517,43 +2517,20 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2517
2517
|
},
|
|
2518
2518
|
};
|
|
2519
2519
|
|
|
2520
|
-
//
|
|
2521
|
-
if (existingTmux) {
|
|
2522
|
-
|
|
2523
|
-
setFloatingTerminals(prev => [...prev, {
|
|
2524
|
-
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2525
|
-
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2526
|
-
tmuxSession: existingTmux, sessionName: sessName,
|
|
2527
|
-
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2528
|
-
}]);
|
|
2529
|
-
return;
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
// Primary without session → open directly (no dialog)
|
|
2533
|
-
if (agent.primary) {
|
|
2534
|
-
const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id }).catch(() => ({})) as any;
|
|
2535
|
-
setFloatingTerminals(prev => [...prev, {
|
|
2536
|
-
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2537
|
-
cliId: agent.agentId || 'claude', ...launchInfo, workDir,
|
|
2538
|
-
tmuxSession: res?.tmuxSession || sessName, sessionName: sessName,
|
|
2539
|
-
isPrimary: true, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2540
|
-
}]);
|
|
2541
|
-
return;
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
// Non-primary: has boundSessionId → use it directly; no bound → show dialog
|
|
2545
|
-
if (agent.boundSessionId) {
|
|
2520
|
+
// All paths: let daemon create/ensure session, then attach
|
|
2521
|
+
if (existingTmux || agent.primary || agent.persistentSession || agent.boundSessionId) {
|
|
2522
|
+
// Daemon creates session via ensurePersistentSession (launch script, no truncation)
|
|
2546
2523
|
const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id }).catch(() => ({})) as any;
|
|
2524
|
+
const tmux = existingTmux || res?.tmuxSession || sessName;
|
|
2547
2525
|
setFloatingTerminals(prev => [...prev, {
|
|
2548
2526
|
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2549
|
-
cliId: agent.agentId || 'claude',
|
|
2550
|
-
tmuxSession:
|
|
2551
|
-
|
|
2552
|
-
isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2527
|
+
cliId: agent.agentId || 'claude', workDir,
|
|
2528
|
+
tmuxSession: tmux, sessionName: sessName,
|
|
2529
|
+
isPrimary: agent.primary, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: agent.boundSessionId, initialPos,
|
|
2553
2530
|
}]);
|
|
2554
2531
|
return;
|
|
2555
2532
|
}
|
|
2556
|
-
// No bound session → show launch dialog
|
|
2533
|
+
// No persistent session, no bound session → show launch dialog
|
|
2557
2534
|
setTermLaunchDialog({ agent, sessName, workDir, sessions: [], supportsSession: resolveRes?.supportsSession ?? true, initialPos });
|
|
2558
2535
|
},
|
|
2559
2536
|
onSwitchSession: async () => {
|
|
@@ -2934,28 +2911,19 @@ function WorkspaceViewInner({ projectPath, projectName, onClose }: {
|
|
|
2934
2911
|
onLaunch={async (resumeMode, sessionId) => {
|
|
2935
2912
|
const { agent, sessName, workDir } = termLaunchDialog;
|
|
2936
2913
|
setTermLaunchDialog(null);
|
|
2937
|
-
|
|
2938
|
-
if (
|
|
2939
|
-
|
|
2940
|
-
if (sessionId) {
|
|
2941
|
-
wsApi(workspaceId, 'update', { agentId: agent.id, config: { ...agent, boundSessionId: sessionId } }).catch(() => {});
|
|
2942
|
-
}
|
|
2943
|
-
setFloatingTerminals(prev => [...prev, {
|
|
2944
|
-
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2945
|
-
cliId: agent.agentId || 'claude',
|
|
2946
|
-
cliCmd: res.cliCmd || 'claude',
|
|
2947
|
-
cliType: res.cliType || 'claude-code',
|
|
2948
|
-
workDir,
|
|
2949
|
-
sessionName: sessName, resumeMode, resumeSessionId: sessionId, isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId, initialPos: termLaunchDialog.initialPos,
|
|
2950
|
-
profileEnv: {
|
|
2951
|
-
...(res.env || {}),
|
|
2952
|
-
...(res.model ? { CLAUDE_MODEL: res.model } : {}),
|
|
2953
|
-
FORGE_AGENT_ID: agent.id,
|
|
2954
|
-
FORGE_WORKSPACE_ID: workspaceId,
|
|
2955
|
-
FORGE_PORT: String(window.location.port || 8403),
|
|
2956
|
-
},
|
|
2957
|
-
}]);
|
|
2914
|
+
// Save selected session as boundSessionId
|
|
2915
|
+
if (sessionId) {
|
|
2916
|
+
await wsApi(workspaceId, 'update', { agentId: agent.id, config: { ...agent, boundSessionId: sessionId } }).catch(() => {});
|
|
2958
2917
|
}
|
|
2918
|
+
// Daemon creates session (launch script), then attach
|
|
2919
|
+
const res = await wsApi(workspaceId, 'open_terminal', { agentId: agent.id }).catch(() => ({})) as any;
|
|
2920
|
+
const tmux = res?.tmuxSession || sessName;
|
|
2921
|
+
setFloatingTerminals(prev => [...prev, {
|
|
2922
|
+
agentId: agent.id, label: agent.label, icon: agent.icon,
|
|
2923
|
+
cliId: agent.agentId || 'claude', workDir,
|
|
2924
|
+
tmuxSession: tmux, sessionName: sessName,
|
|
2925
|
+
isPrimary: false, skipPermissions: agent.skipPermissions !== false, persistentSession: agent.persistentSession, boundSessionId: sessionId || agent.boundSessionId, initialPos: termLaunchDialog.initialPos,
|
|
2926
|
+
}]);
|
|
2959
2927
|
}}
|
|
2960
2928
|
onCancel={() => setTermLaunchDialog(null)}
|
|
2961
2929
|
/>
|
|
@@ -1353,16 +1353,20 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
1353
1353
|
}
|
|
1354
1354
|
}
|
|
1355
1355
|
|
|
1356
|
-
// Case 4: Failed → notify sender
|
|
1357
|
-
if (msg.status === 'failed'
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
this.
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1356
|
+
// Case 4: Failed → notify sender (once per sender→target pair)
|
|
1357
|
+
if (msg.status === 'failed') {
|
|
1358
|
+
const failKey = `failed-${msg.from}->${msg.to}`;
|
|
1359
|
+
if (!this.forgeActedMessages.has(failKey)) {
|
|
1360
|
+
const senderEntry = this.agents.get(msg.from);
|
|
1361
|
+
const targetLabel = this.agents.get(msg.to)?.config.label || msg.to;
|
|
1362
|
+
if (senderEntry && msg.from !== '_forge' && msg.from !== '_system') {
|
|
1363
|
+
this.bus.send('_forge', msg.from, 'notify', {
|
|
1364
|
+
action: 'update_notify',
|
|
1365
|
+
content: `Your message to ${targetLabel} has failed. You may want to retry or take a different approach.`,
|
|
1366
|
+
});
|
|
1367
|
+
console.log(`[forge-agent] Notified ${senderEntry.config.label} that message to ${targetLabel} failed (once)`);
|
|
1368
|
+
}
|
|
1369
|
+
this.forgeActedMessages.add(failKey);
|
|
1366
1370
|
}
|
|
1367
1371
|
this.forgeActedMessages.add(`failed-${msg.id}`);
|
|
1368
1372
|
}
|
|
@@ -2023,27 +2027,24 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
2023
2027
|
|
|
2024
2028
|
execSync(`tmux new-session -d -s "${sessionName}" -c "${workDir}"`, { timeout: 5000 });
|
|
2025
2029
|
|
|
2026
|
-
//
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
+
// Build launch script to avoid tmux send-keys truncation
|
|
2031
|
+
const scriptLines: string[] = ['#!/bin/bash', `cd "${workDir}"`];
|
|
2032
|
+
|
|
2033
|
+
// Unset old profile vars
|
|
2034
|
+
scriptLines.push('unset 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');
|
|
2030
2035
|
|
|
2031
|
-
// Set FORGE env vars
|
|
2032
|
-
|
|
2036
|
+
// Set FORGE env vars
|
|
2037
|
+
scriptLines.push(`export FORGE_WORKSPACE_ID="${this.workspaceId}" FORGE_AGENT_ID="${config.id}" FORGE_PORT="${Number(process.env.PORT) || 8403}"`);
|
|
2033
2038
|
|
|
2034
|
-
// Set profile env vars
|
|
2039
|
+
// Set profile env vars
|
|
2035
2040
|
if (envExports) {
|
|
2036
|
-
|
|
2041
|
+
scriptLines.push(envExports.replace(/ && /g, '\n').replace(/\n$/, ''));
|
|
2037
2042
|
}
|
|
2038
2043
|
|
|
2039
|
-
// Build CLI
|
|
2040
|
-
const parts: string[] = [];
|
|
2044
|
+
// Build CLI command
|
|
2041
2045
|
let cmd = cliCmd;
|
|
2042
|
-
|
|
2043
|
-
// Session resume: use bound session ID (primary from project-sessions, others from config)
|
|
2044
2046
|
if (supportsSession) {
|
|
2045
2047
|
let sessionId: string | undefined;
|
|
2046
|
-
|
|
2047
2048
|
if (config.primary) {
|
|
2048
2049
|
try {
|
|
2049
2050
|
const { getFixedSession } = await import('../project-sessions') as any;
|
|
@@ -2052,7 +2053,6 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
2052
2053
|
} else {
|
|
2053
2054
|
sessionId = config.boundSessionId;
|
|
2054
2055
|
}
|
|
2055
|
-
|
|
2056
2056
|
if (sessionId) {
|
|
2057
2057
|
const sessionFile = join(this.getCliSessionDir(config.workDir), `${sessionId}.jsonl`);
|
|
2058
2058
|
if (existsSync(sessionFile)) {
|
|
@@ -2061,15 +2061,16 @@ export class WorkspaceOrchestrator extends EventEmitter {
|
|
|
2061
2061
|
console.log(`[daemon] ${config.label}: bound session ${sessionId} missing, starting fresh`);
|
|
2062
2062
|
}
|
|
2063
2063
|
}
|
|
2064
|
-
// No bound session → start fresh (no -c, avoids "No conversation found")
|
|
2065
2064
|
}
|
|
2066
2065
|
if (modelFlag) cmd += modelFlag;
|
|
2067
2066
|
if (config.skipPermissions !== false && skipPermissionsFlag) cmd += ` ${skipPermissionsFlag}`;
|
|
2068
2067
|
if (mcpConfigFlag) cmd += mcpConfigFlag;
|
|
2069
|
-
|
|
2068
|
+
scriptLines.push(`exec ${cmd}`);
|
|
2070
2069
|
|
|
2071
|
-
|
|
2072
|
-
|
|
2070
|
+
// Write script and execute in tmux
|
|
2071
|
+
const scriptPath = `/tmp/forge-launch-${config.id.replace(/[^a-z0-9-]/g, '')}.sh`;
|
|
2072
|
+
writeFileSync(scriptPath, scriptLines.join('\n'), { mode: 0o755 });
|
|
2073
|
+
execSync(`tmux send-keys -t "${sessionName}" 'bash ${scriptPath}' Enter`, { timeout: 5000 });
|
|
2073
2074
|
|
|
2074
2075
|
console.log(`[daemon] ${config.label}: persistent session created (${sessionName}) [${cliType}: ${cliCmd}]`);
|
|
2075
2076
|
|
package/package.json
CHANGED