@aion0/forge 0.5.27 → 0.5.28
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 +11 -13
- package/app/api/terminal-cwd/route.ts +7 -4
- package/components/CodeViewer.tsx +3 -31
- package/components/Dashboard.tsx +34 -20
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
# Forge v0.5.
|
|
1
|
+
# Forge v0.5.28
|
|
2
2
|
|
|
3
|
-
Released: 2026-04-
|
|
3
|
+
Released: 2026-04-09
|
|
4
4
|
|
|
5
|
-
## Changes since v0.5.
|
|
6
|
-
|
|
7
|
-
### Features
|
|
8
|
-
- feat: tmux mouse toggle button in terminal toolbar
|
|
5
|
+
## Changes since v0.5.27
|
|
9
6
|
|
|
10
7
|
### Bug Fixes
|
|
11
|
-
- fix:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
8
|
+
- fix: restore notification polling for Telegram, add Suspense wrappers
|
|
9
|
+
|
|
10
|
+
### Performance
|
|
11
|
+
- perf: notifications fetch on-demand instead of polling
|
|
12
|
+
- perf: remove task completion polling (replaced by hook stop)
|
|
13
|
+
- perf: reduce polling frequency and lazy-load non-essential components
|
|
14
|
+
- perf: async terminal-cwd to avoid blocking event loop
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.
|
|
17
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.5.27...v0.5.28
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
|
-
import {
|
|
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
|
|
14
|
+
const { stdout } = await execAsync(`tmux display-message -p -t ${session} '#{pane_current_path}'`, {
|
|
12
15
|
encoding: 'utf-8',
|
|
13
16
|
timeout: 3000,
|
|
14
|
-
})
|
|
15
|
-
return NextResponse.json({ path:
|
|
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
|
|
228
|
-
const timer = setInterval(fetchCwd,
|
|
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
|
-
//
|
|
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]));
|
package/components/Dashboard.tsx
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
//
|
|
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,
|
|
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,
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
fetch('/api/
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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">
|
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