Myeongseon Choi dca8fb5a9c
All checks were successful
boundary-lint / PR boundary-claim (Lint (push) Has been skipped
boundary-lint / ban-list lint (Lint (push) Successful in 19s
boundary-lint / duplication-deadline (Layer 1/2) (push) Successful in 19s
Release Publish (Gitea session_helper) / verify-release-tag (push) Successful in 17s
Release Publish (Gitea session_helper) / publish-linux-x86_64 (push) Successful in 4m47s
ci / rust debug (push) Successful in 1m47s
ci / rust release (push) Successful in 2m24s
ci / test-health gate (push) Successful in 16s
ci / mutation test (broker) (push) Successful in 1m14s
ci / python (push) Successful in 1m41s
fix(terminal+lsp): SHELL=$(POSIX-fallback) + skip selection-restore on empty buffer (v0.7.43)
Two follow-ups to v0.7.42 user reports.

1. Terminal: ``zsh:1: permission denied:`` exit 126
---------------------------------------------------

v0.7.42 dropped the ``${SHELL:-/bin/sh}`` fallback assuming sshd
populates ``$SHELL`` in every login shell, but ``ssh -t host cmd``
runs the user's login shell with ``-c`` (NON-login mode); on some
remotes ``$SHELL`` is unset there, so ``exec "$SHELL" -il`` becomes
``exec "" -il`` → ``permission denied:`` exit 126.

Reinstate the fallback via POSIX ``if [ -z "$SHELL" ]; then
SHELL=/bin/sh; fi`` instead of ``${SHELL:-...}`` so the parser-bug
class that produced ``zsh:1: unknown exec flag -/`` in v0.7.31+ is
still avoided.

2. LSP: cross-file goto-def to unhydrated placeholder lands at (0,0)
--------------------------------------------------------------------

When LSP-pyright / rust-analyzer return a definition target whose
local cache copy is still a 0-byte placeholder, Sublime's
``window.open_file(path:42:5, ENCODED_POSITION)`` cannot place the
caret at row 42 col 5 — that row doesn't exist in an empty buffer —
and clamps to ``(0, 0)``. ``_apply_hydrate_result`` then captured
that ``(0, 0)`` selection before revert and restored it after,
overriding whatever position Sublime defers / re-applies once the
buffer has content. Net result: user lands at the file top instead
of the definition.

Skip capture/restore entirely when the pre-revert buffer was empty.
For the empty-pre-revert case the captured selection is always
``(0, 0)`` — restoring it can only override a Sublime-side deferred
placement, never recover the LSP target — so dropping the restore
is at least as good as before and lets any deferred ENCODED_POSITION
take effect.

The non-empty branch (e21b3a4 cross-file caret fix for already-
hydrated buffers) is unchanged.

Tests
-----

Python 1368 pass; Rust 486 pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 00:23:55 +09:00
2026-04-22 18:28:42 +09:00

Sessions

Sessions is a Sublime Text package and Rust helper toolkit for remote workspaces over SSH.

Current focus:

  • Completed milestones: Phase 06.2 (all closed), Phase 7 - Stability Hardening (closed), Phase 8 - Rust Transport Expansion (closed), Remote LSP integration track (#34, #35, #36, #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.
  • Open milestones: Phase 9 - Quality Gates & Scale (#10, #32 large-file streaming). #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 and 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 and planning/SHIPPED.md.
  • Execution order (2026-04, Rust-first): P0.5 stabilization → crate consolidation → artifact publish + manifest/checksum → #24 Rust runtime ownership → #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 (execution priority and schedule), migration waves: planning/PYTHON_RUST_BOUNDARY.md. Distribution-readiness + ownership-migration plan: 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 windowdropped 2026-04-27, residue removed 2026-04-30: the v0.6.0v0.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 Track D and planning/SHIPPED.md.

Repository layout

  • sublime/: Sublime package code and Python tests
  • rust/: Rust workspace for bridge/helper/shared crates
  • docs/: human-facing project documentation (when present)
  • planning/: roadmap, Gitea issue bootstrap, transport model, Python/Rust boundary, deep-research notes

Planning index (browse on Gitea)

Document Role
planning/GITEA_ISSUES.md Milestones, parent issue text, execution priority
planning/PYTHON_RUST_BOUNDARY.md Python vs Rust split, Rust-first migration waves 05
planning/VSCODE_REMOTE_TRANSPORT_MODEL.md Envelope + logical channels (VS Codealigned)
planning/REMOTE_DEV_MVP_LSP.md Phase 6.2 LSP / tool transport choices
planning/DEEP-RESEARCH-REPORT.md External audit + priority reconciliation (end)
planning/TRACK_G_V1_BIDIRECTIONAL_SYNC.md Track G v1 plan: bidirectional .git sync redesign (op-log + ref snapshot + git bundle, replaces tar-wipe)

Installing In Sublime Text

sublime/ is the actual Sublime package root for this repository.

  • Development install: link or copy sublime/ to Packages/Sessions
  • Current example: Packages/Sessions -> /path/to/sessions/sublime
  • Unsupported layout: linking the repository root directly

The repository root remains a development workspace for Python tooling, Rust crates, docs, and planning files. Sublime does not discover the nested sublime/ directory automatically when the whole repository is linked as one package.

Building A Release Archive

Build a distributable archive from the sublime/ package root with:

uv run python scripts/build_sublime_package.py

The script writes dist/Sessions.sublime-package and excludes development-only artifacts such as tests/ and __pycache__/.

To build the current platform's Rust bridge/helper in release mode and bundle them automatically, run:

uv run python scripts/build_sublime_package.py --bundle-built-rust-binaries

To ship pre-built Rust binaries (or any other release-only files), pass an explicit layout rather than relying on discovery inside sublime/:

  • Repeat --bundle-file SOURCE=PATH_IN_PACKAGE so each PATH_IN_PACKAGE is the zip entry path (POSIX separators, no ..).
  • Or point --bundle-manifest at a JSON file: a list of objects with source and path_in_package strings. Relative source paths resolve from the manifest directory, which keeps CI artifacts and the install tree layout in one place.

Example manifest:

[
  {
    "source": "artifacts/darwin-aarch64/local_bridge",
    "path_in_package": "sessions/bin/local-bridge/darwin-aarch64/local_bridge"
  },
  {
    "source": "artifacts/linux-x86_64/session_helper",
    "path_in_package": "sessions/bin/remote-helper/linux-x86_64/session_helper"
  }
]

Rust Binary Status

Current product policy (local vs remote):

  • Remote host: Linux only. The remote session_helper is fetched by the editor (not the remote): local_bridge downloads the matching binary from the Gitea generic registry into the editor cache, then pushes it to the remote over the existing SSH session. No curl / wget runs on the remote, and no remote cargo build. Binary is keyed by Linux platform tag + workspace semver from rust/Cargo.toml.
  • Local machine (editor side): Linux, macOS, and Windows are supported for running Sublime + this package. For day-to-day development, treat local_bridge as built on that machine (cargo build -p local_bridge, see Development). The plugin discovers rust/target/debug/local_bridge (or .exe on Windows) when no shipped binary is present under sessions/bin/....

Planned packaging (later): pre-built local_bridge per local OS/arch inside release archives; until then, local build is the supported path for macOS and Windows editors.

Current behavior:

  • Persistent bridge session: local_bridge --persistent runs as a long-lived process per host, communicating with session_helper over a single SSH stdio session. Python sends NDJSON request envelopes and receives async responses via a background reader thread. Each request gets a unique monotonic envelope_id to prevent response mis-routing under concurrency.
  • Editor-cache helper resolution: session_helper is downloaded by the editor host (local_bridge) from the Gitea generic registry into the editor cache, then pushed to the remote over the existing SSH session — curl / wget never run on the remote. Identified by workspace semver + Linux platform tag, the remote-side cache lives at $HOME/.cache/sessions/helpers/<revision>/session_helper. If the editor-side download fails, the connection fails explicitly (no cargo build fallback).
  • Required handshake fields: Handshake.remote_home and Handshake.arch are required (no Option, no fallback). The bridge merges helper ensure + launch into a single SSH command to avoid double authentication.
  • Async multiplexer: local_bridge acts as an async multiplexer with a background mirror thread performing BFS via session_helper. Python sends commands and manages async I/O; file opens are never blocked by mirror progress.
  • Reconnect: SessionsReconnectCurrentWorkspaceCommand runs in a background thread with ssh_prompt_callback, explicitly resets the bridge, and fails fast on handshake timeout (kills the bridge process immediately instead of returning a broken session).
  • After you open a remote file into the workspace cache, a normal editor save (Cmd+S / Ctrl+S) writes the local cache file and then pushes those bytes to the remote path automatically; Sessions: Save Remote File remains available as an explicit palette action.

Planned end-user install behavior (when releases ship all local binaries):

  • the local local_bridge binary will ship inside the released Sessions package for each supported local platform/architecture
  • the remote session_helper binary is downloaded on demand from the Gitea generic registry by the remote host itself (curl/wget)
  • the uploaded helper path is versioned as $HOME/.cache/sessions/helpers/<revision>/session_helper
  • the bridge rejects helper handshakes whose reported helper version does not match the bridge version, so mixed bridge/helper bundles fail fast
  • end users should not need a repository checkout or Cargo installed

Windows: running Sessions on the editor host

Typical flow when your PC is Windows and the SSH server is Linux:

  1. Sublime Text — Install a current build (ST4). This package is loaded from Packages/Sessions pointing at this repos sublime/ tree (symlink or copy; see Installing In Sublime Text above).
  2. SSH client — Use Windows optional OpenSSH Client or Git for Windows so ssh is on PATH. Put hosts in %USERPROFILE%\.ssh\config (same idea as ~/.ssh/config on Unix).
  3. Rust toolchain — Install rustup for Windows. From the repository root (the directory that contains rust/ and sublime/), run: cargo build --manifest-path rust/Cargo.toml -p local_bridge That produces rust\target\debug\local_bridge.exe, which the package picks up when no shipped binary exists under sessions/bin/local-bridge/windows-*/.
  4. Gitea / registry (if required) — If helper downloads need auth or a non-default host, set the sessions_gitea_* keys in Preferences → Settings for Sessions (see comments in sublime/Sessions.sublime-settings). The Linux remote still performs the download; these settings only affect URL and headers resolved on the editor side.
  5. Use the package — In Sublime, run the Sessions connect flow, pick your Host from SSH config, then open a remote folder. The first connection builds or reuses the persistent local_bridge session; the remote runs session_helper after fetching the Linux artifact.

If bridge commands fail with “local_bridge binary not found”, confirm the Cargo debug output path exists and that you opened a workspace whose sublime/ parent is the repo root (so rust/target/debug resolves correctly).

Troubleshooting: duplicate “Sessions:” commands in the palette

The package ships a single Sessions.sublime-commands file; if every Sessions entry appears twice, Sublime is almost certainly loading two package trees that both contribute commands (merged resources).

Typical causes:

  1. Unpacked + packaged copy — e.g. Packages/Sessions/ (dev link) and Installed Packages/Sessions.sublime-package from Package Control. Remove one: disable the package in Package Control, or delete the shipped archive, or set ignored_packages so only your dev link is active.
  2. Wrong link target — install only sublime/ as Packages/Sessions, not the repository root (see Installing In Sublime Text). A mis-linked tree can lead to confusing layouts; fix the symlink/copy so there is exactly one package root.
  3. Case-insensitive volume duplicates — on Windows or default macOS disks, avoid having two folder names that only differ in case both pointing at the same code.

cargo build --workspace does not register Sublime commands by itself; rebuilding Rust only changes binaries. If duplicates appeared right after a package change, reload the editor (Developer: Reload from Disk / restart) once duplicates are resolved so stale command tables are cleared.

The shipped Sessions.sublime-commands intentionally lists sessions_run_remote_python_tool twice with different args (lint vs format); that is not a duplicate installation.

Troubleshooting: Remote LSP install vs save-time ruff/pyright

Install Remote LSP Server (and the status panel) use the persistent bridges exec/once path to run install/remove scripts and probe_argv on the remote host. That checks that tools such as ruff --version exist in the remote environment.

Save-time diagnostics for mirrored .py files are controlled separately by sessions_remote_python_auto_diagnostics_on_save and sessions_remote_python_tool_pipeline in Sessions.sublime-settings. That pipeline still needs an active bridge session. If you see Rust bridge closed the persistent session (or similar), reconnect before expecting on-save lint to run.

Enable sessions_debug_trace_enabled for <Sublime cache>/Sessions/logs/debug-trace.log (correlates Python bridge.* lines with Rust bridge.rust_* when SESSIONS_BRIDGE_DIAG_LOG is set). After this package version, remote LSP install/remove/probe also emit [Sessions LSP] lines to the console (stderr).

Remote workspace window title

Generated .sublime-project files include a name field such as app [SSH: prod] (remote root basename plus SSH host alias) so the Sublime window title is easier to recognize when several projects are open.

Output panels and remote diagnostics

Sessions output panels (LSP status, tool output) scroll to the end after open so the latest lines stay visible. Remote Python diagnostics from Sessions use add_regions on the mirrored buffer; if underlines from another package (for example Sublime LSP) look stale until a window reload, that is usually editor refresh ordering rather than the remote ruff run being skipped.

Per-project LSP settings are owned by the LSP package you use, not by Sessions: configure them in .sublime-project / project-specific user settings as documented for that package (for example client enablement, server paths, and syntax scopes). Sessions writes/updates sessions_workspace_key and workspace display name, while preserving existing project settings (including settings.LSP) on rematerialize/reconnect. sessions_remote_code_servers in Sessions.sublime-settings is for Sessions transport wiring, not Sublime LSP UI.

Troubleshooting: git status noise (GitSavvy)

The Sessions sidebar mirror adds your workspace cache folder (under Sublimes cache path) to the project. That directory is a plain file tree, not a Git repository. GitSavvy (and similar packages) may still run git status for the current working directory and log errors such as:

fatal: not a git repository (or any of the parent directories): .git

with a stack trace under GitSavvy.sublime-package/.../status_bar.py or git_mixins/status.py. That comes from GitSavvy, not from Sessions own code.

Mitigations:

  1. Turn off GitSavvys status-bar polling (simplest): in Preferences → Package Settings → GitSavvy → Settings, set "git_status_in_status_bar": false. Branch/dirty text disappears globally, but GitSavvys palette commands still work when you are inside a real repo.
  2. Project-only override: if GitSavvy exposes project settings for your build, add the same key under the .sublime-project settings block for Sessions-only windows.
  3. Unrelated console line top level value must be an array: usually means some JSON (often folders in a .sublime-project) is not shaped the way Sublime expects. If it appears right after editing the project file, validate that "folders" is a JSON array of objects.

Sessions does not ship GitSavvy; avoiding non-repo git status spam is a third-party / user-settings concern.

Development

The expected workflow is:

  1. add or refine a skeleton
  2. add or update focused tests
  3. implement the smallest coherent behavior
  4. run the relevant checks
  5. commit only after pre-commit passes
Description
Multiple session agent windows in SublimeText
Readme 25 MiB
Languages
Python 72.1%
Rust 27.9%