@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 CHANGED
@@ -1,11 +1,11 @@
1
- # Forge v0.4.8
1
+ # Forge v0.4.9
2
2
 
3
3
  Released: 2026-03-22
4
4
 
5
- ## Changes since v0.4.7
5
+ ## Changes since v0.4.8
6
6
 
7
- ### Features
8
- - feat: image preview, all file types support, docs tab limit
7
+ ### Other
8
+ - add browser window
9
9
 
10
10
 
11
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.4.7...v0.4.8
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 — top */}
424
- <div className={codeOpen ? 'shrink-0' : 'flex-1'} style={codeOpen ? { height: terminalHeight } : undefined}>
425
- <Suspense fallback={<div className="h-full flex items-center justify-center text-[var(--text-secondary)] text-xs">Loading...</div>}>
426
- <WebTerminal ref={terminalRef} onActiveSession={handleActiveSession} onCodeOpenChange={handleCodeOpenChange} />
427
- </Suspense>
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-gray-400 hover:text-red-400 hover:bg-[var(--term-border)] rounded">
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
  )}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {