@aion0/forge 0.5.27 → 0.5.29

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,19 +1,11 @@
1
- # Forge v0.5.27
1
+ # Forge v0.5.29
2
2
 
3
- Released: 2026-04-08
3
+ Released: 2026-04-09
4
4
 
5
- ## Changes since v0.5.26
6
-
7
- ### Features
8
- - feat: tmux mouse toggle button in terminal toolbar
5
+ ## Changes since v0.5.28
9
6
 
10
7
  ### Bug Fixes
11
- - fix: terminal bell respects settings.terminalBellEnabled
12
- - fix: disable terminal bell on state restore
13
- - fix: mouse toggle applies to all sessions, not just global
14
- - fix: mouse toggle takes effect immediately via tmux-command
15
- - fix: swap mouse toggle hint and button position
16
- - fix: restore tmux mouse on for trackpad scrolling in web terminal
8
+ - fix: auto-reconnect workspace terminal WebSocket on disconnect
17
9
 
18
10
 
19
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.26...v0.5.27
11
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.28...v0.5.29
@@ -1,5 +1,8 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { execSync } from 'node:child_process';
2
+ import { exec } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+
5
+ const execAsync = promisify(exec);
3
6
 
4
7
  export async function GET(req: Request) {
5
8
  const { searchParams } = new URL(req.url);
@@ -8,11 +11,11 @@ export async function GET(req: Request) {
8
11
  return NextResponse.json({ path: null });
9
12
  }
10
13
  try {
11
- const cwd = execSync(`tmux display-message -p -t ${session} '#{pane_current_path}'`, {
14
+ const { stdout } = await execAsync(`tmux display-message -p -t ${session} '#{pane_current_path}'`, {
12
15
  encoding: 'utf-8',
13
16
  timeout: 3000,
14
- }).trim();
15
- return NextResponse.json({ path: cwd || null });
17
+ });
18
+ return NextResponse.json({ path: stdout.trim() || null });
16
19
  } catch {
17
20
  return NextResponse.json({ path: null });
18
21
  }
@@ -224,8 +224,8 @@ export default function CodeViewer({ terminalRef }: { terminalRef: React.RefObje
224
224
  };
225
225
 
226
226
  fetchCwd();
227
- // Poll cwd every 5s (user might cd to a different directory)
228
- const timer = setInterval(fetchCwd, 5000);
227
+ // Poll cwd every 15s (user might cd to a different directory)
228
+ const timer = setInterval(fetchCwd, 15000);
229
229
  return () => { cancelled = true; clearInterval(timer); };
230
230
  }, [activeSession]);
231
231
 
@@ -247,35 +247,7 @@ export default function CodeViewer({ terminalRef }: { terminalRef: React.RefObje
247
247
  fetchDir();
248
248
  }, [currentDir]);
249
249
 
250
- // Poll for task completions in the current project
251
- useEffect(() => {
252
- if (!currentDir) return;
253
- const dirName = currentDir.split('/').pop() || '';
254
- const check = async () => {
255
- try {
256
- const res = await fetch('/api/tasks?status=done');
257
- const tasks = await res.json();
258
- if (!Array.isArray(tasks) || tasks.length === 0) return;
259
- const latest = tasks.find((t: any) => t.projectPath === currentDir || t.projectName === dirName);
260
- if (latest && latest.id !== lastTaskCheckRef.current && latest.completedAt) {
261
- // Only notify if completed in the last 30s
262
- const age = Date.now() - new Date(latest.completedAt).getTime();
263
- if (age < 30_000) {
264
- lastTaskCheckRef.current = latest.id;
265
- setTaskNotification({
266
- id: latest.id,
267
- status: latest.status,
268
- prompt: latest.prompt,
269
- sessionId: latest.conversationId,
270
- });
271
- setTimeout(() => setTaskNotification(null), 15_000);
272
- }
273
- }
274
- } catch {}
275
- };
276
- const timer = setInterval(check, 5000);
277
- return () => clearInterval(timer);
278
- }, [currentDir]);
250
+ // Task completion is notified via hook stop — no polling needed
279
251
 
280
252
  // Build git status map for tree coloring
281
253
  const gitMap: GitStatusMap = new Map(gitChanges.map(g => [g.path, g.status]));
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { useState, useEffect, useCallback, useRef, lazy, Suspense } from 'react';
4
4
  import { signOut } from 'next-auth/react';
