FloeTerm is semantic terminal infrastructure for product teams. A Go SessionActor
owns the PTY and one native Ghostty VT instance. Browsers receive immutable semantic
presentations and provide view-local rendering, palette, selection, and input.
FloeTerm has one terminal state owner:
PTY bytes/input/resize/history
|
v
terminal-go SessionActor + native Ghostty VT
|
v
immutable SemanticPresentation
|
v
terminal-web RendererSurface + TerminalInputBridge
The browser does not run a second VT parser, restore raw checkpoints, replay a raw journal, or own PTY geometry. Every view renders the same authoritative frame while keeping its palette, canvas backing store, selection, crop/pad, and IME anchor local.
Key contracts:
- PTY output, structured input, resize, semantic clear, and semantic history are serialized by the session actor.
- User-visible clear is a generation-bound
Session.ClearSemanticScreenoperation: the native VT owner resets screen, bounded semantic history, graphics, and view projections, then publishes one newcontentEpochto every attached view. It never sends Ctrl-L or clears only a browser canvas. - A Presentation contains matching state, geometry, frame, cursor, and graphics.
- Live transport uses a bounded reliable FIFO plus one latest-Presentation slot.
- History transport captures one actor-owned immutable viewport at canonical geometry. It may split that snapshot into bounded chunks, but a browser validates and reassembles every chunk before atomically projecting a complete viewport.
- Only the current controller changes PTY geometry or sends input; observers remain render-only.
- A real interaction may bind its measured viewport and input in one ordered actor admission. This transfers same-principal control, applies canonical geometry, and writes the input exactly once without waiting for a separate activation round trip.
- Resize acknowledgements mean canonical geometry was actually applied.
- IME composition commits Unicode input exactly once and anchors to the semantic cursor without applying device-pixel ratio twice.
| Package | Contract |
|---|---|
terminal-go |
PTY lifecycle, native Ghostty SessionActor, canonical geometry, controller ownership, semantic presentations, bounded semantic history, and live protocol backend |
terminal-web |
Presentation validator, canvas RendererSurface, TerminalInputBridge, semantic live transport, themes, and session metadata utilities |
app/ |
Reference HTTP/WebSocket backend and Solid.js UI for single, mirror, and grid views |
Install the released packages:
go get github.com/floegence/floeterm/terminal-go@v0.18.1
npm install @floegence/floeterm-terminal-web@0.18.1Use explicit semantic subpaths so product adapters depend only on the capability they need:
import {
RendererSurface,
HistorySearchController,
HistoryViewportController,
TerminalInputBridge,
getThemeColors,
validatePresentation,
} from '@floegence/floeterm-terminal-web/semantic';
import {
createSemanticTerminalLiveTransport,
} from '@floegence/floeterm-terminal-web/live';
const canvas = document.querySelector('canvas')!;
const renderer = new RendererSurface(canvas, console.error);
renderer.setPalette(getThemeColors('tokyoNight'));
const bundle = createSemanticTerminalLiveTransport({
connectionId: crypto.randomUUID(),
openStream,
control,
});
const history = new HistoryViewportController({
renderer,
request: request => bundle.transport.semanticHistory(sessionId, request),
});
const historySearch = new HistorySearchController({
request: request => bundle.transport.semanticHistory(sessionId, request),
});
const unsubscribe = bundle.eventSource.onTerminalPresentation(sessionId, value => {
const presentation = validatePresentation(value);
history.apply(presentation);
historySearch.apply(presentation);
});
// Invoke only for a real pointer/keyboard activation, before its input write.
await bundle.transport.activate(sessionId, desiredCols, desiredRows);
const input = new TerminalInputBridge({
inputHost,
inputElement,
onData: data => void bundle.transport.sendInput(sessionId, data),
onInputIntent: intent => void bundle.transport.sendInputIntent(sessionId, intent),
onPaste: data => void bundle.transport.sendPaste(sessionId, data),
syncInputGeometry: () => positionInputAt(renderer.getCursorLayoutRect()),
});getCursorLayoutRect() is for an absolutely positioned input bridge that shares
the canvas containing block. A fixed or portal input bridge should instead use
getCursorClientRect(). Both APIs return CSS pixels and already account for the
appropriate transformed coordinate space.
RendererSurface is the only canvas writer. Host bounds determine CSS size; the
renderer updates DPR backing, fills the full background, and paints the latest
Presentation in one scheduled draw. Theme changes repaint the same Presentation and
never mutate the PTY or transport sequence. Keep-mounted panes call
renderer.setVisible(false) while hidden and renderer.setVisible(true) only after
their active host bounds are committed; the canvas remains hidden until its current
DPR backing and latest Presentation are painted without CSS stretching.
manager := terminal.NewManager(terminal.ManagerConfig{})
session, err := manager.CreateSession("shell", "")
if err != nil {
return err
}
if err := manager.ActivateSession(session.ID, 120, 40); err != nil {
return err
}Launch a program with an exact argument vector when the session must not use a login shell or shell initialization:
session, err := manager.CreateProgramSession("container", "", terminal.Program{
Executable: "docker",
Args: []string{"exec", "-it", "example", "/bin/sh"},
})Use livev1.NewService with the manager backend for the bidirectional semantic live
stream. The reference server in app/backend shows attach, input,
resize, generation-bound semantic clear, presentation, lifecycle, and
semantic-history endpoints.
Build and run on loopback:
make runThen open http://127.0.0.1:8280. The app supports single, mirrored, and grid views
of one session, view-local themes, cursor shapes and visibility, IME, CJK/emoji,
Kitty graphics, reconnect, and continuous resize.
make checkThe final gate runs Go race tests and vulnerability checks, terminal-web unit/browser
and package-artifact checks, app tests, real-process Playwright E2E, and npm audits.
Native focused checks are available with make native-check.
The terminal-go/internal/nativevt/generated directory contains reproducible static
archives for Darwin/Linux on amd64/arm64, the thin public-API adapter, Ghostty license,
and exact source/artifact hashes. Regenerate them with scripts/build_native_vt.sh
from the pinned Ghostty source and toolchain recorded by that script.
| Path | Purpose |
|---|---|
terminal-go/ |
Go PTY/session actor and native semantic engine |
terminal-web/ |
Framework-neutral semantic browser package |
app/backend/ |
Reference control plane and WebSocket service |
app/web/ |
Reference semantic terminal UI |
e2e/ |
Real-process functional and diagnostic performance tests |
See THIRD_PARTY_NOTICES.md for third-party licensing.