@aion0/forge 0.4.8 → 0.4.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 +5 -5
- package/components/CodeViewer.tsx +75 -5
- package/components/WebTerminal.tsx +13 -2
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Forge v0.4.
|
|
1
|
+
# Forge v0.4.9
|
|
2
2
|
|
|
3
3
|
Released: 2026-03-22
|
|
4
4
|
|
|
5
|
-
## Changes since v0.4.
|
|
5
|
+
## Changes since v0.4.8
|
|
6
6
|
|
|
7
|
-
###
|
|
8
|
-
-
|
|
7
|
+
### Other
|
|
8
|
+
- add browser window
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.4.
|
|
11
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.4.8...v0.4.9
|
|
@@ -189,6 +189,12 @@ export default function CodeViewer({ terminalRef }: { terminalRef: React.RefObje
|
|
|
189
189
|
const [editing, setEditing] = useState(false);
|
|
190
190
|
const [editContent, setEditContent] = useState('');
|
|
191
191
|
const [saving, setSaving] = useState(false);
|
|
192
|
+
const [browserOpen, setBrowserOpen] = useState(false);
|
|
193
|
+
const [browserUrl, setBrowserUrl] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('forge-browser-url') || '' : '');
|
|
194
|
+
const [browserKey, setBrowserKey] = useState(0);
|
|
195
|
+
const [browserWidth, setBrowserWidth] = useState(640);
|
|
196
|
+
const browserDragRef = useRef<{ startX: number; startW: number } | null>(null);
|
|
197
|
+
const [browserDragging, setBrowserDragging] = useState(false);
|
|
192
198
|
|
|
193
199
|
const handleCodeOpenChange = useCallback((open: boolean) => {
|
|
194
200
|
setCodeOpen(open);
|
|
@@ -420,11 +426,75 @@ export default function CodeViewer({ terminalRef }: { terminalRef: React.RefObje
|
|
|
420
426
|
</div>
|
|
421
427
|
)}
|
|
422
428
|
|
|
423
|
-
{/* Terminal —
|
|
424
|
-
<div className={codeOpen ? 'shrink-0' : 'flex-1'} style={codeOpen ? { height: terminalHeight } : undefined}>
|
|
425
|
-
<
|
|
426
|
-
<
|
|
427
|
-
|
|
429
|
+
{/* Terminal + Browser — main area */}
|
|
430
|
+
<div className={`flex ${codeOpen ? 'shrink-0' : 'flex-1'}`} style={codeOpen ? { height: terminalHeight } : undefined}>
|
|
431
|
+
<div className="flex-1 min-w-0">
|
|
432
|
+
<Suspense fallback={<div className="h-full flex items-center justify-center text-[var(--text-secondary)] text-xs">Loading...</div>}>
|
|
433
|
+
<WebTerminal ref={terminalRef} onActiveSession={handleActiveSession} onCodeOpenChange={handleCodeOpenChange} browserOpen={browserOpen} onBrowserToggle={() => { setBrowserOpen(v => !v); if (!browserOpen) setBrowserKey(k => k + 1); }} />
|
|
434
|
+
</Suspense>
|
|
435
|
+
</div>
|
|
436
|
+
{browserOpen && (
|
|
437
|
+
<>
|
|
438
|
+
<div
|
|
439
|
+
onMouseDown={(e) => {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
browserDragRef.current = { startX: e.clientX, startW: browserWidth };
|
|
442
|
+
setBrowserDragging(true);
|
|
443
|
+
const onMove = (ev: MouseEvent) => {
|
|
444
|
+
if (!browserDragRef.current) return;
|
|
445
|
+
setBrowserWidth(Math.max(320, Math.min(1200, browserDragRef.current.startW - (ev.clientX - browserDragRef.current.startX))));
|
|
446
|
+
};
|
|
447
|
+
const onUp = () => {
|
|
448
|
+
browserDragRef.current = null;
|
|
449
|
+
setBrowserDragging(false);
|
|
450
|
+
window.removeEventListener('mousemove', onMove);
|
|
451
|
+
window.removeEventListener('mouseup', onUp);
|
|
452
|
+
};
|
|
453
|
+
window.addEventListener('mousemove', onMove);
|
|
454
|
+
window.addEventListener('mouseup', onUp);
|
|
455
|
+
}}
|
|
456
|
+
className="w-1 bg-[var(--border)] cursor-col-resize shrink-0 hover:bg-[var(--accent)]/50"
|
|
457
|
+
/>
|
|
458
|
+
<div style={{ width: browserWidth }} className="shrink-0 flex flex-col">
|
|
459
|
+
<div className="flex items-center gap-1 px-2 py-1 border-b border-[var(--border)] bg-[var(--bg-tertiary)] shrink-0">
|
|
460
|
+
<input
|
|
461
|
+
type="text"
|
|
462
|
+
defaultValue={browserUrl}
|
|
463
|
+
placeholder="http://localhost:3000"
|
|
464
|
+
onKeyDown={e => {
|
|
465
|
+
if (e.key === 'Enter') {
|
|
466
|
+
const url = (e.target as HTMLInputElement).value.trim();
|
|
467
|
+
if (url) {
|
|
468
|
+
setBrowserUrl(url);
|
|
469
|
+
localStorage.setItem('forge-browser-url', url);
|
|
470
|
+
setBrowserKey(k => k + 1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}}
|
|
474
|
+
className="flex-1 bg-[var(--bg-secondary)] border border-[var(--border)] rounded px-2 py-0.5 text-[10px] text-[var(--text-primary)] focus:outline-none focus:border-[var(--accent)] min-w-0"
|
|
475
|
+
/>
|
|
476
|
+
<button onClick={() => setBrowserKey(k => k + 1)} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] px-1" title="Refresh">↻</button>
|
|
477
|
+
<button onClick={() => window.open(browserUrl, '_blank')} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)] px-1" title="Open in new tab">↗</button>
|
|
478
|
+
<button onClick={() => setBrowserOpen(false)} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--red)] px-1" title="Close">✕</button>
|
|
479
|
+
</div>
|
|
480
|
+
<div className="flex-1 relative">
|
|
481
|
+
{browserUrl ? (
|
|
482
|
+
<iframe
|
|
483
|
+
key={browserKey}
|
|
484
|
+
src={browserUrl}
|
|
485
|
+
className="absolute inset-0 w-full h-full border-0 bg-white"
|
|
486
|
+
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
|
|
487
|
+
/>
|
|
488
|
+
) : (
|
|
489
|
+
<div className="absolute inset-0 flex items-center justify-center text-[var(--text-secondary)] text-xs">
|
|
490
|
+
Enter a URL and press Enter
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
{browserDragging && <div className="absolute inset-0 z-10" />}
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
</>
|
|
497
|
+
)}
|
|
428
498
|
</div>
|
|
429
499
|
|
|
430
500
|
{/* Resize handle */}
|
|
@@ -15,6 +15,8 @@ export interface WebTerminalHandle {
|
|
|
15
15
|
export interface WebTerminalProps {
|
|
16
16
|
onActiveSession?: (sessionName: string | null) => void;
|
|
17
17
|
onCodeOpenChange?: (open: boolean) => void;
|
|
18
|
+
browserOpen?: boolean;
|
|
19
|
+
onBrowserToggle?: () => void;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
// ─── Types ───────────────────────────────────────────────────
|
|
@@ -164,7 +166,7 @@ let globalDragging = false;
|
|
|
164
166
|
|
|
165
167
|
// ─── Main component ─────────────────────────────────────────
|
|
166
168
|
|
|
167
|
-
const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function WebTerminal({ onActiveSession, onCodeOpenChange }, ref) {
|
|
169
|
+
const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function WebTerminal({ onActiveSession, onCodeOpenChange, browserOpen, onBrowserToggle }, ref) {
|
|
168
170
|
const [tabs, setTabs] = useState<TabState[]>(() => {
|
|
169
171
|
const tree = makeTerminal();
|
|
170
172
|
return [{ id: nextId++, label: 'Terminal 1', tree, ratios: {}, activeId: firstTerminalId(tree) }];
|
|
@@ -634,8 +636,17 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
634
636
|
Code
|
|
635
637
|
</button>
|
|
636
638
|
)}
|
|
639
|
+
{onBrowserToggle && (
|
|
640
|
+
<button
|
|
641
|
+
onClick={onBrowserToggle}
|
|
642
|
+
className={`text-[11px] px-3 py-1 rounded font-bold ${browserOpen ? 'text-white bg-blue-500 hover:bg-blue-400' : 'text-blue-400 border border-blue-500 hover:bg-blue-500 hover:text-white'}`}
|
|
643
|
+
title={browserOpen ? 'Close browser' : 'Open browser'}
|
|
644
|
+
>
|
|
645
|
+
Browser
|
|
646
|
+
</button>
|
|
647
|
+
)}
|
|
637
648
|
{activeTab && countTerminals(activeTab.tree) > 1 && (
|
|
638
|
-
<button onClick={onClosePane} className="text-[10px] px-2 py-0.5 text-
|
|
649
|
+
<button onClick={onClosePane} className="text-[10px] px-2 py-0.5 text-[var(--accent)] hover:text-red-400 hover:bg-[var(--term-border)] rounded font-medium">
|
|
639
650
|
Close Pane
|
|
640
651
|
</button>
|
|
641
652
|
)}
|