Compare commits
2 Commits
e61e56c21d
...
f70999a9d7
| Author | SHA1 | Date | |
|---|---|---|---|
| f70999a9d7 | |||
| 7b43de90ad |
@@ -5,14 +5,14 @@
|
||||
Current focus:
|
||||
|
||||
- **Completed milestones:** Phase 0–6.2 (all closed), Phase 7 - Stability Hardening (closed), Phase 8 - Rust Transport Expansion (closed), Remote LSP integration track ([#34](https://git.teahaven.kr/sublime-rs/sessions/issues/34), [#35](https://git.teahaven.kr/sublime-rs/sessions/issues/35), [#36](https://git.teahaven.kr/sublime-rs/sessions/issues/36), [#37](https://git.teahaven.kr/sublime-rs/sessions/issues/37) — all closed; `local_bridge lsp-stdio`, persistent broker attach IPC, `session_helper lsp_stdio` supervision, URI rewrite + save barrier, host-scoped install with workspace-scoped env/config). See [`planning/GITEA_ISSUES.md`](planning/GITEA_ISSUES.md).
|
||||
- **Open milestones:** Phase 9 - Quality Gates & Scale ([#10](https://git.teahaven.kr/sublime-rs/sessions/issues/10), [#32](https://git.teahaven.kr/sublime-rs/sessions/issues/32) large-file streaming). [#29](https://git.teahaven.kr/sublime-rs/sessions/issues/29) (diff-centric review) was reframed in the 2026-04-25 distribution review and is **no longer the next feature** — see [`planning/REVIEW_v0_6_4_DISTRIBUTION_PLAN.md`](planning/REVIEW_v0_6_4_DISTRIBUTION_PLAN.md) and [`planning/SHIPPED.md`](planning/SHIPPED.md). Track D (in-Sublime agent integration) was dropped 2026-04-27 — see [`planning/BACKLOG.md`](planning/BACKLOG.md).
|
||||
- **Open milestones:** Phase 9 - Quality Gates & Scale ([#10](https://git.teahaven.kr/sublime-rs/sessions/issues/10), [#32](https://git.teahaven.kr/sublime-rs/sessions/issues/32) large-file streaming). [#29](https://git.teahaven.kr/sublime-rs/sessions/issues/29) (diff-centric review) was reframed in the 2026-04-25 distribution review and is **no longer the next feature** — see [`planning/REVIEW_v0_6_4_DISTRIBUTION_PLAN.md`](planning/REVIEW_v0_6_4_DISTRIBUTION_PLAN.md) and [`planning/SHIPPED.md`](planning/SHIPPED.md). Track D (in-Sublime agent integration) was dropped 2026-04-27 and the residual `tmux`/`claude-code`/`codex-cli`/`jupyterlab` catalog entries were excised on 2026-04-30 — see [`planning/BACKLOG.md`](planning/BACKLOG.md) and [`planning/SHIPPED.md`](planning/SHIPPED.md).
|
||||
- **Execution order (2026-04, Rust-first):** P0.5 stabilization → crate consolidation → artifact publish + manifest/checksum → **[#24](https://git.teahaven.kr/sublime-rs/sessions/issues/24)** Rust runtime ownership → **[#32](https://git.teahaven.kr/sublime-rs/sessions/issues/32)** large-file → Track G v1 (multi-repo, refs/ fast-path, line-staging polish). #29 diff-centric review/apply is **deprioritized**, not on this order. Normative detail: [`planning/GITEA_ISSUES.md`](planning/GITEA_ISSUES.md) (execution priority and schedule), migration waves: [`planning/PYTHON_RUST_BOUNDARY.md`](planning/PYTHON_RUST_BOUNDARY.md). Distribution-readiness + ownership-migration plan: [`planning/REVIEW_v0_6_4_DISTRIBUTION_PLAN.md`](planning/REVIEW_v0_6_4_DISTRIBUTION_PLAN.md).
|
||||
- **P0.5 stabilization (2026-04, closed):** persistent bridge, download-only helper, reconnect, mirror ignore patterns, save conflict UI, wire contract test coverage (bridge stdout fixtures, binary smoke test, ABI smoke test), stability hardening (prune symlink/permission edges, multi-window dedup, refresh race prevention), remote file auto-reload via periodic stat → revert, LSP-ready on-demand fetch via external path mapper + `on_window_command` interceptor.
|
||||
- SSH config driven workspace selection
|
||||
- session-bound helper over SSH stdio
|
||||
- local cache with local-host-independent workspace identity
|
||||
- formatter and linter execution in the remote environment (baseline + #30 pipeline on save)
|
||||
- ~~long-term evolution toward a multi-session agent window~~ — **frozen 2026-04-27**: the v0.6.0–v0.6.7 in-Sublime agent code (`agent_tmux`, `agent_window_layout`, `agent_switcher_view`, agent palette commands, `tmux`/`claude-code`/`codex-cli` catalog entries) stays in the tree but has no follow-up work; agents now run in an external terminal that the user manages outside Sublime. See [`planning/BACKLOG.md`](planning/BACKLOG.md) Track D.
|
||||
- ~~long-term evolution toward a multi-session agent window~~ — **dropped 2026-04-27, residue removed 2026-04-30**: the v0.6.0–v0.6.7 in-Sublime agent code (`agent_tmux`, `agent_window_layout`, `agent_switcher_view`, agent palette commands) was deleted in v0.6.7; the `tmux`/`claude-code`/`codex-cli` catalog entries and the parallel `jupyterlab` (`kind="jupyter"`) entry were excised on 2026-04-30. Agents now run in an external terminal that the user manages outside Sublime; `marimo` replaces in-tree Jupyter hosting. See [`planning/BACKLOG.md`](planning/BACKLOG.md) Track D and [`planning/SHIPPED.md`](planning/SHIPPED.md).
|
||||
|
||||
## Repository layout
|
||||
|
||||
|
||||
@@ -1,387 +0,0 @@
|
||||
# AGENT_TMUX_LAYOUT — remote agents via tmux, three-group Sublime window
|
||||
|
||||
**Status**: design only. Supersedes the earlier agent-chat / diff-viewer
|
||||
design (which has been dropped — we don't build a chat UI).
|
||||
|
||||
**Depends on**: `PYTHON_RUST_BOUNDARY.md` (no protocol changes here —
|
||||
agents run as plain CLIs over SSH; the bridge stays for file / LSP
|
||||
channels). Interacts with the managed-extension catalog (`kind="agent"`).
|
||||
|
||||
## Why tmux instead of a custom chat UI
|
||||
|
||||
The previous plan was to reimplement a chat widget in Sublime using
|
||||
phantoms, panels, and a custom NDJSON protocol to codex / claude
|
||||
daemons. That is a lot of UI code that reinvents a terminal. It also
|
||||
fragments when the agent CLI updates its protocol.
|
||||
|
||||
Observation: **every serious remote agent already ships a working
|
||||
terminal UI** (codex, claude code, anthropic CLI). Running that UI
|
||||
inside a Terminus pane that is attached to a tmux session on the
|
||||
remote host gives us:
|
||||
|
||||
- the real UX the agent vendor ships, including their slash commands,
|
||||
markdown, syntax, keybindings;
|
||||
- persistence across Sublime restarts (tmux keeps the session and the
|
||||
scrollback);
|
||||
- trivial switching between agents (just attach to a different tmux
|
||||
session) without any protocol layer;
|
||||
- the ability to run multiple agents in parallel, one tmux session
|
||||
each.
|
||||
|
||||
The Sublime side only needs to:
|
||||
|
||||
1. spawn / attach tmux sessions,
|
||||
2. lay out the window into three groups,
|
||||
3. persist and expose the `(workspace, agent)` pairs.
|
||||
|
||||
## Window layout
|
||||
|
||||
```
|
||||
┌──────────────┬──────────────────────────┬─────────────────┐
|
||||
│ │ │ │
|
||||
│ file │ Terminus │ Agent │
|
||||
│ sidebar │ (ssh host │ Session │
|
||||
│ + │ tmux attach -t ...) │ Switcher │
|
||||
│ editor │ │ │
|
||||
│ (group 0) │ (group 1) │ (group 2) │
|
||||
│ │ │ │
|
||||
└──────────────┴──────────────────────────┴─────────────────┘
|
||||
```
|
||||
|
||||
- Sublime's built-in left sidebar (workspace file tree) is still there;
|
||||
our layout only affects the editor area to its right.
|
||||
- Group 0: the normal editor pane. File tabs open here.
|
||||
- Group 1: Terminus view attached to the agent's tmux session. This
|
||||
group is **single-view** — switching agents replaces the view.
|
||||
- Group 2: a read-only Sublime view rendering the switcher. Clicks on
|
||||
a pair entry dispatch `sessions_switch_agent_session`.
|
||||
|
||||
Proposed column widths: `[0.40, 0.40, 0.20]`. The switcher column
|
||||
can collapse to 0.0 via a toggle command when the user wants more
|
||||
editor room.
|
||||
|
||||
## Session naming convention
|
||||
|
||||
```
|
||||
sessions-agent-<workspace_cache_key[:8]>-<agent_id>
|
||||
```
|
||||
|
||||
Example: `sessions-agent-07c4844b-claude`. Agent ids come from the
|
||||
catalog entry (see below).
|
||||
|
||||
`tmux new-session -A -s <name> -- <agent_cmd>` is idempotent: attaches
|
||||
if the session exists, spawns with `<agent_cmd>` if it doesn't. We
|
||||
never `kill-session` implicitly — detach only. Explicit
|
||||
`Sessions: Kill Agent Session` command drives cleanup.
|
||||
|
||||
## Extension catalog entries
|
||||
|
||||
Agents are installed via the existing managed-extension flow. New
|
||||
`kind="agent"` variant:
|
||||
|
||||
```python
|
||||
ManagedRemoteExtensionCatalogEntry(
|
||||
install_catalog_id="claude-cli",
|
||||
install_label="Claude Code CLI (remote)",
|
||||
install_argv=("bash", "-lc", _CLAUDE_INSTALL_SCRIPT),
|
||||
remove_argv=("bash", "-lc", _CLAUDE_REMOVE_SCRIPT),
|
||||
probe_argv=("bash", "-lc", "command -v claude && claude --version"),
|
||||
install_cwd=None,
|
||||
kind="agent",
|
||||
)
|
||||
```
|
||||
|
||||
The install scripts are the vendor's official install lines (npm /
|
||||
curl-to-bash / etc.). Probes check `command -v <bin>` + `--version`.
|
||||
We do **not** maintain our own agent binaries.
|
||||
|
||||
## Sub-tracks (parallelisable)
|
||||
|
||||
### D1. Tmux session broker — pure Python, unit-testable
|
||||
|
||||
New module `sublime/sessions/agent_tmux.py`. No Sublime imports at
|
||||
module top.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class TmuxAgentSession:
|
||||
host_alias: str
|
||||
workspace_cache_key: str
|
||||
agent_id: str
|
||||
session_name: str # "sessions-agent-<ws>-<agent>"
|
||||
attach_argv: list[str] # ["ssh", "<host>", "tmux", "attach", "-t", name]
|
||||
spawn_argv: list[str] # ["ssh", "<host>", "tmux", "new-session", "-A", "-s", name, "--", <agent_cmd>]
|
||||
|
||||
class AgentTmuxBroker:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
ssh_command_builder: Callable[[str], list[str]] = ...,
|
||||
run: Callable[..., subprocess.CompletedProcess] = subprocess.run,
|
||||
): ...
|
||||
|
||||
def plan(self, host_alias, workspace_cache_key, agent_id, agent_cmd) -> TmuxAgentSession: ...
|
||||
|
||||
def is_running(self, host_alias, session_name) -> bool:
|
||||
# ssh host tmux has-session -t <name>
|
||||
...
|
||||
|
||||
def attach_or_spawn(self, session: TmuxAgentSession) -> None:
|
||||
# has-session → attach_argv, else new-session command
|
||||
# Called by the Terminus launcher (D3).
|
||||
...
|
||||
|
||||
def list_sessions(self, host_alias) -> list[str]:
|
||||
# ssh host tmux list-sessions -F '#{session_name}'
|
||||
# Filtered to "sessions-agent-*".
|
||||
...
|
||||
|
||||
def kill(self, host_alias, session_name) -> None:
|
||||
# ssh host tmux kill-session -t <name>
|
||||
...
|
||||
```
|
||||
|
||||
Injectable `run` makes everything unit-testable.
|
||||
|
||||
**[files]** `agent_tmux.py` (new), `test_agent_tmux.py` (new).
|
||||
|
||||
### D2. Three-group window layout
|
||||
|
||||
New module `sublime/sessions/agent_window_layout.py`. Provides one
|
||||
command:
|
||||
|
||||
```python
|
||||
class SessionsAgentLayoutCommand(sublime_plugin.WindowCommand):
|
||||
def run(self, cols=(0.40, 0.80, 1.00)) -> None:
|
||||
self.window.set_layout({
|
||||
"cols": [0.0, cols[0], cols[1], cols[2]],
|
||||
"rows": [0.0, 1.0],
|
||||
"cells": [[0, 0, 1, 1], [1, 0, 2, 1], [2, 0, 3, 1]],
|
||||
})
|
||||
```
|
||||
|
||||
Plus `SessionsAgentLayoutCollapseSwitcherCommand` that widens to
|
||||
`[0.5, 1.0, 1.0]` (hides group 2). Toggleable via keybind in
|
||||
`Default.sublime-keymap`.
|
||||
|
||||
Persists the active layout in workspace state so reload restores it.
|
||||
|
||||
**[files]** `agent_window_layout.py` (new), `workspace_state.py`
|
||||
(extend with a `layout` field), Default keymap.
|
||||
|
||||
### D3. Terminus launcher
|
||||
|
||||
`sessions_open_agent_terminus`, driven by D1 + D2:
|
||||
|
||||
```python
|
||||
def run(self, host_alias, workspace_cache_key, agent_id, agent_cmd):
|
||||
session = broker.plan(host_alias, workspace_cache_key, agent_id, agent_cmd)
|
||||
broker.attach_or_spawn(session) # ensures tmux session exists
|
||||
# Terminus docs: terminus_open accepts {"cmd": [...], "cwd": str}.
|
||||
self.window.focus_group(1)
|
||||
self.window.run_command("terminus_open", {
|
||||
"shell_cmd": " ".join(shlex.quote(a) for a in session.attach_argv),
|
||||
"cwd": None,
|
||||
"title": f"Agent · {agent_id} · {host_alias}",
|
||||
"pre_window_hooks": [["move_to_group", {"group": 1}]],
|
||||
})
|
||||
```
|
||||
|
||||
Handles the tmux-not-installed case: probe via `ssh host command -v
|
||||
tmux`; if missing, show `Sessions: Install Remote Extension` hint with
|
||||
a one-shot install (tmux goes in the extension catalog too, as
|
||||
`kind="agent"` prerequisite).
|
||||
|
||||
**[files]** `commands.py` (add class), `Sessions.sublime-commands`
|
||||
(palette entry).
|
||||
|
||||
### D4. Switcher view (group 2)
|
||||
|
||||
Group 2 holds a named view (`settings().get("sessions_agent_switcher")
|
||||
== True`). Renders a list like:
|
||||
|
||||
```
|
||||
○ 07c4844b · claude [attached]
|
||||
● a75c7f0f · codex (active)
|
||||
○ a75c7f0f · claude
|
||||
─────────────
|
||||
+ New agent session…
|
||||
```
|
||||
|
||||
Clicks resolved via `on_text_command drag_select` → if the cursor
|
||||
line maps to a pair row, fire `sessions_switch_agent_session
|
||||
{"pair_id": "<cache_key>:<agent_id>"}`.
|
||||
|
||||
Live updates: re-render on D5's pair-change callbacks.
|
||||
|
||||
**[files]** `agent_switcher_view.py` (new), integration hook in
|
||||
`commands.py`.
|
||||
|
||||
### D5. Pair persistence + switch orchestration
|
||||
|
||||
Workspace-level store in `workspace_state.py`:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class AgentPair:
|
||||
workspace_cache_key: str
|
||||
agent_id: str
|
||||
created_at: float
|
||||
last_activated_at: float
|
||||
|
||||
def register_agent_pair(pair: AgentPair) -> None: ...
|
||||
def active_agent_pair(workspace_cache_key: str) -> Optional[AgentPair]: ...
|
||||
def list_agent_pairs() -> list[AgentPair]: ...
|
||||
```
|
||||
|
||||
New command `SessionsSwitchAgentSessionCommand`:
|
||||
|
||||
1. Find the target pair.
|
||||
2. If the workspace behind `pair.workspace_cache_key` is not the
|
||||
current active workspace, call existing workspace-switch machinery
|
||||
first (project data swap). File sidebar + editor re-targets follow.
|
||||
3. Attach Terminus in group 1 to the pair's tmux session (D3).
|
||||
4. Refresh switcher view (D4).
|
||||
|
||||
**[files]** `commands.py`, `workspace_state.py`, `agent_switcher_view.py`.
|
||||
|
||||
### D6. Lifecycle + teardown
|
||||
|
||||
- `plugin_unloaded`: detach (Terminus `terminus_keypress ctrl-b d`
|
||||
equivalent) — do **not** kill. tmux keeps the agent alive.
|
||||
- `Sessions: Kill Agent Session` command (palette) — explicit kill of
|
||||
the active pair's tmux session; user confirmation prompt.
|
||||
- `Sessions: Kill All Agent Sessions` — explicit sweep on the
|
||||
currently connected host.
|
||||
|
||||
**[files]** `commands.py`, `agent_tmux.py`.
|
||||
|
||||
### D7. Edit-proposal surfacing in the editor
|
||||
|
||||
**Goal**: when the agent proposes an edit (i.e. calls its edit / write /
|
||||
patch tool), show the proposed change as a diff in the Sublime editor,
|
||||
not only inside the Terminus pane. Apply-on-click is a nice-to-have but
|
||||
not required for the first cut; **just making the proposal visible in
|
||||
the editor surface is the MVP**.
|
||||
|
||||
Three phases, ordered by both effort and fidelity:
|
||||
|
||||
#### Phase 1 — pipe-pane scrollback tail (agent-agnostic, visibility-only)
|
||||
|
||||
Mirror the Terminus/tmux pane to a local file via `tmux pipe-pane`, tail
|
||||
it with a Python watcher, and parse out unified-diff blocks. Render them
|
||||
in a dedicated output panel `Sessions: Agent Proposals` with the file
|
||||
path + hunk text. Clicking a path opens the relevant file (via the
|
||||
existing on-demand fetch listener).
|
||||
|
||||
- Works for any agent that prints a unified diff to the terminal
|
||||
(claude, codex, aider, etc.).
|
||||
- **No apply** — the agent still drives its own confirmation step in
|
||||
the terminal. Our panel is purely informational.
|
||||
- **Brittle**: ANSI colour codes, pager truncation, non-standard diff
|
||||
formats can corrupt the parse. We handle the common case and drop
|
||||
silently on weird input.
|
||||
- **[files]** `agent_proposal_watcher.py` (new) — tail + parse + emit to
|
||||
an output panel; `commands.py` — palette entries for `Sessions: Open
|
||||
Agent Proposals`, `Sessions: Clear Agent Proposals`.
|
||||
- **[testability]** `_parse_unified_diff_stream` is pure string→list;
|
||||
unit-testable with fixture blobs. Tail loop mocked with an in-memory
|
||||
file-like.
|
||||
|
||||
#### Phase 2 — post-apply phantom badge (agent-agnostic, already-applied)
|
||||
|
||||
When the agent writes a file on the remote and our existing `file/watch`
|
||||
fires a change event for an already-open local cache file:
|
||||
|
||||
- Snapshot the buffer before re-fetching.
|
||||
- Compute a line-level diff between the snapshot and the new content.
|
||||
- Decorate the modified hunks with a Sublime phantom / region of the
|
||||
form `🤖 claude · <time>` in a distinct colour scope
|
||||
(`region.bluish markup.agent.changed`).
|
||||
- Fades after 30 seconds or on next edit.
|
||||
|
||||
No user action required; purely a visual cue that "the file just
|
||||
under your cursor changed because of the agent, not you". Works for
|
||||
every agent that writes files on the remote, regardless of how the
|
||||
user approved the change.
|
||||
|
||||
- **[files]** `agent_change_badge.py` (new), hooked into the existing
|
||||
`file/watch` handling in `ssh_file_transport`/`commands`.
|
||||
- **Accepts**: that by the time we render the badge the change is
|
||||
already applied. This is the easiest-to-ship "editor sees the diff"
|
||||
path — the user sees what changed, still in the normal file flow.
|
||||
|
||||
#### Phase 3 — pre-apply preview via Claude Code hooks (claude-specific)
|
||||
|
||||
Claude Code ships first-class support for `PreToolUse` and `PostToolUse`
|
||||
hooks (configured in `.claude/settings.json` on the remote). We install
|
||||
a small shell hook that:
|
||||
|
||||
- On `PreToolUse` for `edit_file` / `write_file` / `str_replace`: write
|
||||
the tool-call JSON to a local Unix socket (forwarded via `ssh -L`
|
||||
control-master) and **wait** for an `approve` / `reject` reply from
|
||||
Sublime before letting the hook return.
|
||||
- Sublime receives the JSON, renders a rich diff preview in the
|
||||
relevant editor view (using Sublime's built-in `diff` syntax or a
|
||||
phantom overlay), and shows floating `Apply` / `Reject` buttons.
|
||||
- User's click sends `approve` / `reject` back through the socket; the
|
||||
hook returns; claude proceeds or aborts the tool call.
|
||||
|
||||
This is the most ambitious variant: editor-native preview, user clicks
|
||||
in Sublime (not in the terminal), claude respects the decision. It is
|
||||
**only claude-specific** — codex / aider / others do not expose
|
||||
equivalent hooks at the time of writing.
|
||||
|
||||
- **[files]** `agent_claude_hook.sh` (shipped hook script, `bash -lc`
|
||||
compatible), `claude_hook_server.py` (new: Unix-socket server inside
|
||||
the Sublime plugin process), `agent_proposal_preview.py` (new:
|
||||
phantom/diff rendering).
|
||||
- **[risks]** hook timeout: if Sublime isn't running or the socket isn't
|
||||
listening, claude waits indefinitely. The hook must have a 10 s
|
||||
default-deny fallback.
|
||||
- **[installer]** add the hook script to the managed-extension catalog
|
||||
under `kind="agent"` alongside the claude CLI install. Sessions drops
|
||||
`.claude/settings.json` on first use if missing.
|
||||
|
||||
**Phase adoption plan**:
|
||||
|
||||
- Phase 1 ships with v0.6.0 alongside the tmux layout (it's
|
||||
agent-agnostic and cheap).
|
||||
- Phase 2 ships in a follow-up (v0.6.1) — needs thoughtful diff
|
||||
colouring that doesn't clash with Sublime's save-status markers.
|
||||
- Phase 3 is gated on demand (v0.7.0 candidate). Users who want
|
||||
apply-from-editor get it; others stay on Phase 1/2.
|
||||
|
||||
## Parallel work plan
|
||||
|
||||
Two agents + one integrator:
|
||||
|
||||
### Agent α (pure-Python, no Sublime)
|
||||
|
||||
Owns D1 (broker) and the tmux / SSH CLI details. Output: tested
|
||||
`agent_tmux.py` + comprehensive unit tests.
|
||||
|
||||
### Agent β (Sublime-facing UI)
|
||||
|
||||
Owns D2 (layout) and D4 (switcher view skeleton with fake pair data).
|
||||
Output: clickable layout + list, no integration yet.
|
||||
|
||||
### Integrator (manual)
|
||||
|
||||
Lands D3 + D5 + D6 + extension catalog entries on top of α+β, wires
|
||||
everything, runs full pytest + manual macOS smoke test.
|
||||
|
||||
Total scope ≈ 600–900 Python LoC + 400 test LoC. No Rust changes. No
|
||||
protocol changes. Release as **v0.6.0** (minor bump — new user-visible
|
||||
feature).
|
||||
|
||||
## Out of scope (do not do here)
|
||||
|
||||
- Agent-specific parsing of output beyond unified diffs (markdown
|
||||
rendering, thinking blocks, etc.). Terminus renders the raw agent
|
||||
UI verbatim. If users want richer output, the agent CLI should
|
||||
provide it. Diff surfacing is the one exception — see D7.
|
||||
- In-Sublime chat widgets / side panels. Explicitly dropped.
|
||||
- Replacing the agent's own in-terminal confirmation flow except via
|
||||
the claude-hook path (D7 Phase 3).
|
||||
- Any change to the local_bridge / session_helper protocol.
|
||||
@@ -73,16 +73,21 @@ investment in Terminus-side polish is out of scope.
|
||||
|
||||
---
|
||||
|
||||
## ~~Track D — Agent integration via tmux~~ — **[dropped 2026-04-27]**
|
||||
## ~~Track D — Agent integration via tmux~~ — **[dropped 2026-04-27, residue removed 2026-04-30]**
|
||||
|
||||
Whole-track drop. The new direction: agents (codex / claude / etc.)
|
||||
run in an external terminal that the user manages outside Sublime —
|
||||
no in-Sublime layout / switcher / proposal-surfacing work. The
|
||||
v0.6.0–v0.6.7 work that already shipped (`agent_tmux`,
|
||||
`agent_window_layout`, `agent_switcher_view`, workspace/agent pair
|
||||
registry, three palette commands) stays in the codebase but is
|
||||
considered frozen; D1–D7 sub-tracks no longer have follow-up work.
|
||||
`AGENT_TMUX_LAYOUT.md` retained for historical reference only.
|
||||
v0.6.0–v0.6.7 in-tree code (`agent_tmux`, `agent_window_layout`,
|
||||
`agent_switcher_view`, workspace/agent pair registry, three palette
|
||||
commands) was deleted in v0.6.7. The residual catalog entries
|
||||
(`tmux` / `claude-code` / `codex-cli` `kind="agent"` rows plus their
|
||||
install/remove/probe bash blocks) and the parallel `jupyterlab`
|
||||
`kind="jupyter"` row were excised on 2026-04-30 along with the
|
||||
matching tests and the `planning/AGENT_TMUX_LAYOUT.md` design
|
||||
document; D1–D7 sub-tracks have no follow-up work. Anything still
|
||||
needed about the historical layout lives in git history at
|
||||
`v0.6.6..v0.6.7`.
|
||||
|
||||
---
|
||||
|
||||
@@ -492,7 +497,7 @@ continuous `bridge.request_timeout` on `mirror-sync` (45s),
|
||||
`file/watch` (35s), `file/read` (30s). Subsequent "Sessions
|
||||
disconnected" → reconnect loop.
|
||||
|
||||
**Diagnosed** via debug-trace capture (see V0_6_5_REPRO §B1): the
|
||||
**Diagnosed** via debug-trace capture: the
|
||||
deep mirror-sync at `max_traversal_depth=12` over slow tunnels
|
||||
(AWS SSM) genuinely runs 45-50 s end-to-end, just exceeding the
|
||||
generic 45 s request timeout. helper is alive and streaming the
|
||||
|
||||
@@ -106,7 +106,6 @@ MVP code may live in Python for velocity; **new non-trivial logic should default
|
||||
| `workspace_state.py` (identity) | Cache key, paths | `workspace_identity` | `normalize_remote_root` is **Rust-only** via `sessions_native` cdylib; Python `cache_key` hashing remains until a later slice. |
|
||||
| `ssh_runner.py`, `ssh_file_transport.py` | SSH subprocess, file I/O | `local_bridge`, `session_helper` | Python glue only; **no remote-Python transport fallback** for tree/file (bridge required or structured failure). |
|
||||
| `file_state.py` | Open/save policy, conflict rules | *future* `sessions_file_policy` or similar | Pure functions → good Rust candidate. |
|
||||
| `agent_remote_payload.py` | Sublime-side envelope **glue only** | `local_bridge::agent_remote_payload` + `local_bridge parse-agent-editor-envelope` | **Rust only** for parsing/validation; Python subprocesses `local_bridge` (no second implementation). |
|
||||
| `connect_preflight.py` | remote-root validation | `workspace_identity` + `sessions_native` | Uses ``normalize_remote_root`` (Rust); host-alias resolution stays Python (SSH config objects). |
|
||||
| `settings_model.py` | typed settings | *future* | Optional codegen from JSON schema. |
|
||||
|
||||
|
||||
@@ -172,7 +172,8 @@ not by design or code:
|
||||
- Local build verification: maintainers running on macOS / Windows
|
||||
can `cargo build` + `python -m compileall` + import smoke locally
|
||||
before tagging. Document this as a release-time manual gate in
|
||||
`planning/V0_6_5_REPRO.md` (or successor).
|
||||
the next per-version repro checklist (the v0.6.5-specific one
|
||||
was retired with the Track D residue cleanup; replace as needed).
|
||||
- Document the "currently Linux-only signed bundle" reality in
|
||||
README + SECURITY.md so external users aren't surprised by the
|
||||
asset list.
|
||||
@@ -517,19 +518,18 @@ Python.
|
||||
envelope, invokes the broker, returns the typed result. `_rust_ffi`
|
||||
hosts ~7 functions, not 30+.
|
||||
|
||||
### 3.5 Stage 4 — agent / diff / runtime state ownership `[plan]`
|
||||
### 3.5 Stage 4 — agent / diff / runtime state ownership `[obsolete — Track D dropped 2026-04-27]`
|
||||
|
||||
**Move out of Python:**
|
||||
- `agent_proposal_watcher` diff parser (already pure Python, no
|
||||
Sublime — explicitly tagged as a Rust candidate).
|
||||
- Agent pair registry in `workspace_state.py` (module-global
|
||||
mutable).
|
||||
- `agent_tmux::AgentTmuxBroker` orchestration.
|
||||
- Deferred-dir registry in `workspace_state.py`.
|
||||
|
||||
**Python keeps:** layout (`agent_window_layout.py`), switcher view
|
||||
(`agent_switcher_view.py`), output panel rendering, palette wiring
|
||||
for agent commands.
|
||||
This stage is no longer applicable. Track D was dropped on
|
||||
2026-04-27 and the v0.6.7 commit deleted `agent_proposal_watcher`,
|
||||
`agent_change_badge`, `agent_tmux`, `agent_window_layout`,
|
||||
`agent_switcher_view`, and the workspace/agent pair registry.
|
||||
The 2026-04-30 cleanup excised the residual `tmux`/`claude-code`/
|
||||
`codex-cli` `kind="agent"` catalog entries, the parallel
|
||||
`jupyterlab` `kind="jupyter"` row, and the `AGENT_TMUX_LAYOUT.md`
|
||||
design doc. There is nothing left to migrate to Rust under this
|
||||
stage; the remaining `workspace_state.py` deferred-dir registry can
|
||||
be carried by Stage 1 if it ever needs to move.
|
||||
|
||||
### 3.6 Success metrics — not LOC `[plan]`
|
||||
|
||||
@@ -555,23 +555,19 @@ after each stage.
|
||||
|
||||
Concrete order, lowest risk first:
|
||||
|
||||
1. **Pure-Python no-Sublime modules first** (review-recommended):
|
||||
`agent_proposal_watcher` diff parser is the explicit example —
|
||||
no `sublime` import, easy port surface. Use it as the warm-up
|
||||
for the migration tooling.
|
||||
2. **Stage 1 (broker ownership)** — biggest effect, central
|
||||
1. **Stage 1 (broker ownership)** — biggest effect, central
|
||||
choke point. Land before stages 2/3 because they depend on the
|
||||
broker for cancellation + lifecycle.
|
||||
3. **Stage 2 (materialization)** — paired with §2.2 large-file
|
||||
2. **Stage 2 (materialization)** — paired with §2.2 large-file
|
||||
streaming work; the new chunked `file/read` is implemented
|
||||
inside the new Rust materialization pipeline rather than in
|
||||
Python.
|
||||
4. **Stage 3 (envelope ownership)** — naturally falls out of
|
||||
3. **Stage 3 (envelope ownership)** — naturally falls out of
|
||||
stages 1+2; remaining method-builder code in Python is
|
||||
replaced by `runtime_*` calls.
|
||||
5. **Stage 4 (agent / diff / state)** — paired with §2.1
|
||||
diff-centric review work; agent state moves with the new
|
||||
review module.
|
||||
4. ~~**Stage 4 (agent / diff / state)**~~ — obsolete; see §3.5
|
||||
above. Track D was dropped 2026-04-27 and the agent modules
|
||||
plus catalog residue were deleted in v0.6.7 / 2026-04-30.
|
||||
|
||||
---
|
||||
|
||||
@@ -579,7 +575,7 @@ Concrete order, lowest risk first:
|
||||
|
||||
Per review: "지금은 덜 급한 것":
|
||||
|
||||
- More agent types (catalog already covers Claude Code + Codex CLI).
|
||||
- ~~More agent types (catalog already covers Claude Code + Codex CLI).~~ — moot; Track D dropped 2026-04-27 and the agent catalog rows were excised on 2026-04-30.
|
||||
- More palette commands (palette is already too wide — see §1.3).
|
||||
- Big new LSP redesign (Remote LSP track #34/#35/#36/#37 closed; no
|
||||
unmet need).
|
||||
@@ -621,6 +617,21 @@ Test-health gate stays green after the deletion: adversarial 190
|
||||
(floor 27), mock-only:high-value 0.95 (cap 0.98). No floor
|
||||
adjustment needed.
|
||||
|
||||
**Follow-up cleanup (2026-04-30, v0.7.25)** — the v0.6.7 cut left
|
||||
the catalog install/remove rows behind. Now also deleted:
|
||||
|
||||
- `BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG` rows for `tmux`,
|
||||
`claude-code`, `codex-cli` (`kind="agent"`) and `jupyterlab`
|
||||
(`kind="jupyter"`), plus the twelve `_BUILTIN_BASH_*` install/
|
||||
remove/probe blocks that backed them.
|
||||
- `sublime/tests/test_managed_remote_extension_catalog.py` —
|
||||
`test_catalog_contains_jupyter_extension_entry` and
|
||||
`test_catalog_contains_agent_extension_entries`.
|
||||
- `planning/AGENT_TMUX_LAYOUT.md` (Track D layout design doc).
|
||||
- The frozen-experimental docstring in
|
||||
`managed_remote_extension_catalog.py` and the matching
|
||||
`Sessions.sublime-settings` comment block.
|
||||
|
||||
---
|
||||
|
||||
## Already shipped from this batch
|
||||
|
||||
@@ -9,6 +9,12 @@ Evergreen architecture contracts:
|
||||
- `PYTHON_RUST_BOUNDARY.md` — what lives where, lifecycle invariants.
|
||||
- `VSCODE_REMOTE_TRANSPORT_MODEL.md` — single-session + channel envelopes.
|
||||
|
||||
## v0.7.x — Track G git/SCM, sync mode, Rust ownership
|
||||
|
||||
| ver | landed | module(s) |
|
||||
|---|---|---|
|
||||
| 0.7.25 | **Cleanup: excise Track D residue and the parallel Jupyter catalog row.** Track D (in-Sublime agent integration via tmux) was dropped 2026-04-27; the v0.6.7 commit deleted the live agent code (`agent_tmux`, `agent_window_layout`, `agent_switcher_view`, palette commands) but left three `kind="agent"` catalog rows (`tmux`, `claude-code`, `codex-cli`), nine bash install/remove/probe blocks, the `kind="jupyter"` row for `jupyterlab` (now superseded by `marimo_hosting`), and the matching frozen-experimental docstring + `Sessions.sublime-settings` comment block as install-flow leftovers. All of that residue is now removed: `BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG` drops to four entries (`pyright-langserver`, `ruff`, `rust-analyzer`, `debugpy`), the catalog file shrinks from 358 → 182 lines, and the matching tests (`test_catalog_contains_jupyter_extension_entry`, `test_catalog_contains_agent_extension_entries`) are deleted. `_managed_extension_project_client_keys_for_spec` docstring updated (jupyter → debugger as the non-LSP example), `marimo_hosting.py` comment cleanup (drop dead `tmux`-children + `jupyter_hosting.py` postmortem references — the latter file no longer exists), `commands.py` Open-Remote-Terminal docstring drops "no tmux session multiplexing" framing. README.md + `planning/BACKLOG.md` Track D entry note the 2026-04-30 residue removal date. **No backward compatibility shim** — existing installs that ran the old install flow keep their remote-side `tmux`/`claude-code`/`codex-cli`/`jupyterlab` binaries; users can uninstall manually with the same commands the catalog used to run (apt/dnf/brew for tmux, `rm -rf ~/.claude/bin` for claude-code, `npm uninstall -g @openai/codex` for codex-cli, `pip uninstall jupyterlab jupyter_server jupyterlab_server` for jupyterlab). `debugpy` `kind="debugger"` row stays untouched. **LSP-style project-level override** for the on-save/on-open pipeline: the original Sessions design was that toolchain wiring follows Sublime LSP precedence (package → user → `.sublime-project` `"settings"`), but only the `settings.LSP` row writer (`merge_sessions_lsp_into_project_data`) honored project scope — the on-save toggle path (`_effective_sessions_settings_for_remote_python` → `load_sessions_settings_from_sublime`) read user settings only, so per-workspace toggling required editing global user settings. Now `_effective_sessions_settings_for_remote_python` accepts an optional `window` argument and overlays `window.project_data().get("settings", {})` on top of the user merge for `sessions_remote_python_auto_diagnostics_on_save`, `sessions_remote_python_auto_diagnostics_on_open`, and `sessions_remote_python_tool_pipeline`. All five callers in `commands_python_pipeline.py` now pass `window`; the two listeners (`on_post_save`, `on_activated_async`) reorder window-resolution before the toggle check. Type-safety: bool keys reject non-bool values silently (fall through to user); pipeline runs through `normalize_remote_python_tool_pipeline`. Six new regression tests in `test_commands.py` pin project-overrides-user / user-wins-when-absent / pipeline-override / wrong-type-rejected / null-project_data-safe / no-window-legacy-path. `Sessions.sublime-settings` header comment documents the precedence chain inline. | `sublime/sessions/managed_remote_extension_catalog.py`, `sublime/sessions/commands.py`, `sublime/sessions/commands_python_pipeline.py`, `sublime/sessions/marimo_hosting.py`, `sublime/Sessions.sublime-settings`, `sublime/tests/test_managed_remote_extension_catalog.py`, `sublime/tests/test_commands.py`, `README.md`, `planning/BACKLOG.md` |
|
||||
|
||||
## v0.6.x — tmux-backed remote agent sessions
|
||||
|
||||
| ver | landed | module(s) |
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
# V0_6_5_REPRO — focused repro for current macOS test pass issues
|
||||
|
||||
Short, narrow checklist. **Not** a full feature test (`TEST_CHECKLIST.md`
|
||||
is for that). Goal here: confirm the v0.6.5 batch-3 fixes work on the
|
||||
real macOS host that hit them, and capture diagnostic data for the
|
||||
remaining unresolved issues so the next debug round has signal to work
|
||||
with.
|
||||
|
||||
Run the steps **in order**. Paste the requested log fragments / observed
|
||||
behavior under each step. If a step fails unexpectedly, stop, capture
|
||||
the bundle from §10 of the full TEST_CHECKLIST, and ping back here.
|
||||
|
||||
## 0. Setup
|
||||
|
||||
```sh
|
||||
cd <Sessions checkout>
|
||||
git fetch origin && git checkout v0.6.5 # or main once v0.6.5 lands
|
||||
cargo build --manifest-path rust/Cargo.toml --release --workspace
|
||||
```
|
||||
|
||||
In `Packages/User/Sessions.sublime-settings`, add:
|
||||
|
||||
```json
|
||||
{
|
||||
"sessions_debug_trace_enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
(Set `SESSIONS_BRIDGE_DIAG_VERBOSE=1` in the Sublime launch env only if
|
||||
asked below — it's noisy.)
|
||||
|
||||
Restart Sublime, reopen the test workspace.
|
||||
|
||||
---
|
||||
|
||||
## A. Verify just-fixed items (should now PASS)
|
||||
|
||||
### A1. Agent tmux spawn — no `not a terminal`
|
||||
|
||||
Palette → `Sessions: New Agent Session` → pick `Claude Code CLI (remote)`.
|
||||
|
||||
- [ ] **No** "Sessions warning: Agent session start failed ... open
|
||||
terminal failed: not a terminal". Terminus pane opens; tmux
|
||||
session `sessions-agent-<ws>-claude-code` runs.
|
||||
- [ ] On the remote: `tmux list-sessions | grep sessions-agent` shows it.
|
||||
|
||||
If this still errors, paste the full warning string + grep
|
||||
`bridge.rust.helper_stdout_message` lines around the failure timestamp
|
||||
from `<Sublime cache>/Sessions/logs/debug-trace.log`.
|
||||
|
||||
### A2. New Remote Terminal Pane / Kill Remote Terminal in palette
|
||||
|
||||
- [ ] Palette → type "Sessions: New Remote Terminal Pane" — entry now
|
||||
appears. Select it; numbered tmux session
|
||||
(`sessions-term-<host>-2`) opens.
|
||||
- [ ] Palette → "Sessions: Kill Remote Terminal" — entry now appears.
|
||||
Select it; quick panel lists live terminals; pick one to kill.
|
||||
|
||||
### A3. localhost:PORT canonical URL
|
||||
|
||||
In any Terminus pane: `python3 -m http.server 8080`.
|
||||
|
||||
- [ ] Hover the `0.0.0.0:8080` line — underlined.
|
||||
- [ ] Cmd+click → browser opens **`http://localhost:8080/`** (canonical
|
||||
form with `localhost` host + trailing slash). Should NOT be
|
||||
`about:blank-` or `about:blank` anymore.
|
||||
- [ ] Repeat with `127.0.0.1:8080` line — opens
|
||||
`http://127.0.0.1:8080/`.
|
||||
|
||||
### A4. `Sessions: Preview Remote Agent Payload` hidden
|
||||
|
||||
- [ ] Palette → type "Sessions: Preview" — should **not** show
|
||||
"Preview Remote Agent Payload" by default.
|
||||
- [ ] In `Packages/User/Sessions.sublime-settings`, add
|
||||
`"sessions_show_dev_commands": true`. Reload settings (or
|
||||
restart). Re-type — entry now appears.
|
||||
- [ ] Revert the setting back to `false` (or remove the line).
|
||||
|
||||
---
|
||||
|
||||
## B. Still-broken — capture diagnostic data
|
||||
|
||||
### B1. mirror-sync deep traversal hang at `awaiting_response_dispatch` — **[diagnosed + fixed v0.7.5]**
|
||||
|
||||
Symptom from previous capture: every ~60s a deep
|
||||
`mirror-sync force_full_sync=true max_traversal_depth=12` request hangs
|
||||
at `bridge.request_timeout` after 45s with
|
||||
`stall_phase=awaiting_response_dispatch`. Shallow sync (depth 2) returns
|
||||
in <300ms. Workspace effectively never finishes hydrating, so
|
||||
"No deferred directory to expand" fires (deferred state never recorded)
|
||||
and `sync.done` never lands (eager-hydrate retry never fires).
|
||||
|
||||
**Root cause confirmed (2026-04-27 capture against `aws-celery`)**:
|
||||
helper is alive and streaming responses continuously throughout the
|
||||
45 s window (verbose `bridge.rust.helper_stdout_message` events every
|
||||
~200 ms; line_bytes 200-120 KB; no `helper_stdout_eof`). Adjacent
|
||||
`file/stat` / `file/watch` requests complete normally during the same
|
||||
window (bridge plumbing healthy). The cycle immediately before the
|
||||
captured timeout — `mirror-sync 300` — completed at `elapsed_ms=44952`,
|
||||
literally 48 ms inside the 45 s budget. The next cycle (`mirror-sync
|
||||
308`) didn't make it. So the hang is **not** OOM, **not** a dispatcher
|
||||
stall — the deep walk legitimately runs ~45-50 s on this remote, just
|
||||
exceeding the timeout. `stall_phase=awaiting_response_dispatch` is only
|
||||
the Python-side label for `RequestOutcomeKind.TIMEOUT`, not an actual
|
||||
"stall" state. Both prior gut hypotheses (helper death, channel buffer
|
||||
overflow) ruled out by the trace.
|
||||
|
||||
**Fix shipped in v0.7.5** (see BACKLOG M5 for the full release notes):
|
||||
- `sessions_mirror_max_traversal_depth` default 12 → 5 so auto-deepen
|
||||
stays well under budget on slow tunnels.
|
||||
- Mirror-sync timeout split from the generic 45 s into a separate 90 s
|
||||
default, configurable via `sessions_mirror_sync_timeout_s`.
|
||||
- Auto-refresh exponential backoff (1×, 2×, 4×, 8×, 16× capped) on
|
||||
consecutive failures + reset on first success — stops the every-
|
||||
minute pile-on while a helper is still working.
|
||||
|
||||
This entry is left as the diagnosis record; the original capture
|
||||
recipe below stays useful for any future timeout-shaped repro.
|
||||
|
||||
Capture:
|
||||
|
||||
1. Set `SESSIONS_BRIDGE_DIAG_VERBOSE=1` in the Sublime launch env (so
|
||||
`bridge.rust.helper_stdout_message` lines also land in the trace).
|
||||
2. Connect to the host that reproduces this (the aws-celery host).
|
||||
3. Wait 5 minutes — long enough for at least 2 timeout cycles.
|
||||
4. From `<Sublime cache>/Sessions/logs/debug-trace.log`, paste the
|
||||
block of lines between two consecutive `mirror_queue.enqueue
|
||||
task=work` events that wrap a `bridge.request_timeout`. Should
|
||||
include all `bridge.rust.*` lines in between.
|
||||
|
||||
What to look for in the paste:
|
||||
|
||||
- Any `bridge.rust.helper_stdout_eof` — helper closed stdout before
|
||||
responding (suggests session_helper died on the remote, possibly
|
||||
out-of-memory on the deep walk).
|
||||
- Any `bridge.rust.helper_stdout_message` with abnormally long line
|
||||
payloads — large response chunks that may exceed channel buffer
|
||||
and stall the dispatcher.
|
||||
- The final `bridge.request_done` (if any) for the `mirror-sync` id
|
||||
before the timeout fires.
|
||||
|
||||
Also: on the remote, while a deep sync is in flight, run
|
||||
`ps -ef | grep session_helper` and paste output. We want to see if
|
||||
the helper is actually busy (CPU > 0) or idle (already responded but
|
||||
the response is stuck somewhere local).
|
||||
|
||||
### B2. Hover absolute remote path → does not open in Sublime
|
||||
|
||||
`ls -la /etc` (or any deep path) in a Terminus pane.
|
||||
|
||||
- [ ] Hover an absolute path line. Underline appears? **yes / no**
|
||||
- [ ] Cmd+click. Sublime opens the file? **yes / no — what happens
|
||||
instead** (about:blank? nothing at all? error in console?)
|
||||
|
||||
If nothing opens: paste any line from `debug-trace.log` matching
|
||||
`terminal_link.click` or `bridge.request` that lands within ~3 seconds
|
||||
of the click.
|
||||
|
||||
### B3. `Sessions: Open Remote Jupyter` — silent
|
||||
|
||||
Palette → "Sessions: Open Remote Jupyter".
|
||||
|
||||
- [ ] Browser tab opens? **yes / no**.
|
||||
- [ ] `Sessions: Stop Remote Jupyter` available afterward?
|
||||
- [ ] Paste lines from `debug-trace.log` matching `jupyter` and
|
||||
`queue.dequeue task=jupyter_open`. The previous capture showed
|
||||
`queue.done elapsed_ms=27748` but no visible browser tab —
|
||||
need to know whether the launch URL is being constructed at
|
||||
all, or constructed but not opened.
|
||||
|
||||
### B4. `Sessions: New Agent Session` quick panel
|
||||
|
||||
If A1 succeeds, this is likely also fine. If A1 still errors, A1 is
|
||||
the upstream cause of "역시 아무 것도 뜨지 않음".
|
||||
|
||||
Confirm: after A1 succeeds, can you run `Sessions: New Agent Session`
|
||||
again and pick `OpenAI Codex CLI (remote)` to start a second pair?
|
||||
|
||||
---
|
||||
|
||||
## What to send back
|
||||
|
||||
For each step under §A, mark **PASS / FAIL** + behavior summary.
|
||||
|
||||
For each step under §B, paste:
|
||||
- the requested log fragments (B1, B3 especially)
|
||||
- a short observation note ("nothing opens", "browser opens to wrong
|
||||
URL X", etc.)
|
||||
|
||||
Optional: attach the full debug-trace.log slice from the start of the
|
||||
session to the end of the test pass — useful for cross-correlation
|
||||
when individual steps look fine but downstream behavior breaks.
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "sessions-sublime"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
description = "Sublime-facing Python code for Sessions."
|
||||
requires-python = ">=3.8"
|
||||
license = {text = "MIT"}
|
||||
|
||||
12
rust/Cargo.lock
generated
12
rust/Cargo.lock
generated
@@ -221,7 +221,7 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "local_bridge"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"glob",
|
||||
@@ -432,7 +432,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "session_helper"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"notify",
|
||||
@@ -443,7 +443,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "session_protocol"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"serde",
|
||||
@@ -452,14 +452,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sessions_askpass"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
dependencies = [
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sessions_native"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"session_protocol",
|
||||
@@ -770,7 +770,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "workspace_identity"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
|
||||
@@ -12,7 +12,7 @@ resolver = "2"
|
||||
[workspace.package]
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
version = "0.7.24"
|
||||
version = "0.7.25"
|
||||
authors = ["Myeongseon Choi <key262yek@gmail.com>"]
|
||||
repository = "https://git.teahaven.kr/sublime-rs/sessions"
|
||||
homepage = "https://git.teahaven.kr/sublime-rs/sessions"
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
//! Validated JSON payloads from a remote agent for client-side display (v0).
|
||||
//!
|
||||
//! Schema v1: whitespace-only `title` / `unified_diff` rejected; `schema_version`
|
||||
//! must be a JSON **integer** (not bool/float). The Sublime package calls this
|
||||
//! logic only via `local_bridge parse-agent-editor-envelope`—see
|
||||
//! `planning/PYTHON_RUST_BOUNDARY.md` (single source of truth).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// `sessions.agent_editor_preview`
|
||||
pub const AGENT_EDITOR_PREVIEW_KIND: &str = "sessions.agent_editor_preview";
|
||||
|
||||
/// Supported envelope schema version.
|
||||
pub const SUPPORTED_SCHEMA_VERSION: i64 = 1;
|
||||
|
||||
/// Pre-rendered text for editor-side preview (diff computed remotely).
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct AgentEditorPayload {
|
||||
pub kind: String,
|
||||
pub schema_version: i32,
|
||||
pub title: String,
|
||||
pub unified_diff: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub target_remote_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Parse a JSON object into [`AgentEditorPayload`] or return `None` if invalid.
|
||||
pub fn parse_agent_editor_payload(raw: &Value) -> Option<AgentEditorPayload> {
|
||||
let map = raw.as_object()?;
|
||||
let kind = map.get("kind")?.as_str()?;
|
||||
if kind != AGENT_EDITOR_PREVIEW_KIND {
|
||||
return None;
|
||||
}
|
||||
let version = map.get("schema_version")?;
|
||||
let schema_version = match version {
|
||||
Value::Number(n) => {
|
||||
if !n.is_i64() {
|
||||
return None;
|
||||
}
|
||||
let i = n.as_i64()?;
|
||||
if i != SUPPORTED_SCHEMA_VERSION {
|
||||
return None;
|
||||
}
|
||||
i32::try_from(i).ok()?
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
let title = map.get("title")?.as_str()?;
|
||||
let unified_diff = map.get("unified_diff")?.as_str()?;
|
||||
if title.trim().is_empty() || unified_diff.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let path = match map.get("target_remote_path") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(s)) => Some(s.clone()),
|
||||
Some(_) => return None,
|
||||
};
|
||||
Some(AgentEditorPayload {
|
||||
kind: kind.to_string(),
|
||||
schema_version,
|
||||
title: title.to_string(),
|
||||
unified_diff: unified_diff.to_string(),
|
||||
target_remote_path: path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse remote command stdout into a payload, or a short error reason.
|
||||
///
|
||||
/// Accepts either a single JSON object or extra lines where the **last** non-empty
|
||||
/// line is the object (prefix log lines).
|
||||
pub fn parse_agent_editor_envelope_from_stdout(
|
||||
text: &str,
|
||||
) -> (Option<AgentEditorPayload>, Option<String>) {
|
||||
let stripped = text.trim();
|
||||
if stripped.is_empty() {
|
||||
return (
|
||||
None,
|
||||
Some("Remote agent stdout was empty (expected one JSON object).".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
let mut first_decode_error: Option<String> = None;
|
||||
let first: Value = match serde_json::from_str(stripped) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
first_decode_error = Some(e.to_string());
|
||||
Value::Null
|
||||
}
|
||||
};
|
||||
|
||||
if let Value::Object(_) = &first
|
||||
&& let Some(parsed) = parse_agent_editor_payload(&first)
|
||||
{
|
||||
return (Some(parsed), None);
|
||||
}
|
||||
|
||||
let lines: Vec<&str> = stripped
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.collect();
|
||||
|
||||
if lines.is_empty() {
|
||||
let msg = first_decode_error
|
||||
.map(|e| format!("JSON decode failed: {e}"))
|
||||
.unwrap_or_else(|| "JSON decode failed: unknown".to_string());
|
||||
return (None, Some(msg));
|
||||
}
|
||||
|
||||
let last: Value = match serde_json::from_str(lines[lines.len() - 1]) {
|
||||
Ok(v) => v,
|
||||
Err(e) => return (None, Some(format!("JSON decode failed: {e}"))),
|
||||
};
|
||||
|
||||
if let Some(parsed) = parse_agent_editor_payload(&last) {
|
||||
return (Some(parsed), None);
|
||||
}
|
||||
|
||||
if !last.is_object() {
|
||||
return (
|
||||
None,
|
||||
Some("JSON root must be an object (mapping).".to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
(
|
||||
None,
|
||||
Some(format!(
|
||||
"Schema validation failed: expected kind {AGENT_EDITOR_PREVIEW_KIND:?}, schema_version \
|
||||
{SUPPORTED_SCHEMA_VERSION}, non-empty strings title and unified_diff, optional string \
|
||||
target_remote_path."
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn round_trip() {
|
||||
let raw = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": SUPPORTED_SCHEMA_VERSION,
|
||||
"title": "Preview",
|
||||
"unified_diff": "--- a/x\n+++ b/x\n",
|
||||
"target_remote_path": "/srv/app/readme.md",
|
||||
});
|
||||
let parsed = parse_agent_editor_payload(&raw);
|
||||
assert_eq!(
|
||||
parsed,
|
||||
Some(AgentEditorPayload {
|
||||
kind: AGENT_EDITOR_PREVIEW_KIND.to_string(),
|
||||
schema_version: 1,
|
||||
title: "Preview".to_string(),
|
||||
unified_diff: "--- a/x\n+++ b/x\n".to_string(),
|
||||
target_remote_path: Some("/srv/app/readme.md".to_string()),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_path_omitted() {
|
||||
let raw = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": SUPPORTED_SCHEMA_VERSION,
|
||||
"title": "t",
|
||||
"unified_diff": "d",
|
||||
});
|
||||
let parsed = parse_agent_editor_payload(&raw);
|
||||
assert!(
|
||||
matches!(&parsed, Some(p) if p.target_remote_path.is_none()),
|
||||
"expected Some(payload) without target_remote_path, got {:?}",
|
||||
parsed
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_wrong_kind() {
|
||||
let raw = json!({
|
||||
"kind": "other",
|
||||
"schema_version": SUPPORTED_SCHEMA_VERSION,
|
||||
"title": "t",
|
||||
"unified_diff": "d",
|
||||
});
|
||||
assert!(parse_agent_editor_payload(&raw).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bad_schema() {
|
||||
let raw = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": 99,
|
||||
"title": "t",
|
||||
"unified_diff": "d",
|
||||
});
|
||||
assert!(parse_agent_editor_payload(&raw).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_object() {
|
||||
assert!(parse_agent_editor_payload(&json!([])).is_none());
|
||||
assert!(parse_agent_editor_payload(&json!("x")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_bool_schema() {
|
||||
let raw = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": true,
|
||||
"title": "t",
|
||||
"unified_diff": "d",
|
||||
});
|
||||
assert!(parse_agent_editor_payload(&raw).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_whitespace_title_or_diff() {
|
||||
let raw = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": SUPPORTED_SCHEMA_VERSION,
|
||||
"title": " ",
|
||||
"unified_diff": "x",
|
||||
});
|
||||
assert!(parse_agent_editor_payload(&raw).is_none());
|
||||
let raw = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": SUPPORTED_SCHEMA_VERSION,
|
||||
"title": "ok",
|
||||
"unified_diff": "\n\t\n",
|
||||
});
|
||||
assert!(parse_agent_editor_payload(&raw).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_not_json() {
|
||||
let (p, e) = parse_agent_editor_envelope_from_stdout("not json");
|
||||
assert!(p.is_none());
|
||||
assert!(e.is_some(), "expected err Some");
|
||||
if let Some(err) = e {
|
||||
assert!(err.contains("JSON decode failed"), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_schema_failed_message() {
|
||||
let raw = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": 99,
|
||||
"title": "t",
|
||||
"unified_diff": "d",
|
||||
});
|
||||
let (p, e) = parse_agent_editor_envelope_from_stdout(&raw.to_string());
|
||||
assert!(p.is_none());
|
||||
assert!(e.is_some(), "expected err Some");
|
||||
if let Some(err) = e {
|
||||
assert!(err.contains("Schema validation failed"), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_last_line_wins_with_prefix_logs() {
|
||||
let body = json!({
|
||||
"kind": AGENT_EDITOR_PREVIEW_KIND,
|
||||
"schema_version": SUPPORTED_SCHEMA_VERSION,
|
||||
"title": "ok",
|
||||
"unified_diff": "diff",
|
||||
});
|
||||
let text = format!("noise line\n{}", body);
|
||||
let (p, e) = parse_agent_editor_envelope_from_stdout(&text);
|
||||
assert!(e.is_none());
|
||||
assert!(p.is_some(), "expected payload Some");
|
||||
if let Some(payload) = p {
|
||||
assert_eq!(payload.title, "ok");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
//! - validate the helper handshake
|
||||
//! - forward requests and return responses/errors
|
||||
//! - mirror remote directory trees into a local cache ([`remote_cache_mirror`])
|
||||
//! - parse agent→editor JSON envelopes ([`agent_remote_payload`])
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
@@ -18,7 +17,6 @@
|
||||
//! assert!(default_remote_helper_path().contains("session_helper"));
|
||||
//! ```
|
||||
|
||||
pub mod agent_remote_payload;
|
||||
pub mod diag_log;
|
||||
pub mod helper_command;
|
||||
pub mod lsp_uri_rewrite;
|
||||
|
||||
@@ -11,8 +11,7 @@
|
||||
//!
|
||||
//! ``main`` only handles version-banner short-circuiting and the top-level
|
||||
//! mode switch (``lsp-stdio`` subcommand vs. forwarder); ``run`` then dispatches
|
||||
//! between ``parse-agent-editor-envelope``, persistent mode, and one-shot
|
||||
//! request mode.
|
||||
//! between persistent mode and one-shot request mode.
|
||||
|
||||
mod cli;
|
||||
mod lsp_stdio;
|
||||
@@ -23,7 +22,6 @@ use crate::cli::BridgeCliArgs;
|
||||
use crate::lsp_stdio::run_lsp_stdio;
|
||||
use crate::persistent::run_persistent;
|
||||
use local_bridge::{BridgeCliOutput, BridgeRunError, run_request_over_ssh};
|
||||
use serde_json::json;
|
||||
use session_protocol::RequestEnvelope;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -79,9 +77,6 @@ fn main() {
|
||||
}
|
||||
|
||||
fn run(args: &[String]) -> Result<BridgeCliOutput, BridgeRunError> {
|
||||
if args.first().map(String::as_str) == Some("parse-agent-editor-envelope") {
|
||||
return run_parse_agent_editor_envelope();
|
||||
}
|
||||
if args.iter().any(|arg| arg == "--persistent") {
|
||||
run_persistent(args)?;
|
||||
return Ok(BridgeCliOutput {
|
||||
@@ -137,27 +132,6 @@ pub(crate) fn write_bridge_output(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_parse_agent_editor_envelope() -> Result<BridgeCliOutput, BridgeRunError> {
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin().read_to_string(&mut buffer)?;
|
||||
let (payload, error) =
|
||||
local_bridge::agent_remote_payload::parse_agent_editor_envelope_from_stdout(&buffer);
|
||||
let payload_json = payload
|
||||
.as_ref()
|
||||
.map(serde_json::to_value)
|
||||
.transpose()
|
||||
.map_err(BridgeRunError::Json)?;
|
||||
Ok(BridgeCliOutput {
|
||||
ok: true,
|
||||
id: None,
|
||||
result: Some(json!({
|
||||
"agent_editor_payload": payload_json,
|
||||
"agent_editor_error": error,
|
||||
})),
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn read_stdin() -> Result<String, BridgeRunError> {
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin().read_to_string(&mut buffer)?;
|
||||
|
||||
@@ -167,14 +167,26 @@
|
||||
"sessions_remote_terminal_shell": "bash -il",
|
||||
|
||||
// After saving a mirrored workspace .py file, run the remote diagnostics pipeline
|
||||
// (ruff + pyright by default). See planning/REMOTE_DEV_MVP_LSP.md.
|
||||
// (ruff + pyright by default).
|
||||
//
|
||||
// Three keys in this group — ``sessions_remote_python_auto_diagnostics_on_save``,
|
||||
// ``sessions_remote_python_auto_diagnostics_on_open``, and
|
||||
// ``sessions_remote_python_tool_pipeline`` — follow LSP-style precedence:
|
||||
// package default → ``Packages/User/Sessions.sublime-settings`` → the
|
||||
// ``.sublime-project`` ``"settings"`` block (per-workspace override). Drop
|
||||
// ``"sessions_remote_python_auto_diagnostics_on_save": true`` into a
|
||||
// workspace's ``.sublime-project`` to enable on-save lint/typecheck just for
|
||||
// that project without flipping the global default.
|
||||
"sessions_remote_python_auto_diagnostics_on_save": false,
|
||||
|
||||
// When true, run the same pipeline when a .py buffer under the cache is focused
|
||||
// (debounced ~1.5s per view).
|
||||
// (debounced ~1.5s per view). Same project-level override semantics as
|
||||
// ``sessions_remote_python_auto_diagnostics_on_save``.
|
||||
"sessions_remote_python_auto_diagnostics_on_open": false,
|
||||
|
||||
// Ordered steps: "ruff_lint", "pyright_check" (each runs on the remote host).
|
||||
// Per-project override allowed via the ``.sublime-project`` ``"settings"``
|
||||
// block (LSP-style precedence).
|
||||
"sessions_remote_python_tool_pipeline": ["ruff_lint", "pyright_check"],
|
||||
|
||||
// Phase 6.3 channel-based code-server registry. New servers should be added here
|
||||
@@ -232,12 +244,5 @@
|
||||
// Each entry runs through bridge exec/once:
|
||||
// install_argv -> probe_argv -> (status)
|
||||
// remove_argv -> probe_argv -> (status)
|
||||
//
|
||||
// frozen-experimental: the bundled ``kind="agent"`` entries (``tmux``,
|
||||
// ``claude-code``, ``codex-cli``) are leftovers from the dropped Track D
|
||||
// (in-Sublime agent integration via tmux, dropped 2026-04-27). They stay
|
||||
// installable so existing setups keep working, but the recommended path
|
||||
// is to run agents in an external terminal. Use the LSP entries
|
||||
// (Pyright, Ruff, rust-analyzer) as the supported surface.
|
||||
"sessions_remote_extensions": []
|
||||
}
|
||||
|
||||
@@ -2704,7 +2704,7 @@ def _managed_extension_project_client_keys_for_spec(
|
||||
) -> Tuple[str, ...]:
|
||||
"""Return managed + legacy project LSP client keys for one catalog spec id.
|
||||
|
||||
Non-LSP kinds (``jupyter``) have no Sublime-LSP client rows to manage, so
|
||||
Non-LSP kinds (``debugger``) have no Sublime-LSP client rows to manage, so
|
||||
we return an empty tuple for them; only LSP-kind catalog matches contribute
|
||||
client keys.
|
||||
"""
|
||||
@@ -6960,11 +6960,11 @@ class SessionsOpenRemoteTerminalCommand(sublime_plugin.WindowCommand):
|
||||
|
||||
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. ``auto_close=False`` so the pane survives
|
||||
no session multiplexing. ``auto_close=False`` so the pane survives
|
||||
an unexpected shell exit — without it a flash-close hides whatever
|
||||
error the shell printed on its way out, which is the only signal the
|
||||
user has to diagnose dotfile breakage or remote-root vanish. For
|
||||
long-running or tmux-heavy workflows the user is expected to open
|
||||
long-running workflows the user is expected to open
|
||||
their own external terminal.
|
||||
"""
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import time
|
||||
import webbrowser
|
||||
from dataclasses import replace
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Dict, List, Optional, Sequence, Tuple
|
||||
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple
|
||||
|
||||
from . import commands as _root
|
||||
from .connect_preflight import ConnectStatus
|
||||
@@ -69,7 +69,11 @@ from .remote_tool_wiring import (
|
||||
build_python_lsp_source_action_tool_execution_request,
|
||||
build_requests_for_python_tool_pipeline,
|
||||
)
|
||||
from .settings_model import SessionsSettings, load_sessions_settings_from_sublime
|
||||
from .settings_model import (
|
||||
SessionsSettings,
|
||||
load_sessions_settings_from_sublime,
|
||||
normalize_remote_python_tool_pipeline,
|
||||
)
|
||||
|
||||
_LOG = logging.getLogger("sessions.commands_python_pipeline")
|
||||
|
||||
@@ -114,15 +118,65 @@ _DEBUG_PANEL_NAME = "sessions_debug_setup"
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _effective_sessions_settings_for_remote_python() -> SessionsSettings:
|
||||
"""Merge default settings with ``Sessions.sublime-settings`` pipeline keys."""
|
||||
def _project_settings_block_for_window(window: Optional[object]) -> Mapping[str, Any]:
|
||||
"""Return ``window.project_data()['settings']`` if structurally valid, else ``{}``.
|
||||
|
||||
Mirrors the safety pattern already used elsewhere in this module: tolerate
|
||||
a missing ``project_data`` callable, ``None`` payloads, and any non-mapping
|
||||
value at either level instead of raising.
|
||||
"""
|
||||
if window is None:
|
||||
return {}
|
||||
project_data_fn = getattr(window, "project_data", None)
|
||||
if not callable(project_data_fn):
|
||||
return {}
|
||||
project_data = project_data_fn()
|
||||
if not isinstance(project_data, Mapping):
|
||||
return {}
|
||||
settings = project_data.get("settings")
|
||||
if not isinstance(settings, Mapping):
|
||||
return {}
|
||||
return settings
|
||||
|
||||
|
||||
def _effective_sessions_settings_for_remote_python(
|
||||
window: Optional[object] = None,
|
||||
) -> SessionsSettings:
|
||||
"""Merge default → user → project settings for the on-save/on-open pipeline.
|
||||
|
||||
Reads ``Packages/Sessions/Sessions.sublime-settings`` (default) merged
|
||||
with ``Packages/User/Sessions.sublime-settings`` (user) via
|
||||
``load_sessions_settings_from_sublime``, then overlays the active
|
||||
``.sublime-project`` ``"settings"`` block when ``window`` is given.
|
||||
Mirrors how Sublime LSP layers per-project overrides on top of user
|
||||
settings — see ``planning/REVIEW_v0_6_4_DISTRIBUTION_PLAN.md`` for the
|
||||
LSP-style precedence rationale.
|
||||
"""
|
||||
base = SessionsSettings()
|
||||
plug = load_sessions_settings_from_sublime()
|
||||
on_save = plug.remote_python_auto_diagnostics_on_save
|
||||
on_open = plug.remote_python_auto_diagnostics_on_open
|
||||
pipeline = plug.remote_python_tool_pipeline
|
||||
project_settings = _project_settings_block_for_window(window)
|
||||
project_on_save = project_settings.get(
|
||||
"sessions_remote_python_auto_diagnostics_on_save"
|
||||
)
|
||||
if isinstance(project_on_save, bool):
|
||||
on_save = project_on_save
|
||||
project_on_open = project_settings.get(
|
||||
"sessions_remote_python_auto_diagnostics_on_open"
|
||||
)
|
||||
if isinstance(project_on_open, bool):
|
||||
on_open = project_on_open
|
||||
if "sessions_remote_python_tool_pipeline" in project_settings:
|
||||
pipeline = normalize_remote_python_tool_pipeline(
|
||||
project_settings.get("sessions_remote_python_tool_pipeline")
|
||||
)
|
||||
return replace(
|
||||
base,
|
||||
remote_python_auto_diagnostics_on_save=plug.remote_python_auto_diagnostics_on_save,
|
||||
remote_python_auto_diagnostics_on_open=plug.remote_python_auto_diagnostics_on_open,
|
||||
remote_python_tool_pipeline=plug.remote_python_tool_pipeline,
|
||||
remote_python_auto_diagnostics_on_save=on_save,
|
||||
remote_python_auto_diagnostics_on_open=on_open,
|
||||
remote_python_tool_pipeline=pipeline,
|
||||
)
|
||||
|
||||
|
||||
@@ -166,7 +220,7 @@ def _collect_remote_python_pipeline_results(
|
||||
"""
|
||||
if not remote_path.endswith(".py"):
|
||||
return ()
|
||||
merged = _effective_sessions_settings_for_remote_python()
|
||||
merged = _effective_sessions_settings_for_remote_python(window)
|
||||
if not merged.remote_python_auto_diagnostics_on_save:
|
||||
return ()
|
||||
if post_save_view is not None:
|
||||
@@ -301,7 +355,7 @@ def _schedule_remote_python_pipeline(
|
||||
trigger: RunTrigger,
|
||||
) -> None:
|
||||
"""Kick off the remote diagnostics pipeline when targets are valid."""
|
||||
merged = _effective_sessions_settings_for_remote_python()
|
||||
merged = _effective_sessions_settings_for_remote_python(window)
|
||||
targets = _remote_python_pipeline_targets(view, window, merged)
|
||||
if targets is None:
|
||||
return
|
||||
@@ -329,7 +383,7 @@ def _maybe_schedule_remote_python_pipeline_after_cache_push(
|
||||
"""
|
||||
if not remote_path.endswith(".py"):
|
||||
return
|
||||
merged = _effective_sessions_settings_for_remote_python()
|
||||
merged = _effective_sessions_settings_for_remote_python(window)
|
||||
if not merged.remote_python_auto_diagnostics_on_save:
|
||||
return
|
||||
if post_save_view is not None:
|
||||
@@ -550,26 +604,26 @@ class SessionsRemotePythonPipelineListener(sublime_plugin.EventListener):
|
||||
|
||||
def on_post_save(self, view) -> None:
|
||||
"""Lint/typecheck after save when enabled in ``Sessions.sublime-settings``."""
|
||||
merged = _effective_sessions_settings_for_remote_python()
|
||||
if not merged.remote_python_auto_diagnostics_on_save:
|
||||
return
|
||||
window_fn = getattr(view, "window", None)
|
||||
window = window_fn() if callable(window_fn) else None
|
||||
if window is None:
|
||||
return
|
||||
merged = _effective_sessions_settings_for_remote_python(window)
|
||||
if not merged.remote_python_auto_diagnostics_on_save:
|
||||
return
|
||||
if _root._remote_save_target_after_local_save(view, window) is not None:
|
||||
return
|
||||
_schedule_remote_python_pipeline(view, window, RunTrigger.ON_SAVE)
|
||||
|
||||
def on_activated_async(self, view) -> None:
|
||||
"""Optionally run the pipeline when a ``.py`` cache buffer is focused."""
|
||||
merged = _effective_sessions_settings_for_remote_python()
|
||||
if not merged.remote_python_auto_diagnostics_on_open:
|
||||
return
|
||||
window_fn = getattr(view, "window", None)
|
||||
window = window_fn() if callable(window_fn) else None
|
||||
if window is None:
|
||||
return
|
||||
merged = _effective_sessions_settings_for_remote_python(window)
|
||||
if not merged.remote_python_auto_diagnostics_on_open:
|
||||
return
|
||||
# Same rationale as the version-probe listener: don't spawn the
|
||||
# bridge on a restored project window before the user has
|
||||
# explicitly reconnected.
|
||||
|
||||
@@ -8,16 +8,6 @@ Each :class:`ManagedRemoteExtensionCatalogEntry` bundles:
|
||||
Add a new built-in extension by appending one frozen row to
|
||||
:data:`BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG` and wiring any host-specific hints in
|
||||
``sessions.commands`` if needed.
|
||||
|
||||
frozen-experimental — agent surface
|
||||
-----------------------------------
|
||||
``kind == "agent"`` rows (``tmux``, ``claude-code``, ``codex-cli``) are leftovers
|
||||
from the v0.6.0–v0.6.7 in-Sublime agent direction. Track D (in-Sublime agent
|
||||
integration via tmux) was dropped 2026-04-27 — agents now run in an external
|
||||
terminal that the user manages outside Sublime. The catalog entries stay in the
|
||||
tree so existing installs keep working, but **do not extend or polish them**;
|
||||
add new agent entries only after coordinating with the maintainers. See
|
||||
``planning/BACKLOG.md`` § "Track D" and ``planning/SHIPPED.md`` v0.6.7.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -90,48 +80,6 @@ export PATH="$HOME/.cargo/bin:$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||
rustup component remove rust-analyzer 2>/dev/null || true
|
||||
exit 0
|
||||
"""
|
||||
_BUILTIN_BASH_JUPYTER_INSTALL = """\
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
GET_PIP_URL=https://bootstrap.pypa.io/get-pip.py
|
||||
set -e
|
||||
PKGS="jupyterlab ipykernel"
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "Sessions: python3 required to install Jupyter Lab." >&2
|
||||
exit 127
|
||||
fi
|
||||
if python3 -m pip install --user $PKGS; then exit 0; fi
|
||||
if command -v pip3 >/dev/null 2>&1 && pip3 install --user $PKGS; then
|
||||
exit 0
|
||||
fi
|
||||
if command -v pip >/dev/null 2>&1 && pip install --user $PKGS; then
|
||||
exit 0
|
||||
fi
|
||||
if python3 -m ensurepip --user --default-pip >/dev/null 2>&1 \\
|
||||
&& python3 -m pip install --user $PKGS; then exit 0; fi
|
||||
if command -v curl >/dev/null 2>&1 && curl -fsSL "$GET_PIP_URL" \\
|
||||
| python3 - --user >/dev/null 2>&1 \\
|
||||
&& python3 -m pip install --user $PKGS; then exit 0; fi
|
||||
echo "Sessions: could not install Jupyter Lab." >&2
|
||||
exit 1
|
||||
"""
|
||||
_BUILTIN_BASH_JUPYTER_REMOVE = """\
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
PKGS="jupyterlab jupyter_server jupyterlab_server"
|
||||
python3 -m pip uninstall -y $PKGS 2>/dev/null || true
|
||||
if command -v pip3 >/dev/null 2>&1; then
|
||||
pip3 uninstall -y $PKGS 2>/dev/null || true
|
||||
fi
|
||||
if command -v pip >/dev/null 2>&1; then
|
||||
pip uninstall -y $PKGS 2>/dev/null || true
|
||||
fi
|
||||
exit 0
|
||||
"""
|
||||
_BUILTIN_BASH_JUPYTER_PROBE = """\
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
set -e
|
||||
command -v jupyter >/dev/null 2>&1 || { echo "jupyter not on PATH" >&2; exit 127; }
|
||||
jupyter lab --version
|
||||
"""
|
||||
_BUILTIN_BASH_DEBUGPY_INSTALL = """\
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
set -e
|
||||
@@ -154,94 +102,6 @@ if [ -z "{ACTIVE_PYTHON}" ]; then
|
||||
fi
|
||||
"{ACTIVE_PYTHON}" -c "import debugpy, sys; print(debugpy.__version__)"
|
||||
"""
|
||||
_BUILTIN_BASH_TMUX_INSTALL = """\
|
||||
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||
if command -v tmux >/dev/null 2>&1; then
|
||||
tmux -V
|
||||
exit 0
|
||||
fi
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get update && sudo apt-get install -y tmux
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
sudo dnf install -y tmux
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
sudo yum install -y tmux
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
sudo pacman -S --noconfirm tmux
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
brew install tmux
|
||||
else
|
||||
echo "Sessions: no supported package manager found (apt/dnf/yum/pacman/brew)." >&2
|
||||
echo "Install tmux manually; see https://github.com/tmux/tmux/wiki/Installing" >&2
|
||||
exit 127
|
||||
fi
|
||||
"""
|
||||
_BUILTIN_BASH_TMUX_REMOVE = """\
|
||||
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
sudo apt-get remove -y tmux 2>/dev/null || true
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
sudo dnf remove -y tmux 2>/dev/null || true
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
sudo yum remove -y tmux 2>/dev/null || true
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
sudo pacman -R --noconfirm tmux 2>/dev/null || true
|
||||
elif command -v brew >/dev/null 2>&1; then
|
||||
brew uninstall tmux 2>/dev/null || true
|
||||
fi
|
||||
exit 0
|
||||
"""
|
||||
_BUILTIN_BASH_TMUX_PROBE = """\
|
||||
export PATH="$HOME/.local/bin:/usr/local/bin:$PATH"
|
||||
command -v tmux >/dev/null 2>&1 || { echo "tmux not on PATH" >&2; exit 127; }
|
||||
tmux -V
|
||||
"""
|
||||
_BUILTIN_BASH_CLAUDE_INSTALL = """\
|
||||
export PATH="$HOME/.claude/bin:$HOME/.local/bin:$PATH"
|
||||
set -e
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "Sessions: curl is required to install Claude Code CLI." >&2
|
||||
echo "See https://docs.claude.com/en/docs/claude-code/setup for manual install." >&2
|
||||
exit 127
|
||||
fi
|
||||
if ! curl -fsSL https://claude.ai/install.sh | bash; then
|
||||
echo "Sessions: Claude Code install script failed (URL unreachable?)." >&2
|
||||
echo "See https://docs.claude.com/en/docs/claude-code/setup for manual install." >&2
|
||||
exit 1
|
||||
fi
|
||||
export PATH="$HOME/.claude/bin:$PATH"
|
||||
command -v claude >/dev/null 2>&1 && claude --version
|
||||
"""
|
||||
_BUILTIN_BASH_CLAUDE_REMOVE = """\
|
||||
rm -rf "$HOME/.claude/bin"
|
||||
exit 0
|
||||
"""
|
||||
_BUILTIN_BASH_CLAUDE_PROBE = """\
|
||||
export PATH="$HOME/.claude/bin:$HOME/.local/bin:$PATH"
|
||||
command -v claude >/dev/null 2>&1 || { echo "claude not on PATH" >&2; exit 127; }
|
||||
claude --version
|
||||
"""
|
||||
_BUILTIN_BASH_CODEX_INSTALL = """\
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
set -e
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "Sessions: npm is required to install the OpenAI Codex CLI." >&2
|
||||
echo "Install Node.js / npm first (see https://nodejs.org/)." >&2
|
||||
exit 127
|
||||
fi
|
||||
npm install -g @openai/codex
|
||||
command -v codex >/dev/null 2>&1 && codex --version
|
||||
"""
|
||||
_BUILTIN_BASH_CODEX_REMOVE = """\
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
command -v npm >/dev/null 2>&1 && npm uninstall -g @openai/codex 2>/dev/null || true
|
||||
exit 0
|
||||
"""
|
||||
_BUILTIN_BASH_CODEX_PROBE = """\
|
||||
export PATH="$HOME/.local/bin:$PATH"
|
||||
command -v codex >/dev/null 2>&1 || { echo "codex not on PATH" >&2; exit 127; }
|
||||
codex --version
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -307,15 +167,6 @@ BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG: Tuple[
|
||||
remote_spawn_argv=("rust-analyzer",),
|
||||
sublime_selector="source.rust",
|
||||
),
|
||||
ManagedRemoteExtensionCatalogEntry(
|
||||
install_catalog_id="jupyterlab",
|
||||
install_label="Jupyter Lab (remote)",
|
||||
install_argv=("bash", "-lc", _BUILTIN_BASH_JUPYTER_INSTALL),
|
||||
remove_argv=("bash", "-lc", _BUILTIN_BASH_JUPYTER_REMOVE),
|
||||
probe_argv=("bash", "-lc", _BUILTIN_BASH_JUPYTER_PROBE),
|
||||
install_cwd=None,
|
||||
kind="jupyter",
|
||||
),
|
||||
ManagedRemoteExtensionCatalogEntry(
|
||||
install_catalog_id="debugpy",
|
||||
install_label="debugpy (remote Python debugger)",
|
||||
@@ -328,31 +179,4 @@ BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG: Tuple[
|
||||
install_cwd=None,
|
||||
kind="debugger",
|
||||
),
|
||||
ManagedRemoteExtensionCatalogEntry(
|
||||
install_catalog_id="tmux",
|
||||
install_label="tmux (agent session prerequisite)",
|
||||
install_argv=("bash", "-lc", _BUILTIN_BASH_TMUX_INSTALL),
|
||||
remove_argv=("bash", "-lc", _BUILTIN_BASH_TMUX_REMOVE),
|
||||
probe_argv=("bash", "-lc", _BUILTIN_BASH_TMUX_PROBE),
|
||||
install_cwd=None,
|
||||
kind="agent",
|
||||
),
|
||||
ManagedRemoteExtensionCatalogEntry(
|
||||
install_catalog_id="claude-code",
|
||||
install_label="Claude Code CLI (remote)",
|
||||
install_argv=("bash", "-lc", _BUILTIN_BASH_CLAUDE_INSTALL),
|
||||
remove_argv=("bash", "-lc", _BUILTIN_BASH_CLAUDE_REMOVE),
|
||||
probe_argv=("bash", "-lc", _BUILTIN_BASH_CLAUDE_PROBE),
|
||||
install_cwd=None,
|
||||
kind="agent",
|
||||
),
|
||||
ManagedRemoteExtensionCatalogEntry(
|
||||
install_catalog_id="codex-cli",
|
||||
install_label="OpenAI Codex CLI (remote)",
|
||||
install_argv=("bash", "-lc", _BUILTIN_BASH_CODEX_INSTALL),
|
||||
remove_argv=("bash", "-lc", _BUILTIN_BASH_CODEX_REMOVE),
|
||||
probe_argv=("bash", "-lc", _BUILTIN_BASH_CODEX_PROBE),
|
||||
install_cwd=None,
|
||||
kind="agent",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -262,7 +262,7 @@ class MarimoSessionManager:
|
||||
"""
|
||||
self._ssh = ssh_command_builder or _default_ssh_command_builder
|
||||
# Default run/popen wrap subprocess with CREATE_NO_WINDOW on Windows
|
||||
# so the underlying ssh / tmux children don't pop a console window
|
||||
# so the underlying ssh children don't pop a console window
|
||||
# every time the plugin talks to the remote. Injected overrides
|
||||
# (unit tests) retain their exact behaviour — the helper returns an
|
||||
# empty kwargs dict on non-Windows, so the wrapper is a no-op there.
|
||||
@@ -447,8 +447,7 @@ class MarimoSessionManager:
|
||||
)
|
||||
# Pass ``bash -lc <script>`` as a single SSH-side argument so the
|
||||
# remote login shell doesn't tokenise the script and pass only the
|
||||
# leading word to ``bash -lc``. (See jupyter_hosting.py for the full
|
||||
# postmortem of the prior tokenisation bug.)
|
||||
# leading word to ``bash -lc``.
|
||||
argv = list(self._ssh(host_alias)) + [
|
||||
"bash -lc " + shlex.quote(remote_script),
|
||||
]
|
||||
|
||||
@@ -430,6 +430,96 @@ def test_effective_sessions_settings_for_remote_python(
|
||||
assert isinstance(settings, SessionsSettings)
|
||||
|
||||
|
||||
def test_effective_settings_project_overrides_user_for_on_save(
|
||||
sublime_settings,
|
||||
) -> None:
|
||||
"""``.sublime-project`` ``settings`` block beats user setting (LSP-style)."""
|
||||
sublime_settings({"sessions_remote_python_auto_diagnostics_on_save": False})
|
||||
window = FakeWindow(
|
||||
project_data={
|
||||
"settings": {
|
||||
"sessions_remote_python_auto_diagnostics_on_save": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
settings = commands._effective_sessions_settings_for_remote_python(window)
|
||||
|
||||
assert settings.remote_python_auto_diagnostics_on_save is True
|
||||
|
||||
|
||||
def test_effective_settings_user_wins_when_project_lacks_key(
|
||||
sublime_settings,
|
||||
) -> None:
|
||||
"""Missing project key falls through to user/default precedence."""
|
||||
sublime_settings({"sessions_remote_python_auto_diagnostics_on_save": True})
|
||||
window = FakeWindow(project_data={"settings": {"unrelated": "x"}})
|
||||
|
||||
settings = commands._effective_sessions_settings_for_remote_python(window)
|
||||
|
||||
assert settings.remote_python_auto_diagnostics_on_save is True
|
||||
|
||||
|
||||
def test_effective_settings_project_pipeline_overrides_user(
|
||||
sublime_settings,
|
||||
) -> None:
|
||||
"""Project ``sessions_remote_python_tool_pipeline`` replaces user value."""
|
||||
sublime_settings(
|
||||
{"sessions_remote_python_tool_pipeline": ["ruff_lint", "pyright_check"]},
|
||||
)
|
||||
window = FakeWindow(
|
||||
project_data={
|
||||
"settings": {
|
||||
"sessions_remote_python_tool_pipeline": ["ruff_lint"],
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
settings = commands._effective_sessions_settings_for_remote_python(window)
|
||||
|
||||
assert settings.remote_python_tool_pipeline == ("ruff_lint",)
|
||||
|
||||
|
||||
def test_effective_settings_project_invalid_type_ignored(
|
||||
sublime_settings,
|
||||
) -> None:
|
||||
"""Non-bool project value for a bool key falls through to user setting."""
|
||||
sublime_settings({"sessions_remote_python_auto_diagnostics_on_save": True})
|
||||
window = FakeWindow(
|
||||
project_data={
|
||||
"settings": {
|
||||
"sessions_remote_python_auto_diagnostics_on_save": "yes",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
settings = commands._effective_sessions_settings_for_remote_python(window)
|
||||
|
||||
# Wrong type is rejected → user setting wins.
|
||||
assert settings.remote_python_auto_diagnostics_on_save is True
|
||||
|
||||
|
||||
def test_effective_settings_no_project_data_safe(sublime_settings) -> None:
|
||||
"""Window with ``project_data() is None`` must not raise."""
|
||||
sublime_settings({})
|
||||
window = FakeWindow(project_data=None)
|
||||
|
||||
settings = commands._effective_sessions_settings_for_remote_python(window)
|
||||
|
||||
assert isinstance(settings, SessionsSettings)
|
||||
|
||||
|
||||
def test_effective_settings_no_window_skips_project_merge(
|
||||
sublime_settings,
|
||||
) -> None:
|
||||
"""Calling without ``window`` is the legacy global-only path."""
|
||||
sublime_settings({"sessions_remote_python_auto_diagnostics_on_save": True})
|
||||
|
||||
settings = commands._effective_sessions_settings_for_remote_python(None)
|
||||
|
||||
assert settings.remote_python_auto_diagnostics_on_save is True
|
||||
|
||||
|
||||
def test_interactive_ssh_lane_basic() -> None:
|
||||
commands._begin_interactive_ssh_lane("test-host-lane")
|
||||
commands._end_interactive_ssh_lane("test-host-lane")
|
||||
|
||||
@@ -39,20 +39,6 @@ def test_catalog_project_keys_match_managed_client_snapshot() -> None:
|
||||
assert SESSIONS_LSP_RUST_ANALYZER_CLIENT_KEY in lsp_keys
|
||||
|
||||
|
||||
def test_catalog_contains_jupyter_extension_entry() -> None:
|
||||
entries = [
|
||||
e for e in BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG if e.kind == "jupyter"
|
||||
]
|
||||
assert len(entries) == 1
|
||||
entry = entries[0]
|
||||
assert entry.install_catalog_id == "jupyterlab"
|
||||
# LSP-specific fields are cleared for non-LSP kinds.
|
||||
assert entry.project_client_key is None
|
||||
assert entry.bridge_server_id is None
|
||||
assert entry.remote_spawn_argv is None
|
||||
assert entry.sublime_selector is None
|
||||
|
||||
|
||||
def test_catalog_contains_debugger_extension_entry() -> None:
|
||||
entries = [
|
||||
e for e in BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG if e.kind == "debugger"
|
||||
@@ -70,19 +56,3 @@ def test_catalog_contains_debugger_extension_entry() -> None:
|
||||
assert entry.bridge_server_id is None
|
||||
assert entry.remote_spawn_argv is None
|
||||
assert entry.sublime_selector is None
|
||||
|
||||
|
||||
def test_catalog_contains_agent_extension_entries() -> None:
|
||||
entries = [e for e in BUILTIN_MANAGED_REMOTE_EXTENSION_CATALOG if e.kind == "agent"]
|
||||
assert [e.install_catalog_id for e in entries] == [
|
||||
"tmux",
|
||||
"claude-code",
|
||||
"codex-cli",
|
||||
]
|
||||
for entry in entries:
|
||||
# LSP-specific fields are cleared for non-LSP kinds.
|
||||
assert entry.project_client_key is None
|
||||
assert entry.bridge_server_id is None
|
||||
assert entry.remote_spawn_argv is None
|
||||
assert entry.sublime_selector is None
|
||||
assert entry.legacy_project_client_keys == ()
|
||||
|
||||
@@ -162,11 +162,7 @@ def test_merge_remote_extension_catalog_user_overrides_builtin_by_id() -> None:
|
||||
"pyright-langserver",
|
||||
"ruff",
|
||||
"rust-analyzer",
|
||||
"jupyterlab",
|
||||
"debugpy",
|
||||
"tmux",
|
||||
"claude-code",
|
||||
"codex-cli",
|
||||
]
|
||||
assert merged[0].label == "Custom Pyright"
|
||||
assert merged[0].probe_argv == ("pyright-langserver", "--help")
|
||||
@@ -188,11 +184,7 @@ def test_merge_remote_extension_catalog_appends_user_only_ids() -> None:
|
||||
"pyright-langserver",
|
||||
"ruff",
|
||||
"rust-analyzer",
|
||||
"jupyterlab",
|
||||
"debugpy",
|
||||
"tmux",
|
||||
"claude-code",
|
||||
"codex-cli",
|
||||
"my-lsp",
|
||||
]
|
||||
|
||||
@@ -350,11 +342,7 @@ def test_load_settings_from_sublime_with_full_mock(monkeypatch) -> None:
|
||||
"pyright-langserver",
|
||||
"ruff",
|
||||
"rust-analyzer",
|
||||
"jupyterlab",
|
||||
"debugpy",
|
||||
"tmux",
|
||||
"claude-code",
|
||||
"codex-cli",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user