Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
fd4353e
feat(herdr): CLI discovery client and shared invocation resolver
islee23520 Aug 22, 2026
81bb180
test(terminals): pin one-PTY behavior ahead of transport seam
islee23520 Aug 22, 2026
9572c1d
docs(research): pin herdr 0.8.2 control-bridge protocol shapes
islee23520 Aug 22, 2026
69fcec7
refactor(terminals): TerminalTransport seam with shell-preserving att…
islee23520 Aug 22, 2026
941f100
feat(herdr): control-bridge transport with NDJSON codec and scroll-aw…
islee23520 Aug 22, 2026
dad80f8
feat(herdr): attach controller with atomic first-frame cutover and fa…
islee23520 Aug 22, 2026
c59f6e8
feat(webview): source-state badge and reset for attached sessions
islee23520 Aug 22, 2026
7c05fbb
feat(terminal): route one surface between shell and attached herdr so…
islee23520 Aug 22, 2026
1121674
feat(commands): herdr attach/detach commands with agent QuickPick and…
islee23520 Aug 22, 2026
178ffb4
docs(project): amend one-terminal contract for herdr attach
islee23520 Aug 22, 2026
7264412
fix(herdr): tolerate expected stdin EPIPE while the bridge child is c…
islee23520 Aug 22, 2026
3b23452
test(e2e): live herdr attach cycle and rendered visual evidence
islee23520 Aug 22, 2026
5fef17d
fix(docs): restore herdr README section and strip trailing whitespace
islee23520 Aug 22, 2026
a9930f2
feat(herdr): add read-only Spaces and Agents explorer
islee23520 Aug 23, 2026
9f70a9f
feat(herdr): route Space and Agent clicks like Switcher
islee23520 Aug 23, 2026
466ea0b
fix(herdr): opt-in explorer and tolerate missing agent cwd
islee23520 Aug 23, 2026
a9f6a3b
feat(terminal): hide the ULW sidebar label behind a setting
islee23520 Aug 23, 2026
7002611
fix(herdr): refresh Spaces/Agents when enabled at runtime
islee23520 Aug 23, 2026
0ba3cf2
fix(herdr): ship node-pty in the VSIX so activate succeeds
islee23520 Aug 23, 2026
7ca00d7
feat(herdr): open each agent in its own editor tab
islee23520 Aug 23, 2026
7beb44b
feat(herdr): reach remote Herdr servers over SSH socket forwarding
islee23520 Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions .vscode-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function resolveLocalVsCodeExecutable() {

if (process.platform === "darwin") {
const candidate =
"/Applications/Visual Studio Code.app/Contents/MacOS/Electron";
"/Applications/Visual Studio Code.app/Contents/MacOS/Code";
return fs.existsSync(candidate) ? candidate : undefined;
}

Expand Down Expand Up @@ -39,10 +39,14 @@ function resolveLocalVsCodeExecutable() {

const localVsCodeExecutable = resolveLocalVsCodeExecutable();

module.exports = defineConfig({
files: "out/test/e2e/**/*.e2e.js",
const packagedExtensionPath = process.env.ULW_E2E_EXTENSION_PATH;

const shared = {
version: "stable",
workspaceFolder: "src/test/e2e/fixtures/workspace",
...(packagedExtensionPath
? { extensionDevelopmentPath: packagedExtensionPath }
: {}),
...(localVsCodeExecutable
? {
useInstallation: {
Expand All @@ -54,4 +58,23 @@ module.exports = defineConfig({
ui: "tdd",
timeout: 20000,
},
});
};

const herdrRequested = process.argv.some(
(argument, index, argv) =>
argument === "--label=herdr" ||
(argument === "--label" && argv[index + 1] === "herdr"),
);

module.exports = defineConfig(
herdrRequested
? {
...shared,
label: "herdr",
files: "out/test/e2e/suite/herdr-attach.e2e.js",
}
: {
...shared,
files: "out/test/e2e/suite/activation.e2e.js",
},
);
49 changes: 32 additions & 17 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,28 @@

## OVERVIEW

VS Code extension that runs one native shell terminal in the secondary sidebar or an editor-group tab. The extension host owns one `node-pty` process; each surface owns one xterm.js instance, with one active surface at a time.
VS Code extension that runs one native shell terminal in the secondary sidebar or an editor-group tab. With Herdr off, the host owns one persistent `node-pty` shell PTY on one active xterm surface. With Herdr on, the sidebar terminal is hidden and each attached agent gets its own editor-group webview plus one control-bridge child.

## SOURCE TOPOLOGY

```text
src/
├── extension.ts # activate/deactivate entry
├── types.ts # seven-message host/webview contract
├── types.ts # host/webview contract
├── core/ExtensionLifecycle.ts # creates and registers the terminal provider
├── providers/TerminalProvider.ts # sidebar webview and PTY message bridge
├── terminals/TerminalManager.ts # one native shell PTY lifecycle
├── terminals/
│ ├── TerminalManager.ts # one native shell PTY lifecycle
│ ├── TerminalTransport.ts # transport seam for shell and Herdr bridge
│ └── LocalShellTransport.ts # local shell transport adapter
├── herdr/
│ ├── HerdrCliClient.ts # CLI discovery, agent listing, workspace listing
│ ├── HerdrInvocationResolver.ts # shared Herdr command/env resolver
│ ├── HerdrControlTransport.ts # official Herdr control bridge child
│ ├── HerdrAttachController.ts # attach/detach lifecycle state machine
│ ├── HerdrExplorer.ts # Activity Bar Spaces/Agents trees
│ ├── types.ts # Herdr data types
│ └── errors.ts # Herdr typed errors
├── webview/
│ ├── main.ts # one xterm bootstrap
│ ├── terminal/index.ts # xterm input/output/resize/config bridge
Expand All @@ -25,30 +36,34 @@ src/
## RUNTIME FLOW

```text
sidebar: contributed view `ulw` -> resolveWebviewView()
editor: ulw.defaultLocation=editor (default) | ulw.toggleEditorLocation -> createWebviewPanel
-> active surface posts `ready`
-> TerminalManager creates or resizes `sidebar-shell`
-> scrollback replay when switching to a fresh xterm
-> node-pty data/exit events post to surfaces
-> active surface input/resize events write/resize the PTY
Herdr off:
sidebar: contributed view `ulw` (when `ulw.sidebar.enabled`) -> resolveWebviewView()
editor: ulw.defaultLocation=editor (default) | ulw.toggleEditorLocation -> one shared webview panel
-> TerminalManager creates or resizes `sidebar-shell`
Herdr on:
sidebar terminal hidden (`when: config.ulw.sidebar.enabled && !config.ulw.herdr.enabled`)
Activity Bar Spaces/Agents -> agent click in this window opens/reveals an editor-group tab per agent
-> one control-bridge child per attached agent -> first-full-frame atomic cutover
-> detach/external closure closes that session without restoring a local shell
```

## CONTRACT

- Webview to host: `ready`, `input`, `resize`, `copy`, `imagePasted`.
- Host to webview: `output`, `exit`, `config`, `focus`, `clipboardImage`.
- No pane or session identifiers: exactly one terminal process exists.
- One active surface at a time: secondary-sidebar webview or one editor-group webview panel.
- `ulw.toggleEditorLocation` moves that single shell between surfaces.
- Host to webview: `output`, `exit`, `config`, `focus`, `clipboardImage`, `reset`, `sourceState`.
- Herdr off: one persistent shell PTY; one active surface (sidebar or one editor panel).
- Herdr on: no sidebar terminal; one editor-group tab and one Herdr bridge child per attached agent.
- Input and resize target the currently ACTIVE surface only.
- `ulw.toggleEditorLocation` moves the shared shell between surfaces only while Herdr is off.

## CONVENTIONS

- Activate for the sidebar view, contributed commands, and startup (so `ulw.defaultLocation=editor` can open an editor tab).
- Keep contributed commands limited to location toggle and send-to-terminal helpers; no keybindings.
- Keep contributed commands limited to location toggle, send-to-terminal helpers, Herdr attach/detach, and the read-only Spaces/Agents explorer; no keybindings.
- Keep `node-pty` as the only runtime dependency. xterm and the fit addon are build-time dependencies bundled into `webview.js`.
- Do not add multiplexer, session, AI, HTTP, dashboard, file-context, or multi-pane features.
- One editor panel max for the shared shell; never spawn a second PTY for editor mode.
- Herdr attach is allowed only through official CLI bridge children using builtin `child_process`; no raw socket client, no agent start/rename, no auto-start/reconnect/reattach. Herdr commands and the Activity Bar Spaces/Agents tree stay hidden until `ulw.herdr.enabled` is true. Then the tree lists live workspaces, opens each clicked agent in this window as its own editor-group tab, and opens another Space's folder in a new VS Code window.
- With Herdr off: one editor panel max for the shared shell; never spawn a second PTY for editor mode.
- With Herdr on: hide the ULW sidebar terminal; open each agent in its own editor-group tab; do not restore a local shell on detach.
- Honor `ulw.defaultLocation` (`editor` default | `sidebar`); toggle always overrides the current surface.
- Use project scripts for verification.

Expand Down
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,60 @@ All notable changes to the "ULW" extension will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.12.16] - 2026-09-07

### Added

- Herdr over SSH: the new `ulw.herdr.remoteTarget` setting reaches the Herdr server behind an SSH target while VS Code is connected to a remote window. ULW forwards the remote Herdr API and client sockets over SSH and routes Spaces/Agents listing and attach through them. Blank targets and local windows keep the previous invocation, Herdr setting changes apply to new invocations without reloading the window, and a configured `ulw.herdr.session` is ignored while forwarding.

### Fixed

- Switching between Herdr agent editor tabs now moves global input, resize, and detach to the focused agent's tab, and closing the focused tab falls back to the most recently focused remaining agent.

## [1.12.15] - 2026-08-24

### Fixed

- Herdr attach scroll: wheel/PageUp/PageDown now send Herdr `terminal.scroll` (host history) instead of CSI arrows as `terminal.input`. Arrows were reaching the agent prompt/input widget and scrolling that field, not the transcript. Follow each scroll with same-size `terminal.resize` so Herdr emits a scrolled `full:true` checkpoint frame.

## [1.12.14] - 2026-08-24

### Fixed

- Herdr attach scroll: capture wheel on `window` and post repeated CSI arrows as `terminal.input`. xterm was eating wheel as local scroll (viewport checkpoint has no history) or dropping it when render dimensions were missing (`consumeWheelEvent` returned 0). Also block xterm's local wheel handler via `customWheelEventHandler` while attached.

## [1.12.13] - 2026-08-24

### Fixed

- Stop capturing wheel/click in the webview. xterm already converts wheel to CSI arrows when scrollback is 0; intercepting the event blocked that path. Hide the xterm viewport overflow so an empty local buffer cannot swallow the gesture.

## [1.12.12] - 2026-08-24

### Fixed

- Wheel an attached agent TUI with CSI arrows when the app has no mouse tracking (typical pi/omo frames omit DECSET 1000/1006). Send SGR mouse only when xterm reports a mouse protocol, so clicks still work in mouse-aware apps.

## [1.12.11] - 2026-08-24

### Fixed

- Send Herdr-attached wheel and clicks as SGR mouse (`ESC[<64;col;rowM`, `ESC[<0;col;rowM/m`) instead of CSI arrows, so the agent TUI gets mouse input rather than keyboard scroll.

## [1.12.10] - 2026-08-24

### Fixed

- Mouse-wheel a Herdr-attached agent TUI by sending CSI arrows as PTY input. Host `terminal.scroll` only moves Herdr history and does not paint alt-screen apps; a follow-up same-size resize snapped the live viewport back.
- Capture wheel on the webview `window` and disable xterm scrollback while attached so the local empty buffer cannot swallow the gesture.

## [1.12.9] - 2026-08-24

### Fixed

- Scroll a Herdr-attached terminal by intercepting wheel/Page keys and forcing a checkpoint after `terminal.scroll`, which otherwise moves host history without painting a new frame.
- Keep the Spaces and Agents trees current by polling Herdr lists while Herdr mode is enabled.

## [1.12.8] - 2026-08-06

### Fixed
Expand Down
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# ULW Sidebar Terminal

ULW is a small VS Code extension that runs one native shell terminal in the secondary sidebar.
ULW is a small VS Code extension that runs one native shell terminal in the secondary sidebar or an editor-group tab.

It intentionally has no terminal multiplexer, session manager, AI integration, HTTP service, dashboard, or multi-pane layout. Opening ULW creates one `node-pty` process and connects it to one xterm.js terminal in either the secondary sidebar or an editor-group tab.
With Herdr integration off, opening ULW creates one `node-pty` process connected to one xterm.js surface. With `ulw.herdr.enabled`, the ULW sidebar terminal is hidden; Spaces/Agents live in the Activity Bar, and each agent opens in its own editor-group tab.

## Use

Expand All @@ -14,6 +14,16 @@ The shell starts in the first workspace folder. When no workspace is open, it st

Run **ULW: Toggle Terminal Location** (`ulw.toggleEditorLocation`) to move the same shell between the secondary sidebar and an editor-group tab. Toggle again, or close the editor tab, to return to the sidebar. Switching surfaces reuses the same shell and replays recent scrollback into the newly focused xterm.

## Attach to a running Herdr agent

Herdr integration is off until you set `ulw.herdr.enabled` (Settings: **ULW › Herdr: Enabled**). After that, use **ULW: Attach Herdr Session** (`ulw.attachHerdrSession`) to open a QuickPick of live Herdr agents, then choose the session to take over. The Activity Bar **Herdr** view lists the same live **Spaces** (`ulw.herdr.spaces`) and **Agents** (`ulw.herdr.agents`). Clicking an agent (`ulw.herdr.openAgent`) attaches the existing terminal when that agent's folder is this VS Code window, otherwise it opens the folder in a new window. Clicking a space (`ulw.herdr.openSpace`) uses the same folder check and never starts an agent. Refresh with `ulw.herdr.refreshExplorer`.

- The picker and trees are populated from the Herdr CLI `agent list` / `workspace list` output, and ULW warns when takeover will replace other direct Herdr clients.
- Taking control is not auto-restored to those other clients; ULW owns the session only while attached.
- Any attach failure or external closure restores the local shell automatically.

Use **ULW: Detach Herdr Session** (`ulw.detachHerdrSession`) to release ULW's controller and restore the local shell. A previously displaced direct Herdr client is not automatically restored.

The terminal automatically inherits the active VS Code terminal palette, including ANSI colors, cursor colors, selections, and live theme changes. Drag-selecting terminal text copies the finished selection to the system clipboard.

## Commands
Expand All @@ -23,19 +33,29 @@ The terminal automatically inherits the active VS Code terminal palette, includi
| `ulw.toggleEditorLocation` | Toggle the terminal between secondary sidebar and editor group |
| `ulw.sendSelectionToTerminal` | Send the active editor selection to the terminal |
| `ulw.sendFileToTerminal` | Send an explorer file path to the terminal |
| `ulw.attachHerdrSession` | Attach to a running Herdr agent |
| `ulw.detachHerdrSession` | Detach from a running Herdr agent |
| `ulw.herdr.openAgent` | Attach the selected Activity Bar agent |
| `ulw.herdr.openSpace` | Open that Space's folder in this window or a new window |
| `ulw.herdr.refreshExplorer` | Refresh Spaces and Agents lists |

## Settings

| Setting | Default | Purpose |
| --- | --- | --- |
| `ulw.defaultLocation` | `editor` | Open in an editor-group tab or the secondary sidebar |
| `ulw.sidebar.enabled` | `true` | Show the ULW label in the secondary sidebar. Off hides ULW from the sidebar completely |
| `ulw.fontSize` | `14` | Terminal font size |
| `ulw.fontFamily` | Nerd Font and monospace fallbacks | Terminal font family |
| `ulw.cursorBlink` | `true` | Blink the cursor |
| `ulw.cursorStyle` | `block` | `block`, `underline`, or `bar` |
| `ulw.scrollback` | `10000` | Scrollback line count |
| `ulw.shellPath` | empty | Shell executable; empty uses the VS Code or system default |
| `ulw.shellArgs` | `[]` | Arguments passed to the shell |
| `ulw.herdr.enabled` | `false` | Turn on Herdr Spaces/Agents and attach. Off until you enable it |
| `ulw.herdr.executablePath` | `herdr` | Herdr executable path; GUI-launched VS Code may need an explicit absolute path if PATH does not include herdr |
| `ulw.herdr.socketPath` | empty | Optional Herdr socket path; ignored when a named session is configured |
| `ulw.herdr.session` | empty | Optional named Herdr session; takes precedence over the socket path |

## Development

Expand Down
Loading
Loading