2026-04-29 02:44:37 +00:00
|
|
|
/* ─── Status bar ─────────────────────────────────────────────────────── */
|
|
|
|
|
|
feat: initial v0.1 scaffold
SSH/SFTP sync engine for Obsidian — SFTP Sync モデルで remote vault を
ローカルにミラーし upload-on-save で変更を反映。Sponsorware 向け JWT
ライセンスゲート付き。
- ssh/: ConnectionPool, SftpSession (fastGet/fastPut/atomic rename),
HostKeyStore (TOFU), AuthResolver (password/key/agent)
- sync/: SyncEngine (state machine), FileIndex, DiffCalculator,
TransferQueue (priority/dedup/concurrency=3), IgnoreFilter,
WatcherBridge (vault events → upload)
- ui/: StatusBar, ConflictModal, ConnectModal, SyncLogView
- license/: LicenseGate + LicenseValidator (JWT HS256, Node crypto only)
- settings/: SettingsTab, ProfileForm
- build: esbuild + native .node plugin (ssh2 pure-JS fallback)
2026-04-24 21:35:30 +00:00
|
|
|
.remote-ssh-status {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-status.is-connected { color: var(--color-green); }
|
|
|
|
|
.remote-ssh-status.is-syncing { color: var(--color-yellow); }
|
|
|
|
|
.remote-ssh-status.is-error { color: var(--color-red); }
|
|
|
|
|
|
2026-04-29 02:44:37 +00:00
|
|
|
/* ─── Pending edits status-bar item ──────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-edits {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-edits.is-hidden {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 06:40:00 +00:00
|
|
|
/* ─── Large transfer indicator (#127) ────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-transfer {
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-transfer.is-hidden {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 02:44:37 +00:00
|
|
|
/* ─── Connect modal ──────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-connect-secret {
|
|
|
|
|
width: 100%;
|
|
|
|
|
margin: 8px 0 16px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-connect-back {
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-connect-button {
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ─── Conflict modal (legacy 2-choice) ───────────────────────────────── */
|
|
|
|
|
|
feat: initial v0.1 scaffold
SSH/SFTP sync engine for Obsidian — SFTP Sync モデルで remote vault を
ローカルにミラーし upload-on-save で変更を反映。Sponsorware 向け JWT
ライセンスゲート付き。
- ssh/: ConnectionPool, SftpSession (fastGet/fastPut/atomic rename),
HostKeyStore (TOFU), AuthResolver (password/key/agent)
- sync/: SyncEngine (state machine), FileIndex, DiffCalculator,
TransferQueue (priority/dedup/concurrency=3), IgnoreFilter,
WatcherBridge (vault events → upload)
- ui/: StatusBar, ConflictModal, ConnectModal, SyncLogView
- license/: LicenseGate + LicenseValidator (JWT HS256, Node crypto only)
- settings/: SettingsTab, ProfileForm
- build: esbuild + native .node plugin (ssh2 pure-JS fallback)
2026-04-24 21:35:30 +00:00
|
|
|
.conflict-list { max-height: 400px; overflow-y: auto; }
|
|
|
|
|
.conflict-row {
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
border-bottom: 1px solid var(--background-modifier-border);
|
|
|
|
|
}
|
|
|
|
|
.conflict-path { font-weight: 600; font-family: var(--font-monospace); display: block; margin-bottom: 4px; }
|
|
|
|
|
.conflict-btn-row { display: flex; gap: 8px; margin-top: 6px; }
|
|
|
|
|
.conflict-bulk-row { display: flex; gap: 8px; margin-bottom: 12px; }
|
|
|
|
|
.conflict-footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
|
|
|
|
|
2026-04-29 02:44:37 +00:00
|
|
|
/* ─── Sync log view ──────────────────────────────────────────────────── */
|
|
|
|
|
|
feat: initial v0.1 scaffold
SSH/SFTP sync engine for Obsidian — SFTP Sync モデルで remote vault を
ローカルにミラーし upload-on-save で変更を反映。Sponsorware 向け JWT
ライセンスゲート付き。
- ssh/: ConnectionPool, SftpSession (fastGet/fastPut/atomic rename),
HostKeyStore (TOFU), AuthResolver (password/key/agent)
- sync/: SyncEngine (state machine), FileIndex, DiffCalculator,
TransferQueue (priority/dedup/concurrency=3), IgnoreFilter,
WatcherBridge (vault events → upload)
- ui/: StatusBar, ConflictModal, ConnectModal, SyncLogView
- license/: LicenseGate + LicenseValidator (JWT HS256, Node crypto only)
- settings/: SettingsTab, ProfileForm
- build: esbuild + native .node plugin (ssh2 pure-JS fallback)
2026-04-24 21:35:30 +00:00
|
|
|
.sync-log-container {
|
|
|
|
|
font-family: var(--font-monospace);
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
}
|
|
|
|
|
.sync-log-line { padding: 1px 0; }
|
|
|
|
|
.sync-log-line.level-error { color: var(--color-red); }
|
|
|
|
|
.sync-log-line.level-warn { color: var(--color-yellow); }
|
|
|
|
|
.sync-log-line.level-debug { color: var(--text-muted); }
|
2026-04-29 02:44:37 +00:00
|
|
|
|
|
|
|
|
/* ─── Pending edits modal ────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-edits-list {
|
|
|
|
|
max-height: 40vh;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
margin-bottom: 1em;
|
|
|
|
|
padding: 0.5em;
|
|
|
|
|
border: 1px solid var(--background-modifier-border);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-family: var(--font-monospace);
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-edits-row {
|
|
|
|
|
padding: 0.15em 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Shared button row (pending edits + pending plugins modals). */
|
|
|
|
|
.remote-ssh-pending-buttons {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 0.5em;
|
|
|
|
|
margin-top: 1em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ─── Pending plugins modal ──────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-plugins {
|
|
|
|
|
max-height: 40vh;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
margin-bottom: 1em;
|
|
|
|
|
padding: 0.5em;
|
|
|
|
|
border: 1px solid var(--background-modifier-border);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-plugin-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 0.25em 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-plugin-checkbox {
|
|
|
|
|
margin-right: 0.5em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-plugin-label {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-pending-plugin-tag {
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
margin-left: 0.5em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ─── Three-way merge modal ──────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-panes {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
|
|
|
gap: 0.5em;
|
|
|
|
|
margin-bottom: 1em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-edit {
|
|
|
|
|
margin-bottom: 1em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-textarea {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 8em;
|
|
|
|
|
font-family: var(--font-monospace);
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-buttons {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
gap: 0.5em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-pane {
|
|
|
|
|
border: 1px solid var(--background-modifier-border);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
padding: 0.5em;
|
|
|
|
|
min-height: 12em;
|
|
|
|
|
max-height: 24em;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-pane-header {
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-bottom: 0.5em;
|
|
|
|
|
font-size: 0.85em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-pane-body {
|
|
|
|
|
margin: 0;
|
|
|
|
|
font-family: var(--font-monospace);
|
|
|
|
|
font-size: 0.8em;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-pane-empty {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-style: italic;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Diff line tints. RGBA chosen for ~18% alpha so the highlight
|
|
|
|
|
reads on both light + dark themes without clashing with body text. */
|
|
|
|
|
.remote-ssh-merge-line.is-add {
|
|
|
|
|
background-color: rgba(34, 197, 94, 0.18);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-merge-line.is-del {
|
|
|
|
|
background-color: rgba(239, 68, 68, 0.18);
|
|
|
|
|
text-decoration: line-through;
|
|
|
|
|
}
|
2026-05-01 06:22:52 +00:00
|
|
|
|
|
|
|
|
/* ─── Daemon log panel (settings tab) ───────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-log-pre {
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
overflow: auto;
|
|
|
|
|
font-family: var(--font-monospace);
|
|
|
|
|
font-size: 0.75em;
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
padding: 0.5em;
|
|
|
|
|
border: 1px solid var(--background-modifier-border);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ─── Remote path browser modal ─────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
.remote-ssh-breadcrumb {
|
|
|
|
|
margin-bottom: 0.5em;
|
|
|
|
|
font-family: var(--font-monospace);
|
|
|
|
|
font-size: 0.9em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-breadcrumb-segment {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
color: var(--text-accent);
|
|
|
|
|
}
|
|
|
|
|
.remote-ssh-breadcrumb-segment:hover {
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-dir-list {
|
|
|
|
|
max-height: 300px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
margin-bottom: 1em;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-dir-item {
|
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
.remote-ssh-dir-item:hover {
|
|
|
|
|
background: var(--background-modifier-hover);
|
|
|
|
|
}
|
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.
## Files
- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
Stdout + stderr both surface as a single `onData(chunk)`. Two
synchronous-flag guards keep `open()` race-free:
`opening` blocks concurrent open() calls before they reach the
await; the post-await `if (this.closed) ch.end()` check prevents a
channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
graceful "Not connected" / "Failed to open shell" disconnected
states. Reads font/scrollback/shell from plugin settings each `onOpen`.
The `onClose` callback logs `reason`/`cause` unconditionally so the
channel-close audit trail isn't lost when the View has torn down.
The `shell.open()` catch block disposes + nulls shell/term/fit so
late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
`addCommand("Open remote terminal")` (checkCallback gates on
client.isAlive). `disconnect()` calls
`app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
shell channel close fires while ssh2.Client is still around.
`openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
`openingTerminal` re-entrant flag prevents two leaves being created
by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
`terminalShell?: string`, `terminalFontSize?: number` (default 12),
`terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
bundle cap raised 600 -> 800 KB to absorb the bundled xterm
(770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
support, so single-bundle is the only option. Comment in workflows
explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
close-while-pending end()s the channel + bypasses listener-attach,
stdout+stderr routing, write/resize/close, lifecycle invariants
(no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
exclude (xterm.js + ResizeObserver under jsdom is impractical to
unit-test; covered by manual smoke against a real Obsidian window,
consistent with the other src/ui/*Modal/Bar exclusions).
## Verified
- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
+ no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)
## Out of scope (per #149 v1)
- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:17:11 +00:00
|
|
|
|
|
|
|
|
/* ─── #149 Remote terminal pane ──────────────────────────────────────
|
|
|
|
|
*
|
|
|
|
|
* Hosts an xterm.js Terminal inside an Obsidian ItemView leaf. The
|
|
|
|
|
* `.remote-ssh-terminal-host` wraps the leaf's content area; the
|
|
|
|
|
* inner `.remote-ssh-terminal-pane` is the element xterm.js attaches
|
|
|
|
|
* to and FitAddon measures. We also inline xterm.js's own default
|
|
|
|
|
* stylesheet below (3 KB, MIT-licensed by The xterm.js authors) so
|
|
|
|
|
* users don't need a separate CSS asset in their plugin folder.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
.remote-ssh-terminal-host {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
height: 100%;
|
|
|
|
|
padding: 0;
|
|
|
|
|
background: var(--background-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-terminal-pane {
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
min-height: 0;
|
|
|
|
|
width: 100%;
|
|
|
|
|
/* Solid background ensures xterm's transparent canvas paints over a
|
|
|
|
|
* predictable colour in both Obsidian light + dark themes. */
|
2026-06-08 08:17:41 +00:00
|
|
|
background: #000000;
|
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.
## Files
- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
Stdout + stderr both surface as a single `onData(chunk)`. Two
synchronous-flag guards keep `open()` race-free:
`opening` blocks concurrent open() calls before they reach the
await; the post-await `if (this.closed) ch.end()` check prevents a
channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
graceful "Not connected" / "Failed to open shell" disconnected
states. Reads font/scrollback/shell from plugin settings each `onOpen`.
The `onClose` callback logs `reason`/`cause` unconditionally so the
channel-close audit trail isn't lost when the View has torn down.
The `shell.open()` catch block disposes + nulls shell/term/fit so
late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
`addCommand("Open remote terminal")` (checkCallback gates on
client.isAlive). `disconnect()` calls
`app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
shell channel close fires while ssh2.Client is still around.
`openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
`openingTerminal` re-entrant flag prevents two leaves being created
by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
`terminalShell?: string`, `terminalFontSize?: number` (default 12),
`terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
bundle cap raised 600 -> 800 KB to absorb the bundled xterm
(770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
support, so single-bundle is the only option. Comment in workflows
explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
close-while-pending end()s the channel + bypasses listener-attach,
stdout+stderr routing, write/resize/close, lifecycle invariants
(no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
exclude (xterm.js + ResizeObserver under jsdom is impractical to
unit-test; covered by manual smoke against a real Obsidian window,
consistent with the other src/ui/*Modal/Bar exclusions).
## Verified
- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
+ no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)
## Out of scope (per #149 v1)
- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:17:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.remote-ssh-terminal-disconnected {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* xterm.js v5 default stylesheet — inlined to avoid a separate
|
|
|
|
|
* @xterm/xterm/css/xterm.css asset. License notice preserved.
|
|
|
|
|
*
|
|
|
|
|
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
|
|
|
|
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
|
|
|
|
* https://github.com/chjj/term.js
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
.xterm {
|
|
|
|
|
cursor: text;
|
|
|
|
|
position: relative;
|
|
|
|
|
user-select: none;
|
|
|
|
|
-ms-user-select: none;
|
|
|
|
|
-webkit-user-select: none;
|
|
|
|
|
}
|
|
|
|
|
.xterm.focus,
|
|
|
|
|
.xterm:focus { outline: none; }
|
|
|
|
|
|
|
|
|
|
.xterm .xterm-helpers {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
z-index: 5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.xterm .xterm-helper-textarea {
|
|
|
|
|
padding: 0;
|
|
|
|
|
border: 0;
|
|
|
|
|
margin: 0;
|
|
|
|
|
position: absolute;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
left: -9999em;
|
|
|
|
|
top: 0;
|
|
|
|
|
width: 0;
|
|
|
|
|
height: 0;
|
|
|
|
|
z-index: -5;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
resize: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.xterm .composition-view {
|
2026-06-08 08:17:41 +00:00
|
|
|
background: #000000;
|
|
|
|
|
color: #FFFFFF;
|
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.
## Files
- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
Stdout + stderr both surface as a single `onData(chunk)`. Two
synchronous-flag guards keep `open()` race-free:
`opening` blocks concurrent open() calls before they reach the
await; the post-await `if (this.closed) ch.end()` check prevents a
channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
graceful "Not connected" / "Failed to open shell" disconnected
states. Reads font/scrollback/shell from plugin settings each `onOpen`.
The `onClose` callback logs `reason`/`cause` unconditionally so the
channel-close audit trail isn't lost when the View has torn down.
The `shell.open()` catch block disposes + nulls shell/term/fit so
late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
`addCommand("Open remote terminal")` (checkCallback gates on
client.isAlive). `disconnect()` calls
`app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
shell channel close fires while ssh2.Client is still around.
`openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
`openingTerminal` re-entrant flag prevents two leaves being created
by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
`terminalShell?: string`, `terminalFontSize?: number` (default 12),
`terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
bundle cap raised 600 -> 800 KB to absorb the bundled xterm
(770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
support, so single-bundle is the only option. Comment in workflows
explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
close-while-pending end()s the channel + bypasses listener-attach,
stdout+stderr routing, write/resize/close, lifecycle invariants
(no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
exclude (xterm.js + ResizeObserver under jsdom is impractical to
unit-test; covered by manual smoke against a real Obsidian window,
consistent with the other src/ui/*Modal/Bar exclusions).
## Verified
- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
+ no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)
## Out of scope (per #149 v1)
- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:17:11 +00:00
|
|
|
display: none;
|
|
|
|
|
position: absolute;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
z-index: 1;
|
|
|
|
|
}
|
|
|
|
|
.xterm .composition-view.active { display: block; }
|
|
|
|
|
|
|
|
|
|
.xterm .xterm-viewport {
|
2026-06-08 08:17:41 +00:00
|
|
|
background-color: #000000;
|
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.
## Files
- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
Stdout + stderr both surface as a single `onData(chunk)`. Two
synchronous-flag guards keep `open()` race-free:
`opening` blocks concurrent open() calls before they reach the
await; the post-await `if (this.closed) ch.end()` check prevents a
channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
graceful "Not connected" / "Failed to open shell" disconnected
states. Reads font/scrollback/shell from plugin settings each `onOpen`.
The `onClose` callback logs `reason`/`cause` unconditionally so the
channel-close audit trail isn't lost when the View has torn down.
The `shell.open()` catch block disposes + nulls shell/term/fit so
late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
`addCommand("Open remote terminal")` (checkCallback gates on
client.isAlive). `disconnect()` calls
`app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
shell channel close fires while ssh2.Client is still around.
`openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
`openingTerminal` re-entrant flag prevents two leaves being created
by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
`terminalShell?: string`, `terminalFontSize?: number` (default 12),
`terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
bundle cap raised 600 -> 800 KB to absorb the bundled xterm
(770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
support, so single-bundle is the only option. Comment in workflows
explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
close-while-pending end()s the channel + bypasses listener-attach,
stdout+stderr routing, write/resize/close, lifecycle invariants
(no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
exclude (xterm.js + ResizeObserver under jsdom is impractical to
unit-test; covered by manual smoke against a real Obsidian window,
consistent with the other src/ui/*Modal/Bar exclusions).
## Verified
- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
+ no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)
## Out of scope (per #149 v1)
- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:17:11 +00:00
|
|
|
overflow-y: scroll;
|
|
|
|
|
cursor: default;
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: 0;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.xterm .xterm-screen { position: relative; }
|
|
|
|
|
.xterm .xterm-screen canvas { position: absolute; left: 0; top: 0; }
|
|
|
|
|
.xterm .xterm-scroll-area { visibility: hidden; }
|
|
|
|
|
|
|
|
|
|
.xterm-char-measure-element {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
visibility: hidden;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
left: -9999em;
|
|
|
|
|
line-height: normal;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.xterm.enable-mouse-events { cursor: default; }
|
|
|
|
|
.xterm.xterm-cursor-pointer,
|
|
|
|
|
.xterm .xterm-cursor-pointer { cursor: pointer; }
|
|
|
|
|
.xterm.column-select.focus { cursor: crosshair; }
|
|
|
|
|
|
|
|
|
|
.xterm .xterm-accessibility:not(.debug),
|
|
|
|
|
.xterm .xterm-message {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: 0;
|
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
color: transparent;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.xterm .xterm-accessibility-tree:not(.debug) *::selection { color: transparent; }
|
|
|
|
|
.xterm .xterm-accessibility-tree { user-select: text; white-space: pre; }
|
|
|
|
|
|
|
|
|
|
.xterm .live-region {
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -9999px;
|
|
|
|
|
width: 1px;
|
|
|
|
|
height: 1px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-08 14:06:02 +00:00
|
|
|
.xterm-dim.xterm-dim { opacity: 1; }
|
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.
## Files
- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
Stdout + stderr both surface as a single `onData(chunk)`. Two
synchronous-flag guards keep `open()` race-free:
`opening` blocks concurrent open() calls before they reach the
await; the post-await `if (this.closed) ch.end()` check prevents a
channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
graceful "Not connected" / "Failed to open shell" disconnected
states. Reads font/scrollback/shell from plugin settings each `onOpen`.
The `onClose` callback logs `reason`/`cause` unconditionally so the
channel-close audit trail isn't lost when the View has torn down.
The `shell.open()` catch block disposes + nulls shell/term/fit so
late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
`addCommand("Open remote terminal")` (checkCallback gates on
client.isAlive). `disconnect()` calls
`app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
shell channel close fires while ssh2.Client is still around.
`openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
`openingTerminal` re-entrant flag prevents two leaves being created
by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
`terminalShell?: string`, `terminalFontSize?: number` (default 12),
`terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
bundle cap raised 600 -> 800 KB to absorb the bundled xterm
(770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
support, so single-bundle is the only option. Comment in workflows
explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
close-while-pending end()s the channel + bypasses listener-attach,
stdout+stderr routing, write/resize/close, lifecycle invariants
(no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
exclude (xterm.js + ResizeObserver under jsdom is impractical to
unit-test; covered by manual smoke against a real Obsidian window,
consistent with the other src/ui/*Modal/Bar exclusions).
## Verified
- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
+ no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)
## Out of scope (per #149 v1)
- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:17:11 +00:00
|
|
|
|
2026-06-09 06:28:18 +00:00
|
|
|
/* Obsidian's bundled Chromium only partially supports text-decoration
|
|
|
|
|
STYLE (double/wavy/dotted/dashed) and MULTIPLE line values
|
|
|
|
|
(e.g. `overline underline`), which the community-plugin validator flags.
|
|
|
|
|
We collapse xterm's fancy underline styles and the simultaneous
|
|
|
|
|
overline+underline to a plain underline — the only fully-supported
|
|
|
|
|
text-decoration form. Single overline / underline / line-through are
|
|
|
|
|
kept as-is (those are fully supported). */
|
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.
## Files
- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
Stdout + stderr both surface as a single `onData(chunk)`. Two
synchronous-flag guards keep `open()` race-free:
`opening` blocks concurrent open() calls before they reach the
await; the post-await `if (this.closed) ch.end()` check prevents a
channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
graceful "Not connected" / "Failed to open shell" disconnected
states. Reads font/scrollback/shell from plugin settings each `onOpen`.
The `onClose` callback logs `reason`/`cause` unconditionally so the
channel-close audit trail isn't lost when the View has torn down.
The `shell.open()` catch block disposes + nulls shell/term/fit so
late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
`addCommand("Open remote terminal")` (checkCallback gates on
client.isAlive). `disconnect()` calls
`app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
shell channel close fires while ssh2.Client is still around.
`openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
`openingTerminal` re-entrant flag prevents two leaves being created
by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
`terminalShell?: string`, `terminalFontSize?: number` (default 12),
`terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
bundle cap raised 600 -> 800 KB to absorb the bundled xterm
(770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
support, so single-bundle is the only option. Comment in workflows
explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
close-while-pending end()s the channel + bypasses listener-attach,
stdout+stderr routing, write/resize/close, lifecycle invariants
(no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
exclude (xterm.js + ResizeObserver under jsdom is impractical to
unit-test; covered by manual smoke against a real Obsidian window,
consistent with the other src/ui/*Modal/Bar exclusions).
## Verified
- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
+ no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)
## Out of scope (per #149 v1)
- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:17:11 +00:00
|
|
|
.xterm-underline-1 { text-decoration: underline; }
|
2026-06-09 06:28:18 +00:00
|
|
|
.xterm-underline-2 { text-decoration: underline; }
|
|
|
|
|
.xterm-underline-3 { text-decoration: underline; }
|
|
|
|
|
.xterm-underline-4 { text-decoration: underline; }
|
|
|
|
|
.xterm-underline-5 { text-decoration: underline; }
|
2026-06-09 06:04:58 +00:00
|
|
|
|
|
|
|
|
.xterm-overline { text-decoration: overline; }
|
2026-06-09 06:28:18 +00:00
|
|
|
.xterm-overline.xterm-underline-1 { text-decoration: underline; }
|
|
|
|
|
.xterm-overline.xterm-underline-2 { text-decoration: underline; }
|
|
|
|
|
.xterm-overline.xterm-underline-3 { text-decoration: underline; }
|
|
|
|
|
.xterm-overline.xterm-underline-4 { text-decoration: underline; }
|
|
|
|
|
.xterm-overline.xterm-underline-5 { text-decoration: underline; }
|
2026-06-09 06:04:58 +00:00
|
|
|
|
|
|
|
|
.xterm-strikethrough { text-decoration: line-through; }
|
feat: integrated remote terminal pane (xterm.js + ssh2 shell channel) (#149) (#234)
Closes #149. Adds a single-pane remote terminal in the right sidebar,
backed by xterm.js and an ssh2 PTY channel multiplexed onto the
already-authenticated SSH connection that SftpClient owns.
## Files
- `src/ssh/SftpClient.ts` — `openShell({rows, cols, term?, cmd?})`
method (mirrors existing `openUnixStream` / `exec` shape). When `cmd`
is supplied, runs it under the PTY (e.g. `/usr/bin/zsh -l`); otherwise
the remote launches the user's default login shell.
- `src/ssh/RemoteShell.ts` (NEW) — thin lifecycle wrapper around the
ssh2 ClientChannel. `open() / write() / resize() / close() / isOpen()`.
Stdout + stderr both surface as a single `onData(chunk)`. Two
synchronous-flag guards keep `open()` race-free:
`opening` blocks concurrent open() calls before they reach the
await; the post-await `if (this.closed) ch.end()` check prevents a
channel leak when close() runs while open() is pending.
- `src/ui/RemoteTerminalView.ts` (NEW) — `extends ItemView`, renders
xterm.js + FitAddon, debounced 100 ms ResizeObserver → setWindow,
graceful "Not connected" / "Failed to open shell" disconnected
states. Reads font/scrollback/shell from plugin settings each `onOpen`.
The `onClose` callback logs `reason`/`cause` unconditionally so the
channel-close audit trail isn't lost when the View has torn down.
The `shell.open()` catch block disposes + nulls shell/term/fit so
late channel events can't reach a half-constructed view.
- `src/main.ts` — `registerView(VIEW_TYPE_REMOTE_TERMINAL, ...)` plus
`addCommand("Open remote terminal")` (checkCallback gates on
client.isAlive). `disconnect()` calls
`app.workspace.detachLeavesOfType(VIEW_TYPE_REMOTE_TERMINAL)` so the
shell channel close fires while ssh2.Client is still around.
`openRemoteTerminal()` uses `setActiveLeaf` rather than `revealLeaf`
to keep the minAppVersion 1.4.0 contract (revealLeaf needs 1.7.2).
`openingTerminal` re-entrant flag prevents two leaves being created
by rapid command-palette activations.
- `src/types.ts` + `src/constants.ts` — three new optional settings:
`terminalShell?: string`, `terminalFontSize?: number` (default 12),
`terminalScrollback?: number` (default 1000).
- `src/settings/SettingsTab.ts` — Terminal section with the three
inputs (font validated 6-32 px, scrollback 100-100_000 lines).
- `styles.css` — terminal pane host/disconnected styling + inlined
xterm.js v5 default stylesheet (~3 KB, MIT license preserved) so
users don't need a separate CSS asset.
- `package.json` — `@xterm/xterm@^5.5` + `@xterm/addon-fit@^0.10`
added as production dependencies.
- `.github/workflows/release.yml` + `.github/workflows/ci.yml` —
bundle cap raised 600 -> 800 KB to absorb the bundled xterm
(770 KB actual). esbuild splitting needs ESM which Obsidian doesn't
support, so single-bundle is the only option. Comment in workflows
explains the rationale.
- `tests/RemoteShell.test.ts` (NEW) — 20 unit tests against a fake
ssh2 ClientChannel: open/double-open guards, concurrent-open guard,
close-while-pending end()s the channel + bypasses listener-attach,
stdout+stderr routing, write/resize/close, lifecycle invariants
(no double-onClose, suppressed-after-close events, etc.).
- `vitest.config.ts` — `RemoteTerminalView.ts` added to coverage
exclude (xterm.js + ResizeObserver under jsdom is impractical to
unit-test; covered by manual smoke against a real Obsidian window,
consistent with the other src/ui/*Modal/Bar exclusions).
## Verified
- `tsc --noEmit` clean
- `npm run lint` clean (sentence-case + active-window-timers
+ no-floating-promises + minAppVersion gates all satisfied)
- `vitest run` 780 pass / 1 Windows-skipped (was 760)
- `node esbuild.config.mjs production` -> 770 KB main.js (under 800 KB cap)
## Out of scope (per #149 v1)
- Multiple terminal tabs
- Persisted scrollback across re-opens
- Split panes
- Search-in-scrollback
- back-pressure on huge pastes (ssh2 handles it; we trust write() return)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:17:11 +00:00
|
|
|
|
|
|
|
|
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
|
|
|
|
z-index: 6;
|
|
|
|
|
position: absolute;
|
|
|
|
|
}
|
|
|
|
|
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
|
|
|
|
z-index: 7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.xterm-decoration-overview-ruler {
|
|
|
|
|
z-index: 8;
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
right: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.xterm-decoration-top {
|
|
|
|
|
z-index: 2;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|