refactor(sublime/terminal): swap external new_terminal for transient Terminus pane

The v0.7.0 Open Remote Terminal command spawned an external OS terminal
via Sublime's `new_terminal` (Terminal package). Walk that back: the
in-package terminal is meant for short ad-hoc commands (`ls`, `git
status`, running a script) and the embedded experience is preferable
when scope is that narrow. Long-running shell workflows are explicitly
an external-terminal job that the user runs themselves outside this
package.

Behavior:
- terminus_open with cmd=['ssh', '-t', alias, 'cd <root> && exec
  $SHELL -l'] and auto_close=True. The pane closes when the shell exits;
  no view-reuse cache, no tmux, no persistent state across launches.
- If Terminus is not installed the command surfaces an install hint via
  the status bar rather than silently doing nothing.
- The `new_terminal` external-spawn path is removed.

The Korean IME bug on Windows that motivated the v0.7.0 external switch
is acceptable in this narrow ad-hoc-command scope: typed inputs are
short, often pre-built (paste-from-clipboard or arrow-key history), and
non-typed-Korean. For interactive shells where IME matters more, users
go external.

1168 sublime tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 13:41:58 +09:00
parent 7daddf82ae
commit d21600f0c1
2 changed files with 91 additions and 34 deletions

View File

@@ -6889,22 +6889,18 @@ def _write_connected_host_state(mapping: Dict[str, str]) -> None:
class SessionsOpenRemoteTerminalCommand(sublime_plugin.WindowCommand):
"""Open the workspace's remote root in an external OS terminal via ``ssh -t``.
"""Open a transient Terminus pane SSH'd into the workspace's remote root.
The command spawns the host's preferred terminal (Sublime's own
``new_terminal``, which the user-installed ``Terminal`` package
provides) running ``ssh -t <alias> "cd <remote_root> && exec
\\$SHELL -l"``. ``ssh -t`` allocates a tty so the remote shell
behaves interactively, and ``exec`` ensures Ctrl-D / shell exit
closes the terminal cleanly.
No tmux, no embedded view: the OS terminal owns the lifecycle so
Windows IME, scrollback, copy/paste, and font rendering are all
handled natively.
Scope is intentionally narrow: a fresh ad-hoc shell for short, simple
commands (``ls``, ``git status``, running a script). No view-reuse cache,
no tmux session multiplexing, no persistence across pane closes — when
the shell exits the pane closes (``auto_close=True``). For long-running
or tmux-heavy workflows the user is expected to open their own external
terminal.
"""
def run(self) -> None:
"""Open an external OS terminal SSH'd into the workspace's remote root."""
"""Open a Terminus pane attached to the workspace's remote root."""
settings = SessionsSettings()
context = _workspace_context(self.window, settings)
if context is None:
@@ -6915,24 +6911,34 @@ class SessionsOpenRemoteTerminalCommand(sublime_plugin.WindowCommand):
if not callable(run_command):
_status_message("No terminal command is available in this Sublime build.")
return
find_resources = getattr(sublime, "find_resources", None)
has_terminus = False
if callable(find_resources):
try:
has_terminus = bool(find_resources("Terminus.sublime-settings"))
except (TypeError, ValueError, RuntimeError):
has_terminus = False
if not has_terminus:
_status_message(
"Sessions: install the Terminus package to use Open Remote Terminal."
)
return
remote_invocation = "cd {} && exec ${{SHELL:-/bin/sh}} -l".format(
shlex.quote(remote_root),
)
ssh_cmd = "ssh -t {} {}".format(
shlex.quote(host_alias),
shlex.quote(remote_invocation),
)
run_command(
"new_terminal",
"terminus_open",
{
"cmd": ssh_cmd,
"cmd": ["ssh", "-t", host_alias, remote_invocation],
"cwd": str(context.local_cache_root),
"title": "ssh {}:{}".format(host_alias, remote_root),
"auto_close": True,
},
)
_status_message(
"Sessions: opening external terminal for {}:{}".format(
host_alias, remote_root
)
"Sessions: opening terminal for {}:{}".format(host_alias, remote_root)
)

View File

@@ -398,22 +398,30 @@ def test_connect_selected_workspace_opens_remote_tree_view(
assert refresh_calls == [1]
def test_open_remote_terminal_invokes_new_terminal_with_ssh_cd_invocation(
def test_open_remote_terminal_opens_transient_terminus_pane(
tmp_path: Path, monkeypatch
) -> None:
"""Resolves workspace alias + remote root and dispatches to ``new_terminal``.
"""Resolves workspace alias + remote root and dispatches to ``terminus_open``.
The terminal lifetime now lives in the OS terminal (no embedded view, no
tmux convenience layer). The command's only job is to assemble the
``ssh -t`` invocation that lands the user in their remote root and hand
it off to Sublime's ``new_terminal`` (provided by the user-installed
``Terminal`` package).
The terminal is intentionally transient: ``auto_close=True`` so the pane
closes when the shell exits, no view-reuse cache, no tmux. For long-lived
or tmux-heavy workflows the user runs an external terminal themselves.
"""
ssh_config_path = tmp_path / "config"
_write_ssh_config(ssh_config_path, "Host prod\n HostName prod.example.com\n")
settings = SessionsSettings(ssh_config_path=ssh_config_path)
monkeypatch.setattr(commands, "SessionsSettings", lambda: settings)
monkeypatch.setattr(commands.sublime, "cache_path", lambda: str(tmp_path / "cache"))
monkeypatch.setattr(
commands.sublime,
"find_resources",
lambda pattern: (
("Packages/Terminus/Terminus.sublime-settings",)
if "Terminus" in pattern
else ()
),
raising=False,
)
recent_store = commands._recent_store(settings)
recent_store.save_index(
RecentWorkspaceIndex(
@@ -433,14 +441,57 @@ def test_open_remote_terminal_invokes_new_terminal_with_ssh_cd_invocation(
commands.SessionsOpenRemoteTerminalCommand(window).run()
new_terminal_calls = [c for c in window.window_commands if c[0] == "new_terminal"]
assert len(new_terminal_calls) == 1
args = new_terminal_calls[0][1]
assert args["cmd"] == ("ssh -t prod 'cd /srv/app && exec ${SHELL:-/bin/sh} -l'")
terminus_calls = [c for c in window.window_commands if c[0] == "terminus_open"]
assert len(terminus_calls) == 1
args = terminus_calls[0][1]
assert args["cmd"] == [
"ssh",
"-t",
"prod",
"cd /srv/app && exec ${SHELL:-/bin/sh} -l",
]
assert args["auto_close"] is True
assert args["title"] == "ssh prod:/srv/app"
assert "cwd" in args
assert any(
"opening external terminal for prod:/srv/app" in m for m in status_messages
assert any("opening terminal for prod:/srv/app" in m for m in status_messages)
def test_open_remote_terminal_prompts_to_install_terminus_when_absent(
tmp_path: Path, monkeypatch
) -> None:
"""Without Terminus installed the command surfaces an install hint, no spawn."""
ssh_config_path = tmp_path / "config"
_write_ssh_config(ssh_config_path, "Host prod\n HostName prod.example.com\n")
settings = SessionsSettings(ssh_config_path=ssh_config_path)
monkeypatch.setattr(commands, "SessionsSettings", lambda: settings)
monkeypatch.setattr(commands.sublime, "cache_path", lambda: str(tmp_path / "cache"))
monkeypatch.setattr(
commands.sublime,
"find_resources",
lambda pattern: (),
raising=False,
)
recent_store = commands._recent_store(settings)
recent_store.save_index(
RecentWorkspaceIndex(
(
RecentWorkspace(
"prod",
"/srv/app",
"cache-123",
"2026-04-12T03:00:00+00:00",
),
)
)
)
status_messages: List[str] = []
monkeypatch.setattr(commands.sublime, "status_message", status_messages.append)
window = FakeWindow(project_data={"settings": {PROJECT_SETTINGS_KEY: "cache-123"}})
commands.SessionsOpenRemoteTerminalCommand(window).run()
assert window.window_commands == []
assert any("install the Terminus package" in m for m in status_messages)
def test_connected_host_alias_recovers_from_persisted_state(