aiprompts/fe-conn-arch.md
The frontend connection architecture provides a reactive interface for managing and interacting with connections (local, SSH, WSL, S3). It follows a unidirectional data flow pattern where the backend manages connection state, the frontend observes this state through Jotai atoms, and user interactions trigger backend operations via RPC commands.
┌─────────────────────────────────────────────────────────────────┐
│ User Interface │
│ - ConnectionButton (displays status) │
│ - ChangeConnectionBlockModal (connection picker) │
│ - ConnStatusOverlay (error states) │
└─────────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────────┐
│ Jotai Reactive State │
│ - ConnStatusMapAtom (connection statuses) │
│ - View Model Atoms (derived connection state) │
│ - Block Metadata (connection selection) │
└─────────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────────┐
│ RPC Commands │
│ - ConnListCommand (list connections) │
│ - ConnEnsureCommand (ensure connected) │
│ - ConnConnectCommand/ConnDisconnectCommand │
│ - SetMetaCommand (change block connection) │
│ - ControllerInputCommand (send data to shell) │
└─────────────────────────────────────────────────────────────────┘
↕
┌─────────────────────────────────────────────────────────────────┐
│ Backend (see conn-arch.md) │
│ - Connection Controllers (SSHConn, WslConn) │
│ - Block Controllers (ShellController) │
│ - Shell Process Execution │
└─────────────────────────────────────────────────────────────────┘
frontend/app/store/global.ts)ConnStatusMapAtom
const ConnStatusMapAtom = atom(new Map<string, PrimitiveAtom<ConnStatus>>())
getConnStatusAtom()
function getConnStatusAtom(connName: string): PrimitiveAtom<ConnStatus>
ConnStatus Structure
interface ConnStatus {
status: "init" | "connecting" | "connected" | "disconnected" | "error"
connection: string // Connection name
connected: boolean // Is currently connected
activeconnnum: number // Color assignment number (1-8)
wshenabled: boolean // WSH available on this connection
error?: string // Error message if status is "error"
wsherror?: string // WSH-specific error
}
allConnStatusAtom
const allConnStatusAtom = atom<ConnStatus[]>((get) => {
const connStatusMap = get(ConnStatusMapAtom)
const connStatuses = Array.from(connStatusMap.values()).map((atom) => get(atom))
return connStatuses
})
frontend/app/block/blockutil.tsx)ConnectionButton Component
export const ConnectionButton = React.memo(
React.forwardRef<HTMLDivElement, ConnectionButtonProps>(
({ connection, changeConnModalAtom }, ref) => {
const connStatusAtom = getConnStatusAtom(connection)
const connStatus = jotai.useAtomValue(connStatusAtom)
// ... renders connection status with colored icon
}
)
)
Responsibilities:
Color Assignment:
function computeConnColorNum(connStatus: ConnStatus): number {
const connColorNum = (connStatus?.activeconnnum ?? 1) % NumActiveConnColors
return connColorNum == 0 ? NumActiveConnColors : connColorNum
}
activeconnnum sequentiallyvar(--conn-icon-color-1) through var(--conn-icon-color-8)frontend/app/modals/conntypeahead.tsx)ChangeConnectionBlockModal Component
Data Fetching:
useEffect(() => {
if (!changeConnModalOpen) return
// Fetch available connections
RpcApi.ConnListCommand(TabRpcClient, { timeout: 2000 })
.then(setConnList)
RpcApi.WslListCommand(TabRpcClient, { timeout: 2000 })
.then(setWslList)
RpcApi.ConnListAWSCommand(TabRpcClient, { timeout: 2000 })
.then(setS3List)
}, [changeConnModalOpen])
Connection Change Handler:
const changeConnection = async (connName: string) => {
// Update block metadata with new connection
await RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", blockId),
meta: {
connection: connName,
file: newFile, // Reset file path for new connection
"cmd:cwd": null // Clear working directory
}
})
// Ensure connection is established
await RpcApi.ConnEnsureCommand(TabRpcClient, {
connname: connName,
logblockid: blockId
}, { timeout: 60000 })
}
Suggestion Categories:
Local Connections
"" or "local:")"local:gitbash")"wsl://Ubuntu", etc.)Remote Connections (SSH)
"user@host" or "user@host:port"display:hidden configS3 Connections (optional)
"aws:profile-name"Actions
Filtering Logic:
function filterConnections(
connList: Array<string>,
connSelected: string,
fullConfig: FullConfigType,
filterOutNowsh: boolean
): Array<string> {
const connectionsConfig = fullConfig.connections
return connList.filter((conn) => {
const hidden = connectionsConfig?.[conn]?.["display:hidden"] ?? false
const wshEnabled = connectionsConfig?.[conn]?.["conn:wshenabled"] ?? true
return conn.includes(connSelected) &&
!hidden &&
(wshEnabled || !filterOutNowsh)
})
}
frontend/app/block/blockframe.tsx)ConnStatusOverlay Component
Displays over block content when:
Features:
Handlers:
// Reconnect to failed connection
const handleTryReconnect = () => {
RpcApi.ConnConnectCommand(TabRpcClient, {
host: connName,
logblockid: nodeModel.blockId
}, { timeout: 60000 })
}
// Disable WSH for this connection
const handleDisableWsh = async () => {
await RpcApi.SetConnectionsConfigCommand(TabRpcClient, {
host: connName,
metamaptype: { "conn:wshenabled": false }
})
}
View models integrate connection state into their reactive data flow:
frontend/app/view/term/term-model.ts)class TermViewModel implements ViewModel {
// Connection management flag
manageConnection = atom((get) => {
const termMode = get(this.termMode)
if (termMode == "vdom") return false // VDOM mode doesn't show conn button
const isCmd = get(this.isCmdController)
if (isCmd) return false // Cmd controller doesn't manage connections
return true // Standard terminals show connection button
})
// Connection status for this block
connStatus = atom((get) => {
const blockData = get(this.blockAtom)
const connName = blockData?.meta?.connection
const connAtom = getConnStatusAtom(connName)
return get(connAtom)
})
// Filter connections without WSH
filterOutNowsh = atom(false)
}
End Icon Button Logic:
endIconButtons = atom((get) => {
const connStatus = get(this.connStatus)
const shellProcStatus = get(this.shellProcStatus)
// Only show restart button if connected
if (connStatus?.status != "connected") {
return []
}
// Show appropriate icon based on shell state
if (shellProcStatus == "init") {
return [{ icon: "play", title: "Click to Start Shell" }]
} else if (shellProcStatus == "running") {
return [{ icon: "refresh", title: "Shell Running. Click to Restart" }]
} else if (shellProcStatus == "done") {
return [{ icon: "refresh", title: "Shell Exited. Click to Restart" }]
}
})
frontend/app/view/preview/preview-model.tsx)class PreviewModel implements ViewModel {
// Always manages connection
manageConnection = atom(true)
// Connection status
connStatus = atom((get) => {
const blockData = get(this.blockAtom)
const connName = blockData?.meta?.connection
const connAtom = getConnStatusAtom(connName)
return get(connAtom)
})
// Filter out connections without WSH (file ops require WSH)
filterOutNowsh = atom(true)
// Ensure connection before operations
connection = atom<Promise<string>>(async (get) => {
const connName = get(this.blockAtom)?.meta?.connection
try {
await RpcApi.ConnEnsureCommand(TabRpcClient, {
connname: connName
}, { timeout: 60000 })
globalStore.set(this.connectionError, "")
} catch (e) {
globalStore.set(this.connectionError, e as string)
}
return connName
})
}
File Operations Over Connection:
// Reads file from remote/local connection
statFile = atom<Promise<FileInfo>>(async (get) => {
const fileName = get(this.metaFilePath)
const path = await this.formatRemoteUri(fileName, get)
return await RpcApi.FileInfoCommand(TabRpcClient, {
info: { path }
})
})
fullFile = atom<Promise<FileData>>(async (get) => {
const fileName = get(this.metaFilePath)
const path = await this.formatRemoteUri(fileName, get)
return await RpcApi.FileReadCommand(TabRpcClient, {
info: { path }
})
})
View models do NOT directly manage shell processes. They interact with block controllers via RPC:
Starting a Shell:
// User clicks restart button in terminal
forceRestartController() {
// Backend handles connection verification and process startup
RpcApi.ControllerRestartCommand(TabRpcClient, {
blockid: this.blockId,
force: true
})
}
Sending Input to Shell:
sendDataToController(data: string) {
const b64data = stringToBase64(data)
RpcApi.ControllerInputCommand(TabRpcClient, {
blockid: this.blockId,
inputdata64: b64data
})
}
Backend Block Controller Flow:
ControllerRestartCommandShellController.Run() startsCheckConnStatus() verifies connection is readysetupAndStartShellProcess()getConnUnion() retrieves appropriate connection (Local/SSH/WSL)StartLocalShellProc(), StartRemoteShellProc(), or StartWslShellProc()manageRunningShellProcess()Wave uses a three-level config hierarchy for connections:
settings)connections[connName])block.meta)Override Resolution:
function getOverrideConfigAtom<T>(blockId: string, key: T): Atom<T> {
return atom((get) => {
// 1. Check block metadata
const metaKeyVal = get(getBlockMetaKeyAtom(blockId, key))
if (metaKeyVal != null) return metaKeyVal
// 2. Check connection config
const connName = get(getBlockMetaKeyAtom(blockId, "connection"))
const connConfigKeyVal = get(getConnConfigKeyAtom(connName, key))
if (connConfigKeyVal != null) return connConfigKeyVal
// 3. Fall back to global settings
const settingsVal = get(getSettingsKeyAtom(key))
return settingsVal ?? null
})
}
Connection Keywords (apply to specific connections):
conn:wshenabled - Enable/disable WSH for this connectionconn:wshpath - Custom WSH binary pathdisplay:hidden - Hide connection from selectordisplay:order - Sort order in connection listterm:fontsize - Font size for terminals on this connectionterm:theme - Color theme for terminals on this connectionExample Usage in View Models:
// Font size with connection override
fontSizeAtom = atom((get) => {
const blockData = get(this.blockAtom)
const connName = blockData?.meta?.connection
const fullConfig = get(atoms.fullConfigAtom)
// Check: block meta > connection config > global settings
const fontSize = blockData?.meta?.["term:fontsize"] ??
fullConfig?.connections?.[connName]?.["term:fontsize"] ??
get(getSettingsKeyAtom("term:fontsize")) ??
12
return boundNumber(fontSize, 4, 64)
})
ConnListCommand
ConnListCommand(client: RpcClient): Promise<string[]>
display:hidden config on frontendWslListCommand
WslListCommand(client: RpcClient): Promise<string[]>
wsl://[distro]ConnListAWSCommand
ConnListAWSCommand(client: RpcClient): Promise<string[]>
aws:[profile]ConnEnsureCommand
ConnEnsureCommand(
client: RpcClient,
data: { connname: string, logblockid?: string }
): Promise<void>
ConnConnectCommand
ConnConnectCommand(
client: RpcClient,
data: { host: string, logblockid?: string }
): Promise<void>
ConnDisconnectCommand
ConnDisconnectCommand(
client: RpcClient,
connName: string
): Promise<void>
SetMetaCommand
SetMetaCommand(
client: RpcClient,
data: {
oref: string, // WaveObject reference
meta: MetaType // Metadata updates
}
): Promise<void>
SetConnectionsConfigCommand
SetConnectionsConfigCommand(
client: RpcClient,
data: {
host: string, // Connection name
metamaptype: any // Config updates
}
): Promise<void>
conn:wshenabled: false)FileInfoCommand
FileInfoCommand(
client: RpcClient,
data: { info: { path: string } }
): Promise<FileInfo>
[connName]:[filepath] (e.g., user@host:~/file.txt)FileReadCommand
FileReadCommand(
client: RpcClient,
data: { info: { path: string } }
): Promise<FileData>
ControllerInputCommand
ControllerInputCommand(
client: RpcClient,
data: { blockid: string, inputdata64: string }
): Promise<void>
ControllerRestartCommand
ControllerRestartCommand(
client: RpcClient,
data: { blockid: string, force?: boolean }
): Promise<void>
Connection Status Updates:
waveEventSubscribe({
eventType: "connstatus",
handler: (event) => {
const status: ConnStatus = event.data
updateConnStatusAtom(status.connection, status)
}
})
Configuration Updates:
waveEventSubscribe({
eventType: "config",
handler: (event) => {
const fullConfig = event.data.fullconfig
globalStore.set(atoms.fullConfigAtom, fullConfig)
}
})
User Action: Click connection button → select new connection
↓
ChangeConnectionBlockModal.changeConnection()
↓
RpcApi.SetMetaCommand({ connection: newConn })
↓
Backend updates block metadata → emits waveobj:update
↓
Frontend WOS updates blockAtom
↓
View model connStatus atom recomputes
↓
ConnectionButton re-renders with new connection
↓
RpcApi.ConnEnsureCommand() ensures connected
↓
Backend triggers connection if needed
↓
Backend emits connstatus events as connection progresses
↓
Frontend updates ConnStatus atom ("connecting" → "connected")
↓
ConnectionButton shows connecting animation → connected state
User Action: Press Enter in disconnected terminal
↓
View model detects shellProcStatus == "init" or "done"
↓
forceRestartController() called
↓
RpcApi.ControllerRestartCommand()
↓
Backend ShellController.Run() starts
↓
CheckConnStatus() verifies connection
↓
If not connected: trigger connection
↓
(Frontend shows ConnStatusOverlay with "connecting")
↓
Connection succeeds → WSH available
↓
setupAndStartShellProcess()
↓
StartRemoteShellProc() with connection's SSH client
↓
Backend emits controllerstatus event
↓
Frontend updates shellProcStatus atom
↓
View model endIconButtons recomputes (restart button)
↓
Terminal ready for input
User Action: Open preview block with file path
↓
PreviewModel initialized with file path
↓
connection atom ensures connection
↓
RpcApi.ConnEnsureCommand(connName)
↓
Backend establishes connection if needed
↓
(Frontend shows ConnStatusOverlay if connecting)
↓
Connection ready
↓
statFile atom triggers FileInfoCommand
↓
Backend routes to connection's WSH
↓
WSH executes stat on remote file
↓
FileInfo returned to frontend
↓
PreviewModel determines if text/binary/streaming
↓
fullFile atom triggers FileReadCommand
↓
Backend streams file via WSH
↓
File content displayed in preview
Connection Names:
"" (empty string)"local""local:""local:gitbash" (Windows only)Frontend Behavior:
os/execView Model Configuration:
connName = "" // or "local" or "local:gitbash"
connStatus = {
status: "connected",
connection: "",
connected: true,
activeconnnum: 0, // No color assignment
wshenabled: true // Local WSH always available
}
Connection Names:
"user@host", "user@host:port", or config name"[email protected]", "myserver", "deploy@prod:2222"Frontend Behavior:
activeconnnumuser@host:~/file.txtConnection States:
// Connecting
connStatus = {
status: "connecting",
connection: "user@host",
connected: false,
activeconnnum: 3,
wshenabled: false // Not yet determined
}
// Connected with WSH
connStatus = {
status: "connected",
connection: "user@host",
connected: true,
activeconnnum: 3,
wshenabled: true
}
// Connected without WSH
connStatus = {
status: "connected",
connection: "user@host",
connected: true,
activeconnnum: 3,
wshenabled: false,
wsherror: "wsh installation failed: permission denied"
}
// Error
connStatus = {
status: "error",
connection: "user@host",
connected: false,
activeconnnum: 3,
wshenabled: false,
error: "ssh: connection refused"
}
WSH Errors:
conn:wshenabled: falseConnection Names:
"wsl://[distro]""wsl://Ubuntu", "wsl://Debian", "wsl://Ubuntu-20.04"Frontend Behavior:
wsl://Ubuntu:~/file.txtBackend Differences:
wsl.exe instead of SSHConnection Names:
"aws:[profile]""aws:default", "aws:production"Frontend Behavior:
aws:profile:/bucket/keyView Model Settings:
// Terminal: S3 not shown
showS3 = atom(false)
// Preview: S3 shown
showS3 = atom(true)
Authentication Failures:
userinput eventsNetwork Errors:
ConnConnectCommandWSH Installation Errors:
Terminal View:
// Shell won't start if connection failed
endIconButtons = atom((get) => {
const connStatus = get(this.connStatus)
if (connStatus?.status != "connected") {
return [] // Hide restart button
}
// ... show restart button
})
// ConnStatusOverlay blocks terminal interaction
Preview View:
// File operations return errors
errorMsgAtom = atom(null) as PrimitiveAtom<ErrorMsg>
statFile = atom(async (get) => {
try {
const fileInfo = await RpcApi.FileInfoCommand(...)
return fileInfo
} catch (e) {
globalStore.set(this.errorMsgAtom, {
status: "File Read Failed",
text: `${e}`
})
throw e
}
})
// Error displayed in preview content area
Use Connection Atoms:
connStatus = atom((get) => {
const blockData = get(this.blockAtom)
const connName = blockData?.meta?.connection
return get(getConnStatusAtom(connName))
})
Check Connection Before Operations:
if (connStatus?.status != "connected") {
return // Don't attempt operation
}
Use ConnEnsureCommand for File Ops:
await RpcApi.ConnEnsureCommand(TabRpcClient, {
connname: connName,
logblockid: blockId // For better logging
}, { timeout: 60000 })
Set manageConnection Appropriately:
// Show connection button for views that need connections
manageConnection = atom(true)
// Hide for views that don't use connections
manageConnection = atom(false)
Use filterOutNowsh for WSH Requirements:
// Filter connections without WSH (file ops, etc.)
filterOutNowsh = atom(true)
// Allow all connections (basic shell)
filterOutNowsh = atom(false)
Always Handle Errors:
try {
await RpcApi.ConnConnectCommand(...)
} catch (e) {
console.error("Connection failed:", e)
// Update UI to show error
}
Use Appropriate Timeouts:
// Connection operations: longer timeout
{ timeout: 60000 } // 60 seconds
// List operations: shorter timeout
{ timeout: 2000 } // 2 seconds
Batch Related Operations:
// Good: Single SetMetaCommand with all changes
await RpcApi.SetMetaCommand(TabRpcClient, {
oref: blockRef,
meta: {
connection: newConn,
file: newPath,
"cmd:cwd": null
}
})
// Bad: Multiple SetMetaCommand calls
The frontend connection architecture is reactive and declarative:
This architecture ensures: