@aion0/forge 0.5.37 → 0.5.40
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
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.40
|
|
2
2
|
|
|
3
|
-
Released: 2026-04-
|
|
3
|
+
Released: 2026-04-14
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
5
|
+
## Changes since v0.5.38
|
|
6
6
|
|
|
7
|
-
###
|
|
8
|
-
-
|
|
7
|
+
### Other
|
|
8
|
+
- v0.5.39
|
|
9
|
+
- add option to disable claude modification
|
|
9
10
|
|
|
10
11
|
|
|
11
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.
|
|
12
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.38...v0.5.40
|
|
@@ -1446,6 +1446,8 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
1446
1446
|
const MAX_CREATE_RETRIES = 2;
|
|
1447
1447
|
let reconnectAttempts = 0;
|
|
1448
1448
|
let isNewlyCreated = false;
|
|
1449
|
+
let lastActivityTime = Date.now();
|
|
1450
|
+
let staleCheckTimer = 0;
|
|
1449
1451
|
|
|
1450
1452
|
function connect() {
|
|
1451
1453
|
if (disposed) return;
|
|
@@ -1455,6 +1457,7 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
1455
1457
|
socket.onopen = () => {
|
|
1456
1458
|
if (disposed) { socket.close(); return; }
|
|
1457
1459
|
if (socket.readyState !== WebSocket.OPEN) return;
|
|
1460
|
+
lastActivityTime = Date.now();
|
|
1458
1461
|
const cols = term.cols;
|
|
1459
1462
|
const rows = term.rows;
|
|
1460
1463
|
|
|
@@ -1477,6 +1480,7 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
1477
1480
|
|
|
1478
1481
|
ws.onmessage = (event) => {
|
|
1479
1482
|
if (disposed || !initDone) return;
|
|
1483
|
+
lastActivityTime = Date.now();
|
|
1480
1484
|
try {
|
|
1481
1485
|
const msg = JSON.parse(event.data);
|
|
1482
1486
|
if (msg.type === 'output') {
|
|
@@ -1573,6 +1577,36 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
1573
1577
|
};
|
|
1574
1578
|
}
|
|
1575
1579
|
|
|
1580
|
+
// Stale detection: WebSocket may appear "open" after Mac sleep but have no traffic.
|
|
1581
|
+
// If no activity for 60s, force-close and reconnect.
|
|
1582
|
+
staleCheckTimer = window.setInterval(() => {
|
|
1583
|
+
if (disposed) return;
|
|
1584
|
+
if (ws?.readyState === WebSocket.OPEN && Date.now() - lastActivityTime > 60000) {
|
|
1585
|
+
console.warn('[ws] terminal stream stalled, forcing reconnect');
|
|
1586
|
+
lastActivityTime = Date.now();
|
|
1587
|
+
try { ws.close(); } catch {}
|
|
1588
|
+
}
|
|
1589
|
+
}, 20000);
|
|
1590
|
+
|
|
1591
|
+
// Detect tab wake from sleep — only triggers when tab was actually hidden.
|
|
1592
|
+
// Conservative: needs > 60s of inactivity to force reconnect.
|
|
1593
|
+
const onVisibilityChange = () => {
|
|
1594
|
+
if (disposed) return;
|
|
1595
|
+
if (document.visibilityState === 'visible' && Date.now() - lastActivityTime > 60000) {
|
|
1596
|
+
console.log('[ws] terminal visible after long idle, forcing reconnect');
|
|
1597
|
+
lastActivityTime = Date.now();
|
|
1598
|
+
try { ws?.close(); } catch {}
|
|
1599
|
+
}
|
|
1600
|
+
};
|
|
1601
|
+
const onOnline = () => {
|
|
1602
|
+
if (disposed) return;
|
|
1603
|
+
console.log('[ws] network online, reconnecting terminal');
|
|
1604
|
+
lastActivityTime = Date.now();
|
|
1605
|
+
try { ws?.close(); } catch {}
|
|
1606
|
+
};
|
|
1607
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
1608
|
+
window.addEventListener('online', onOnline);
|
|
1609
|
+
|
|
1576
1610
|
// NOTE: connect() is called inside initTerminal() — do NOT call it here.
|
|
1577
1611
|
// Calling it both here and in initTerminal() causes duplicate WebSocket
|
|
1578
1612
|
// connections to the same tmux session, resulting in doubled output.
|
|
@@ -1683,6 +1717,9 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
1683
1717
|
visObserver.disconnect();
|
|
1684
1718
|
clearTimeout(resizeTimer);
|
|
1685
1719
|
clearTimeout(reconnectTimer);
|
|
1720
|
+
if (staleCheckTimer) clearInterval(staleCheckTimer);
|
|
1721
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
1722
|
+
window.removeEventListener('online', onOnline);
|
|
1686
1723
|
window.removeEventListener('terminal-drag-end', onDragEnd);
|
|
1687
1724
|
resizeObserver.disconnect();
|
|
1688
1725
|
// Strict Mode cleanup: if disposed within 2s of mount and we created a
|
|
@@ -280,9 +280,83 @@ function useWorkspaceStream(workspaceId: string | null, onEvent?: (event: any) =
|
|
|
280
280
|
useEffect(() => {
|
|
281
281
|
if (!workspaceId) return;
|
|
282
282
|
|
|
283
|
-
|
|
283
|
+
// Reconnection state — survives sleep/wake and network drops
|
|
284
|
+
let es: EventSource | null = null;
|
|
285
|
+
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
286
|
+
let staleCheckTimer: ReturnType<typeof setInterval> | null = null;
|
|
287
|
+
let reconnectAttempts = 0;
|
|
288
|
+
let lastEventTime = Date.now();
|
|
289
|
+
let disposed = false;
|
|
290
|
+
|
|
291
|
+
const scheduleReconnect = () => {
|
|
292
|
+
if (disposed) return;
|
|
293
|
+
if (reconnectTimer) return;
|
|
294
|
+
// Exponential backoff: 2s, 4s, 8s, max 30s
|
|
295
|
+
const delay = Math.min(30000, 2000 * Math.pow(2, reconnectAttempts));
|
|
296
|
+
reconnectAttempts++;
|
|
297
|
+
console.log(`[sse] reconnecting in ${delay / 1000}s (attempt ${reconnectAttempts})`);
|
|
298
|
+
reconnectTimer = setTimeout(() => {
|
|
299
|
+
reconnectTimer = null;
|
|
300
|
+
connect();
|
|
301
|
+
}, delay);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const connect = () => {
|
|
305
|
+
if (disposed) return;
|
|
306
|
+
try { es?.close(); } catch {}
|
|
307
|
+
lastEventTime = Date.now();
|
|
308
|
+
es = new EventSource(`/api/workspace/${workspaceId}/stream`);
|
|
309
|
+
|
|
310
|
+
es.onopen = () => {
|
|
311
|
+
reconnectAttempts = 0;
|
|
312
|
+
lastEventTime = Date.now();
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
es.onerror = () => {
|
|
316
|
+
// Browser fires onerror when the connection drops
|
|
317
|
+
console.warn('[sse] EventSource error — will reconnect');
|
|
318
|
+
try { es?.close(); } catch {}
|
|
319
|
+
scheduleReconnect();
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
es.onmessage = (e) => {
|
|
323
|
+
lastEventTime = Date.now();
|
|
324
|
+
handleEvent(e);
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// Stall detection: if we haven't received any event (including heartbeat ping from server)
|
|
329
|
+
// for 45s, the connection is stuck (common after Mac sleep). Force reconnect.
|
|
330
|
+
staleCheckTimer = setInterval(() => {
|
|
331
|
+
if (disposed) return;
|
|
332
|
+
if (Date.now() - lastEventTime > 45000) {
|
|
333
|
+
console.warn('[sse] stream stalled (no events for 45s), forcing reconnect');
|
|
334
|
+
try { es?.close(); } catch {}
|
|
335
|
+
lastEventTime = Date.now(); // prevent immediate re-trigger
|
|
336
|
+
connect();
|
|
337
|
+
}
|
|
338
|
+
}, 15000);
|
|
339
|
+
|
|
340
|
+
// Detect tab wake from sleep / network recovery — conservative to avoid churn
|
|
341
|
+
const onVisibilityChange = () => {
|
|
342
|
+
if (disposed) return;
|
|
343
|
+
if (document.visibilityState === 'visible' && Date.now() - lastEventTime > 60000) {
|
|
344
|
+
console.log('[sse] page visible after long idle, reconnecting');
|
|
345
|
+
try { es?.close(); } catch {}
|
|
346
|
+
connect();
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const onOnline = () => {
|
|
350
|
+
if (disposed) return;
|
|
351
|
+
console.log('[sse] network online, reconnecting');
|
|
352
|
+
try { es?.close(); } catch {}
|
|
353
|
+
lastEventTime = Date.now();
|
|
354
|
+
connect();
|
|
355
|
+
};
|
|
356
|
+
document.addEventListener('visibilitychange', onVisibilityChange);
|
|
357
|
+
window.addEventListener('online', onOnline);
|
|
284
358
|
|
|
285
|
-
|
|
359
|
+
const handleEvent = (e: MessageEvent) => {
|
|
286
360
|
try {
|
|
287
361
|
const event = JSON.parse(e.data);
|
|
288
362
|
|
|
@@ -408,7 +482,17 @@ function useWorkspaceStream(workspaceId: string | null, onEvent?: (event: any) =
|
|
|
408
482
|
} catch {}
|
|
409
483
|
};
|
|
410
484
|
|
|
411
|
-
|
|
485
|
+
// Start the initial connection
|
|
486
|
+
connect();
|
|
487
|
+
|
|
488
|
+
return () => {
|
|
489
|
+
disposed = true;
|
|
490
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
491
|
+
if (staleCheckTimer) clearInterval(staleCheckTimer);
|
|
492
|
+
document.removeEventListener('visibilitychange', onVisibilityChange);
|
|
493
|
+
window.removeEventListener('online', onOnline);
|
|
494
|
+
try { es?.close(); } catch {}
|
|
495
|
+
};
|
|
412
496
|
}, [workspaceId]);
|
|
413
497
|
|
|
414
498
|
return { agents, states, logPreview, busLog, setAgents, daemonActive, setDaemonActive };
|
|
@@ -23,6 +23,7 @@ Settings are stored in `~/.forge/data/settings.yaml`. Configure via the web UI (
|
|
|
23
23
|
| `telegramAgent` | string | `""` | Agent for Telegram task execution |
|
|
24
24
|
| `docsAgent` | string | `""` | Agent for documentation queries |
|
|
25
25
|
| `skipPermissions` | boolean | `false` | Add `--dangerously-skip-permissions` to claude invocations |
|
|
26
|
+
| `manageClaudeConfig` | boolean | `true` | When `false`, Forge will not modify `~/.claude/` or project `.claude/` files (skills, Stop hook, permissions, profile env/model) |
|
|
26
27
|
| `notificationRetentionDays` | number | `30` | Auto-cleanup notifications older than N days |
|
|
27
28
|
| `skillsRepoUrl` | string | forge-skills URL | GitHub raw URL for skills registry |
|
|
28
29
|
| `displayName` | string | `"Forge"` | Display name shown in header |
|
package/lib/settings.ts
CHANGED
|
@@ -47,6 +47,7 @@ export interface Settings {
|
|
|
47
47
|
pipelineModel: string;
|
|
48
48
|
telegramModel: string;
|
|
49
49
|
skipPermissions: boolean;
|
|
50
|
+
manageClaudeConfig: boolean;
|
|
50
51
|
notificationRetentionDays: number;
|
|
51
52
|
skillsRepoUrl: string;
|
|
52
53
|
displayName: string;
|
|
@@ -74,6 +75,7 @@ const defaults: Settings = {
|
|
|
74
75
|
pipelineModel: 'default',
|
|
75
76
|
telegramModel: 'sonnet',
|
|
76
77
|
skipPermissions: false,
|
|
78
|
+
manageClaudeConfig: true,
|
|
77
79
|
notificationRetentionDays: 30,
|
|
78
80
|
skillsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-skills/main',
|
|
79
81
|
displayName: 'Forge',
|
|
@@ -7,6 +7,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from
|
|
|
7
7
|
import { join, dirname } from 'node:path';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { loadSettings } from '../settings';
|
|
10
11
|
|
|
11
12
|
const _filename = typeof __filename !== 'undefined' ? __filename : fileURLToPath(import.meta.url);
|
|
12
13
|
const _dirname = typeof __dirname !== 'undefined' ? __dirname : dirname(_filename);
|
|
@@ -23,6 +24,7 @@ export function installForgeSkills(
|
|
|
23
24
|
agentId: string,
|
|
24
25
|
forgePort = 8403,
|
|
25
26
|
): { installed: string[] } {
|
|
27
|
+
if (!loadSettings().manageClaudeConfig) return { installed: [] };
|
|
26
28
|
const skillsDir = join(homedir(), '.claude', 'skills');
|
|
27
29
|
mkdirSync(skillsDir, { recursive: true });
|
|
28
30
|
|
|
@@ -126,6 +128,7 @@ export function applyProfileToProject(
|
|
|
126
128
|
profile: { env?: Record<string, string>; model?: string },
|
|
127
129
|
): void {
|
|
128
130
|
if (!profile.env && !profile.model) return;
|
|
131
|
+
if (!loadSettings().manageClaudeConfig) return;
|
|
129
132
|
|
|
130
133
|
const settingsFile = join(projectPath, '.claude', 'settings.json');
|
|
131
134
|
try {
|
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