singularity-forge/web/components/sf/dual-terminal.tsx
ace-pm 172753c3b2 refactor(forge): complete gsd → forge rebrand across native, logging, and build system
- Rename native Rust crates: gsd-engine → forge-engine, gsd-ast → forge-ast, gsd-grep → forge-grep
- Update all crate dependencies (Cargo.toml, .rs source) and N-API artifacts
- Mass rename log prefix [gsd] → [forge] across 81 files (scripts, src/, extensions, tests)
- Rename log prefix "gsd-db:" → "forge-db:" in template literals
- Update nix flake: add sf-run-native devShell with Rust toolchain for native addon builds
- Update CI workflow artifact names (build-native.yml)
- Verify only packages/native/* touched (no upstream pi-* packages renamed)

Rationale: Complete gsd-2 → singularity-forge rebrand (2026-04-15). Native addon is
sf-run-specific; all gsd-prefixed logging and crate names must align with new identity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:11:45 +02:00

119 lines
4.5 KiB
TypeScript

"use client"
import { useState, useRef, useEffect } from "react"
import { GripVertical, Loader2 } from "lucide-react"
import { MainSessionTerminal } from "@/components/sf/main-session-terminal"
import { ShellTerminal } from "@/components/sf/shell-terminal"
import { useTerminalFontSize } from "@/lib/use-terminal-font-size"
import { useGSDWorkspaceState } from "@/lib/sf-workspace-store"
import { derivePendingWorkflowCommandLabel } from "@/lib/workflow-action-execution"
export function DualTerminal() {
const [splitPosition, setSplitPosition] = useState(50)
const containerRef = useRef<HTMLDivElement>(null)
const rootRef = useRef<HTMLDivElement>(null)
const isDragging = useRef(false)
const [terminalFontSize] = useTerminalFontSize()
const workspace = useGSDWorkspaceState()
const projectCwd = workspace.boot?.project.cwd
const pendingCommandLabel = derivePendingWorkflowCommandLabel({
commandInFlight: workspace.commandInFlight,
terminalLines: workspace.terminalLines,
})
const handleMouseDown = () => {
isDragging.current = true
}
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.current || !containerRef.current) return
const rect = containerRef.current.getBoundingClientRect()
const x = e.clientX - rect.left
const percent = (x / rect.width) * 100
setSplitPosition(Math.max(20, Math.min(80, percent)))
}
const handleMouseUp = () => {
isDragging.current = false
}
useEffect(() => {
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [])
// Prevent browser default file-open on drag/drop anywhere in the dual terminal.
// Uses native DOM listeners so xterm's internal DOM can't swallow the events first.
useEffect(() => {
const el = rootRef.current
if (!el) return
const preventDragDefault = (e: DragEvent) => {
e.preventDefault()
}
// Capture phase ensures we fire before any child element can consume the event
el.addEventListener("dragover", preventDragDefault, true)
el.addEventListener("drop", preventDragDefault, true)
return () => {
el.removeEventListener("dragover", preventDragDefault, true)
el.removeEventListener("drop", preventDragDefault, true)
}
}, [])
return (
<div ref={rootRef} className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-border bg-card px-4 py-2">
<span className="font-medium">Power User Mode</span>
<div className="flex items-center gap-4 text-xs text-muted-foreground">
{pendingCommandLabel && (
<span
className="inline-flex items-center gap-1.5 rounded-full border border-primary/20 bg-primary/10 px-2.5 py-1 text-primary"
data-testid="power-mode-pending-command"
title={pendingCommandLabel}
>
<Loader2 className="h-3 w-3 animate-spin" />
Sending {pendingCommandLabel}
</span>
)}
<span>Left: Main Session TUI</span>
<span className="text-border">|</span>
<span>Right: Interactive GSD</span>
</div>
</div>
{/* Split terminals */}
<div ref={containerRef} className="flex flex-1 overflow-hidden">
{/* Left terminal - Main bridge native TUI */}
<div style={{ width: `${splitPosition}%` }} className="flex h-full min-w-0 flex-col overflow-hidden bg-terminal">
<MainSessionTerminal className="min-h-0 flex-1" fontSize={terminalFontSize} projectCwd={projectCwd} />
</div>
{/* Divider */}
<div
className="flex w-1 cursor-col-resize items-center justify-center bg-border hover:bg-muted-foreground/30 transition-colors"
onMouseDown={handleMouseDown}
>
<GripVertical className="h-4 w-4 text-muted-foreground" />
</div>
{/* Right terminal - Interactive GSD instance */}
<div style={{ width: `${100 - splitPosition}%` }} className="h-full min-w-0 overflow-hidden bg-terminal">
<ShellTerminal
className="h-full"
command="gsd"
sessionPrefix="gsd-interactive"
fontSize={terminalFontSize}
hideInitialGsdHeader
projectCwd={projectCwd}
/>
</div>
</div>
</div>
)
}