5
- import TaskBoard from './TaskBoard';
6
- import TaskDetail from './TaskDetail';
7
- import TunnelToggle from './TunnelToggle';
5
+ const TaskBoard = lazy(() => import('./TaskBoard'));
6
+ const TaskDetail = lazy(() => import('./TaskDetail'));
7
+ const TunnelToggle = lazy(() => import('./TunnelToggle'));
8
8
  import type { Task } from '@/src/types';
9
9
  import type { WebTerminalHandle } from './WebTerminal';
10
10
 
@@ -181,7 +181,7 @@ export default function Dashboard({ user }: { user: any }) {
181
181
  return () => clearInterval(id);
182
182
  }, []);
183
183
 
184
- // Notification polling
184
+ // Notifications: poll unread count at 30s, full fetch when panel opens
185
185
  const fetchNotifications = useCallback(() => {
186
186
  fetch('/api/notifications').then(r => r.json()).then(data => {
187
187
  setNotifications(data.notifications || []);
@@ -191,10 +191,15 @@ export default function Dashboard({ user }: { user: any }) {
191
191
 
192
192
  useEffect(() => {
193
193
  fetchNotifications();
194
- const id = setInterval(fetchNotifications, 10000);
194
+ const id = setInterval(fetchNotifications, 30000);
195
195
  return () => clearInterval(id);
196
196
  }, [fetchNotifications]);
197
197
 
198
+ // Refresh full list when notification panel opens
199
+ useEffect(() => {
200
+ if (showNotifications) fetchNotifications();
201
+ }, [showNotifications, fetchNotifications]);
202
+
198
203
  // Heartbeat for online user tracking
199
204
  useEffect(() => {
200
205
  const ping = () => {
@@ -204,26 +209,35 @@ export default function Dashboard({ user }: { user: any }) {
204
209
  .catch(() => {});
205
210
  };
206
211
  ping();
207
- const id = setInterval(ping, 15_000); // every 15s
212
+ const id = setInterval(ping, 60_000); // every 60s
208
213
  return () => clearInterval(id);
209
214
  }, []);
210
215
 
211
216
  const fetchData = useCallback(async () => {
212
217
  try {
213
- const [tasksRes, statusRes, projectsRes] = await Promise.all([
214
- fetch('/api/tasks'),
215
- fetch('/api/status'),
216
- fetch('/api/projects'),
217
- ]);
218
- if (tasksRes.ok) setTasks(await tasksRes.json());
219
- if (statusRes.ok) { const s = await statusRes.json(); setProviders(s.providers); setUsage(s.usage); }
220
- if (projectsRes.ok) setProjects(await projectsRes.json());
218
+ // Only fetch what's needed for current view
219
+ const fetches: Promise<void>[] = [
220
+ fetch('/api/projects').then(async r => { if (r.ok) setProjects(await r.json()); }),
221
+ ];
222
+ // Tasks + status only when relevant tabs are active
223
+ if (viewMode === 'tasks' || viewMode === 'terminal') {
224
+ fetches.push(
225
+ fetch('/api/tasks').then(async r => { if (r.ok) setTasks(await r.json()); }),
226
+ );
227
+ }
228
+ if (viewMode === 'usage') {
229
+ fetches.push(
230
+ fetch('/api/status').then(async r => { if (r.ok) { const s = await r.json(); setProviders(s.providers); setUsage(s.usage); } }),
231
+ );
232
+ }
233
+ await Promise.all(fetches);
221
234
  } catch {}
222
- }, []);
235
+ }, [viewMode]);
223
236
 
224
237
  useEffect(() => {
225
238
  fetchData();
226
- const interval = setInterval(fetchData, 5000);
239
+ // Poll less aggressively: 10s instead of 5s
240
+ const interval = setInterval(fetchData, 10000);
227
241
  return () => clearInterval(interval);
228
242
  }, [fetchData]);
229
243
 
@@ -421,7 +435,7 @@ export default function Dashboard({ user }: { user: any }) {
421
435
  : 'border-[var(--border)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:border-[var(--text-secondary)]'
422
436
  }`}
423
437
  >Usage</button>
424
- <TunnelToggle />
438
+ <Suspense fallback={null}><TunnelToggle /></Suspense>
425
439
  {onlineCount.total > 0 && (
426
440
  <span className="text-[10px] text-[var(--text-secondary)] flex items-center gap-1" title={`${onlineCount.total} online${onlineCount.remote > 0 ? `, ${onlineCount.remote} remote` : ''}`}>
427
441
  <span className="text-green-500">●</span>
@@ -596,13 +610,13 @@ export default function Dashboard({ user }: { user: any }) {
596
610
  <>
597
611
  {/* Left — Task list */}
598
612
  <aside className="w-72 border-r border-[var(--border)] flex flex-col shrink-0">
599
- <TaskBoard tasks={tasks} activeId={activeTaskId} onSelect={setActiveTaskId} onRefresh={fetchData} />
613
+ <Suspense fallback={null}><TaskBoard tasks={tasks} activeId={activeTaskId} onSelect={setActiveTaskId} onRefresh={fetchData} /></Suspense>
600
614
  </aside>
601
615
 
602
616
  {/* Center — Task detail / empty state */}
603
617
  <main className="flex-1 flex flex-col min-w-0">
604
618
  {activeTask ? (
605
- <TaskDetail
619
+ <Suspense fallback={null}><TaskDetail
606
620
  task={activeTask}
607
621
  onRefresh={fetchData}
608
622
  onFollowUp={async (data) => {
@@ -615,7 +629,7 @@ export default function Dashboard({ user }: { user: any }) {
615
629
  setActiveTaskId(newTask.id);
616
630
  fetchData();
617
631
  }}
618
- />
632
+ /></Suspense>
619
633
  ) : (
620
634
  <div className="flex-1 flex items-center justify-center text-[var(--text-secondary)]">
621
635
  <div className="text-center space-y-2">
@@ -2292,11 +2292,36 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
2292
2292
  }
2293
2293
  };
2294
2294
 
2295
+ let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
2296
+ const reconnect = () => {
2297
+ if (disposed || reconnectTimer) return;
2298
+ term.write('\r\n\x1b[93m[Reconnecting...]\x1b[0m\r\n');
2299
+ reconnectTimer = setTimeout(() => {
2300
+ reconnectTimer = null;
2301
+ if (disposed) return;
2302
+ const newWs = new WebSocket(getWsUrl());
2303
+ wsRef.current = newWs;
2304
+ const sn = sessionNameRef.current || preferredSessionName;
2305
+ newWs.onopen = () => {
2306
+ newWs.send(JSON.stringify({ type: 'attach', sessionName: sn, cols: term.cols, rows: term.rows }));
2307
+ };
2308
+ newWs.onerror = () => { if (!disposed) reconnect(); };
2309
+ newWs.onclose = () => { if (!disposed) reconnect(); };
2310
+ newWs.onmessage = ws.onmessage;
2311
+ }, 2000);
2312
+ };
2313
+
2295
2314
  ws.onerror = () => {
2296
- if (!disposed) term.write('\r\n\x1b[91m[Connection error]\x1b[0m\r\n');
2315
+ if (!disposed) {
2316
+ term.write('\r\n\x1b[91m[Connection error]\x1b[0m\r\n');
2317
+ reconnect();
2318
+ }
2297
2319
  };
2298
2320
  ws.onclose = () => {
2299
- if (!disposed) term.write('\r\n\x1b[90m[Disconnected]\x1b[0m\r\n');
2321
+ if (!disposed) {
2322
+ term.write('\r\n\x1b[90m[Disconnected]\x1b[0m\r\n');
2323
+ reconnect();
2324
+ }
2300
2325
  };
2301
2326
 
2302
2327
  let launched = false;
@@ -2390,16 +2415,19 @@ function FloatingTerminal({ agentLabel, agentIcon, projectPath, agentCliId, cliC
2390
2415
  };
2391
2416
 
2392
2417
  term.onData(data => {
2393
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'input', data }));
2418
+ const activeWs = wsRef.current;
2419
+ if (activeWs?.readyState === WebSocket.OPEN) activeWs.send(JSON.stringify({ type: 'input', data }));
2394
2420
  });
2395
2421
  term.onResize(({ cols, rows }) => {
2396
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'resize', cols, rows }));
2422
+ const activeWs = wsRef.current;
2423
+ if (activeWs?.readyState === WebSocket.OPEN) activeWs.send(JSON.stringify({ type: 'resize', cols, rows }));
2397
2424
  });
2398
2425
 
2399
2426
  return () => {
2400
2427
  disposed = true;
2428
+ if (reconnectTimer) clearTimeout(reconnectTimer);
2401
2429
  ro.disconnect();
2402
- ws.close();
2430
+ (wsRef.current || ws).close();
2403
2431
  term.dispose();
2404
2432
  };
2405
2433
  });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.5.27",
3
+ "version": "0.5.29",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {