summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/src/components/terminal.tsx97
-rw-r--r--packages/app/src/pages/session.tsx4
-rw-r--r--packages/opencode/src/pty/index.ts31
3 files changed, 81 insertions, 51 deletions
diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx
index 4fe6f0f0d..a298e3f76 100644
--- a/packages/app/src/components/terminal.tsx
+++ b/packages/app/src/components/terminal.tsx
@@ -36,12 +36,14 @@ export const Terminal = (props: TerminalProps) => {
const theme = useTheme()
let container!: HTMLDivElement
const [local, others] = splitProps(props, ["pty", "class", "classList", "onConnectError"])
- let ws: WebSocket
- let term: Term
+ let ws: WebSocket | undefined
+ let term: Term | undefined
let ghostty: Ghostty
let serializeAddon: SerializeAddon
let fitAddon: FitAddon
let handleResize: () => void
+ let reconnect: number | undefined
+ let disposed = false
const getTerminalColors = (): TerminalColors => {
const mode = theme.mode()
@@ -76,22 +78,24 @@ export const Terminal = (props: TerminalProps) => {
if (!term || !term.hasSelection()) return false
const selection = term.getSelection()
if (!selection) return false
+ if (document.body) {
+ const textarea = document.createElement("textarea")
+ textarea.value = selection
+ textarea.setAttribute("readonly", "")
+ textarea.style.position = "fixed"
+ textarea.style.opacity = "0"
+ document.body.appendChild(textarea)
+ textarea.select()
+ const copied = document.execCommand("copy")
+ document.body.removeChild(textarea)
+ if (copied) return true
+ }
const clipboard = navigator.clipboard
if (clipboard?.writeText) {
clipboard.writeText(selection).catch(() => {})
return true
}
- if (!document.body) return false
- const textarea = document.createElement("textarea")
- textarea.value = selection
- textarea.setAttribute("readonly", "")
- textarea.style.position = "fixed"
- textarea.style.opacity = "0"
- document.body.appendChild(textarea)
- textarea.select()
- const copied = document.execCommand("copy")
- document.body.removeChild(textarea)
- return copied
+ return false
}
const handlePointerDown = () => {
const activeElement = document.activeElement
@@ -104,8 +108,12 @@ export const Terminal = (props: TerminalProps) => {
onMount(async () => {
ghostty = await Ghostty.load()
- ws = new WebSocket(sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`)
- term = new Term({
+ const socket = new WebSocket(
+ sdk.url + `/pty/${local.pty.id}/connect?directory=${encodeURIComponent(sdk.directory)}`,
+ )
+ ws = socket
+
+ const t = new Term({
cursorBlink: true,
fontSize: 14,
fontFamily: "IBM Plex Mono, monospace",
@@ -114,7 +122,9 @@ export const Terminal = (props: TerminalProps) => {
scrollback: 10_000,
ghostty,
})
- term.attachCustomKeyEventHandler((event) => {
+ term = t
+
+ t.attachCustomKeyEventHandler((event) => {
const key = event.key.toLowerCase()
if (key === "c") {
const macCopy = event.metaKey && !event.ctrlKey && !event.altKey
@@ -134,21 +144,21 @@ export const Terminal = (props: TerminalProps) => {
fitAddon = new FitAddon()
serializeAddon = new SerializeAddon()
- term.loadAddon(serializeAddon)
- term.loadAddon(fitAddon)
+ t.loadAddon(serializeAddon)
+ t.loadAddon(fitAddon)
- term.open(container)
+ t.open(container)
container.addEventListener("pointerdown", handlePointerDown)
focusTerminal()
if (local.pty.buffer) {
if (local.pty.rows && local.pty.cols) {
- term.resize(local.pty.cols, local.pty.rows)
+ t.resize(local.pty.cols, local.pty.rows)
}
- term.reset()
- term.write(local.pty.buffer, () => {
+ t.reset()
+ t.write(local.pty.buffer, () => {
if (local.pty.scrollY) {
- term.scrollToLine(local.pty.scrollY)
+ t.scrollToLine(local.pty.scrollY)
}
fitAddon.fit()
})
@@ -157,8 +167,8 @@ export const Terminal = (props: TerminalProps) => {
fitAddon.observeResize()
handleResize = () => fitAddon.fit()
window.addEventListener("resize", handleResize)
- term.onResize(async (size) => {
- if (ws && ws.readyState === WebSocket.OPEN) {
+ t.onResize(async (size) => {
+ if (socket.readyState === WebSocket.OPEN) {
await sdk.client.pty
.update({
ptyID: local.pty.id,
@@ -170,39 +180,39 @@ export const Terminal = (props: TerminalProps) => {
.catch(() => {})
}
})
- term.onData((data) => {
- if (ws && ws.readyState === WebSocket.OPEN) {
- ws.send(data)
+ t.onData((data) => {
+ if (socket.readyState === WebSocket.OPEN) {
+ socket.send(data)
}
})
- term.onKey((key) => {
+ t.onKey((key) => {
if (key.key == "Enter") {
props.onSubmit?.()
}
})
- // term.onScroll((ydisp) => {
+ // t.onScroll((ydisp) => {
// console.log("Scroll position:", ydisp)
// })
- ws.addEventListener("open", () => {
+ socket.addEventListener("open", () => {
console.log("WebSocket connected")
sdk.client.pty
.update({
ptyID: local.pty.id,
size: {
- cols: term.cols,
- rows: term.rows,
+ cols: t.cols,
+ rows: t.rows,
},
})
.catch(() => {})
})
- ws.addEventListener("message", (event) => {
- term.write(event.data)
+ socket.addEventListener("message", (event) => {
+ t.write(event.data)
})
- ws.addEventListener("error", (error) => {
+ socket.addEventListener("error", (error) => {
console.error("WebSocket error:", error)
props.onConnectError?.(error)
})
- ws.addEventListener("close", () => {
+ socket.addEventListener("close", () => {
console.log("WebSocket disconnected")
})
})
@@ -212,18 +222,21 @@ export const Terminal = (props: TerminalProps) => {
window.removeEventListener("resize", handleResize)
}
container.removeEventListener("pointerdown", handlePointerDown)
- if (serializeAddon && props.onCleanup) {
+
+ const t = term
+ if (serializeAddon && props.onCleanup && t) {
const buffer = serializeAddon.serialize()
props.onCleanup({
...local.pty,
buffer,
- rows: term.rows,
- cols: term.cols,
- scrollY: term.getViewportY(),
+ rows: t.rows,
+ cols: t.cols,
+ scrollY: t.getViewportY(),
})
}
+
ws?.close()
- term?.dispose()
+ t?.dispose()
})
return (
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index 3c23ac61e..b2da4bf03 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -863,9 +863,9 @@ export default function Page() {
data-message-id={message.id}
classList={{
"min-w-0 w-full max-w-full": true,
- "last:min-h-[calc(100vh-13.5rem)] md:last:min-h-[calc(100vh-14.5rem)]":
+ "last:min-h-[calc(100vh-5.5rem-var(--prompt-height,8rem)-32px)] md:last:min-h-[calc(100vh-4.5rem-var(--prompt-height,10rem)-32px)]":
platform.platform !== "desktop",
- "last:min-h-[calc(100vh-15rem)] md:last:min-h-[calc(100vh-16rem)]":
+ "last:min-h-[calc(100vh-7rem-var(--prompt-height,8rem)-32px)] md:last:min-h-[calc(100vh-6rem-var(--prompt-height,10rem)-32px)]":
platform.platform === "desktop",
}}
>
diff --git a/packages/opencode/src/pty/index.ts b/packages/opencode/src/pty/index.ts
index afc6a78da..39ccebf96 100644
--- a/packages/opencode/src/pty/index.ts
+++ b/packages/opencode/src/pty/index.ts
@@ -12,6 +12,9 @@ import { Shell } from "@/shell/shell"
export namespace Pty {
const log = Log.create({ service: "pty" })
+ const BUFFER_LIMIT = 1024 * 1024 * 2
+ const BUFFER_CHUNK = 64 * 1024
+
const pty = lazy(async () => {
const { spawn } = await import("bun-pty")
return spawn
@@ -126,15 +129,19 @@ export namespace Pty {
}
state().set(id, session)
ptyProcess.onData((data) => {
- if (session.subscribers.size === 0) {
- session.buffer += data
- return
- }
+ let open = false
for (const ws of session.subscribers) {
- if (ws.readyState === 1) {
- ws.send(data)
+ if (ws.readyState !== 1) {
+ session.subscribers.delete(ws)
+ continue
}
+ open = true
+ ws.send(data)
}
+ if (open) return
+ session.buffer += data
+ if (session.buffer.length <= BUFFER_LIMIT) return
+ session.buffer = session.buffer.slice(-BUFFER_LIMIT)
})
ptyProcess.onExit(({ exitCode }) => {
log.info("session exited", { id, exitCode })
@@ -196,8 +203,18 @@ export namespace Pty {
log.info("client connected to session", { id })
session.subscribers.add(ws)
if (session.buffer) {
- ws.send(session.buffer)
+ const buffer = session.buffer.length <= BUFFER_LIMIT ? session.buffer : session.buffer.slice(-BUFFER_LIMIT)
session.buffer = ""
+ try {
+ for (let i = 0; i < buffer.length; i += BUFFER_CHUNK) {
+ ws.send(buffer.slice(i, i + BUFFER_CHUNK))
+ }
+ } catch {
+ session.subscribers.delete(ws)
+ session.buffer = buffer
+ ws.close()
+ return
+ }
}
return {
onMessage: (message: string | ArrayBuffer) => {