diff --git a/package-lock.json b/package-lock.json
index 6d755e4..bb1da6d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "drover",
- "version": "0.1.18",
+ "version": "0.1.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "drover",
- "version": "0.1.18",
+ "version": "0.1.19",
"license": "MIT",
"dependencies": {
"@radix-ui/react-alert-dialog": "1.1.23",
@@ -23,7 +23,6 @@
"@xterm/addon-fit": "0.11.0",
"@xterm/addon-search": "0.16.0",
"@xterm/addon-web-links": "0.12.0",
- "@xterm/addon-webgl": "0.19.0",
"@xterm/xterm": "6.0.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
@@ -5359,12 +5358,6 @@
"integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==",
"license": "MIT"
},
- "node_modules/@xterm/addon-webgl": {
- "version": "0.19.0",
- "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz",
- "integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==",
- "license": "MIT"
- },
"node_modules/@xterm/xterm": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz",
diff --git a/package.json b/package.json
index d0c37d6..f23b1cc 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "drover",
"productName": "Drover",
- "version": "0.1.18",
+ "version": "0.1.19",
"description": "A native workspace for Herdr-powered agents, terminals, worktrees, and live sessions.",
"main": ".vite/build/main.js",
"private": true,
@@ -79,7 +79,6 @@
"@xterm/addon-fit": "0.11.0",
"@xterm/addon-search": "0.16.0",
"@xterm/addon-web-links": "0.12.0",
- "@xterm/addon-webgl": "0.19.0",
"@xterm/xterm": "6.0.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
diff --git a/src/renderer/App.test.tsx b/src/renderer/App.test.tsx
index ad6287c..701b73a 100644
--- a/src/renderer/App.test.tsx
+++ b/src/renderer/App.test.tsx
@@ -1121,6 +1121,30 @@ describe('App', () => {
expect(screen.getByTestId('terminal-w1:p1')).not.toBeVisible();
});
+ it('constrains the persistent terminal while switching pane views', async () => {
+ const user = userEvent.setup();
+ const terminalFirstSnapshot: SessionSnapshot = {
+ ...snapshot,
+ panes: snapshot.panes.map((pane) =>
+ pane.pane_id === 'w1:p1' ? { ...pane, agent_has_arguments: true } : pane,
+ ),
+ };
+ vi.mocked(window.herdr.bootstrap).mockResolvedValue({
+ ...connected,
+ snapshot: terminalFirstSnapshot,
+ });
+ render();
+
+ const terminal = await screen.findByTestId('terminal-w1:p1');
+ const surface = terminal.parentElement;
+ expect(surface).toHaveClass('flex', 'min-h-0', 'min-w-0', 'flex-1', 'overflow-hidden');
+ expect(surface).not.toHaveClass('hidden');
+
+ await user.click(screen.getByRole('button', { name: 'Chat view' }));
+ expect(terminal.parentElement).toBe(surface);
+ expect(surface).toHaveClass('hidden', 'min-h-0', 'min-w-0', 'flex-1', 'overflow-hidden');
+ });
+
it('opens complete pane controls and routes graphical actions to Herdr', async () => {
const user = userEvent.setup();
render();
@@ -1740,6 +1764,32 @@ describe('App', () => {
expect(screen.getByRole('heading', { name: 'drover' })).toBeInTheDocument();
});
+ it('keeps the canonical session when a background bootstrap transiently fails', async () => {
+ let sessionEvent:
+ | ((event: { event: string; data: Record }) => void)
+ | undefined;
+ window.herdr.onSessionEvent = vi.fn((listener) => {
+ sessionEvent = listener;
+ return () => undefined;
+ });
+ vi.mocked(window.herdr.bootstrap).mockResolvedValueOnce(connected).mockResolvedValue({
+ state: 'error',
+ message: 'Herdr could not be reached.',
+ details: 'transient socket error',
+ });
+ render();
+ await screen.findByRole('heading', { name: 'drover' });
+
+ act(() => sessionEvent?.({ event: 'layout.updated', data: {} }));
+ await waitFor(() => expect(window.herdr.bootstrap).toHaveBeenCalledTimes(2), {
+ timeout: 2_000,
+ });
+
+ expect(screen.getByRole('heading', { name: 'drover' })).toBeInTheDocument();
+ expect(screen.queryByText('Herdr could not be reached')).not.toBeInTheDocument();
+ expect(screen.getByText('Engine disconnected')).toBeInTheDocument();
+ });
+
it('coalesces background refreshes while session events stream', async () => {
let sessionEvent:
| ((event: { event: string; data: Record }) => void)
diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx
index 30e881d..eef5e9d 100644
--- a/src/renderer/App.tsx
+++ b/src/renderer/App.tsx
@@ -752,7 +752,11 @@ function PersistentTerminalSurface({ pane, visible }: { pane: PaneInfo; visible:
return null;
}
return (
-
+
void window.herdr.openExternal(url)}
onScrollRequest={(request) =>
@@ -1936,13 +1940,20 @@ function AppContent() {
>([]);
const streamStateRevision = useRef(0);
- const applyLatestResult = useCallback((sequence: number, next: EngineBootstrap) => {
- if (sequence !== resultRequestSequence.current) {
- return false;
- }
- setResult(next);
- return true;
- }, []);
+ const applyLatestResult = useCallback(
+ (sequence: number, next: EngineBootstrap, preserveConnected = false) => {
+ if (sequence !== resultRequestSequence.current) {
+ return false;
+ }
+ setResult((current) =>
+ preserveConnected && next.state !== 'connected' && current?.state === 'connected'
+ ? current
+ : next,
+ );
+ return true;
+ },
+ [],
+ );
const load = useCallback(
async (quiet = false) => {
@@ -1955,7 +1966,10 @@ function AppContent() {
}
try {
const next = await window.herdr.bootstrap();
- if (applyLatestResult(sequence, next) && streamStateRevision.current === streamRevision) {
+ if (
+ applyLatestResult(sequence, next, quiet) &&
+ streamStateRevision.current === streamRevision
+ ) {
setConnectionState(next.state === 'connected' ? 'connected' : 'disconnected');
}
} finally {
diff --git a/src/renderer/terminal/TerminalPanel.test.tsx b/src/renderer/terminal/TerminalPanel.test.tsx
index 5ab8221..9485529 100644
--- a/src/renderer/terminal/TerminalPanel.test.tsx
+++ b/src/renderer/terminal/TerminalPanel.test.tsx
@@ -40,20 +40,6 @@ const terminalControl = vi.hoisted(() => ({
const webLinks = vi.hoisted(() => ({
activate: undefined as ((event: MouseEvent, uri: string) => void) | undefined,
}));
-const webgl = vi.hoisted(() => ({
- contextLoss: undefined as (() => void) | undefined,
- dispose: vi.fn(),
-}));
-
-vi.mock('@xterm/addon-webgl', () => ({
- WebglAddon: class {
- dispose = webgl.dispose;
- onContextLoss(listener: () => void) {
- webgl.contextLoss = listener;
- return { dispose() {} };
- }
- },
-}));
vi.mock('@xterm/addon-fit', () => ({
FitAddon: class {
fit() {}
@@ -164,8 +150,6 @@ describe('TerminalPanel', () => {
terminalControl.write.mockReset();
resizeObserver.listener = undefined;
webLinks.activate = undefined;
- webgl.contextLoss = undefined;
- webgl.dispose.mockReset();
terminalControl.scrollToBottom.mockReset();
window.herdr = {
terminal: {
@@ -201,13 +185,20 @@ describe('TerminalPanel', () => {
});
});
- it('uses the WebGL renderer and falls back cleanly after context loss', () => {
+ it('uses xterm DOM rendering without a GPU context', () => {
render();
- expect(terminalControl.loadedAddons).toHaveLength(4);
- expect(webgl.contextLoss).toEqual(expect.any(Function));
- webgl.contextLoss?.();
- expect(webgl.dispose).toHaveBeenCalledOnce();
+ expect(terminalControl.loadedAddons).toHaveLength(3);
+ });
+
+ it('contains xterm intrinsic dimensions within the pane', () => {
+ render();
+
+ expect(screen.getByRole('region', { name: 'Terminal output w1:p2' }).parentElement).toHaveClass(
+ 'min-h-0',
+ 'min-w-0',
+ 'overflow-hidden',
+ );
});
it('enables xterm screen-reader DOM only when Electron accessibility is active', async () => {
diff --git a/src/renderer/terminal/TerminalPanel.tsx b/src/renderer/terminal/TerminalPanel.tsx
index e987103..b75a2f9 100644
--- a/src/renderer/terminal/TerminalPanel.tsx
+++ b/src/renderer/terminal/TerminalPanel.tsx
@@ -1,7 +1,6 @@
import { FitAddon } from '@xterm/addon-fit';
import { SearchAddon } from '@xterm/addon-search';
import { WebLinksAddon } from '@xterm/addon-web-links';
-import { WebglAddon } from '@xterm/addon-webgl';
import { Terminal } from '@xterm/xterm';
import {
ArrowDownToLine,
@@ -217,13 +216,6 @@ export function TerminalPanel({ pane, onOpenExternal, onScrollRequest }: Termina
})
.catch(() => undefined);
terminal.open(container);
- try {
- const webglAddon = new WebglAddon();
- webglAddon.onContextLoss(() => webglAddon.dispose());
- terminal.loadAddon(webglAddon);
- } catch {
- // xterm keeps its default renderer when WebGL2 is unavailable.
- }
terminal.attachCustomKeyEventHandler((event) => {
if (
event.type === 'keydown' &&
@@ -367,7 +359,7 @@ export function TerminalPanel({ pane, onOpenExternal, onScrollRequest }: Termina
}, [copySelection, pane.pane_id, pasteClipboard]);
return (
-