diff --git a/docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md b/docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md index c6f5428..e3525f7 100644 --- a/docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md +++ b/docs/superpowers/specs/2026-10-15-remote-repositories-over-ssh-design.md @@ -35,7 +35,7 @@ one character, and no local folder with exactly that name exists. Every other te local folder. So `C:\code\x` and `C:/code/x` stay local, `./a:b` stays local, and a folder in the current folder named `notes:2024` stays local when it exists. -A path on the server that starts with `~/` is handed to the server's shell with the +A path on the server that is `~` or starts with `~/` is handed to the server's shell with the `~` outside the quotes, so the shell expands it to the home folder there. ## Running git @@ -49,6 +49,7 @@ A path on the server that starts with `~/` is handed to the server's shell with ``` ssh -o BatchMode=yes -o ConnectTimeout=10 + -o ServerAliveInterval=5 -o ServerAliveCountMax=2 -o ControlMaster=auto -o ControlPath= -o ControlPersist=10m user@myserver "git -C '/srv/app' 'log' '--format=…' …" ``` @@ -57,12 +58,18 @@ A path on the server that starts with `~/` is handed to the server's shell with has no terminal to ask in. Keys and the ssh agent work. - `ConnectTimeout=10`: a server that does not answer gives an error after 10 seconds instead of a window that waits for ever. + - `ServerAliveInterval=5`, `ServerAliveCountMax=2`: when a server stops answering + on a connection that is already open (also a shared one), ssh notices it in about + 10 seconds and gives an error instead of waiting for ever. - `ControlMaster`, `ControlPath`, `ControlPersist`: the first command opens one connection and leaves it open; later commands go through it and cost about one network round trip each instead of a new login. The connection closes 10 minutes after the last command. The socket is `gites-%C` in the user's runtime folder - (`/run/user/` on Linux), or in `/tmp` where there is none; `%C` is a hash ssh - makes from the host, port and user, so the path stays short. + (`/run/user/` on Linux). Where there is none (always on macOS), it is in a + `gites` folder in the user's cache folder, made so that only the user can open it; + never in `/tmp`. If neither folder can be found or made, these three options are + left out and every command opens a new connection. `%C` is a hash ssh makes from + the host, port and user, so the path stays short. - On Windows, ssh cannot share connections, so these three options are left out and every command opens a new connection. - Quoting: every word of the git command is put in single quotes, and a `'` inside a @@ -107,6 +114,12 @@ If the connection drops while a repository is open, the next command shows its e as a message at the bottom of the window and the graph that is already on screen stays. The next reload connects again by itself. +A message that reports an error stays for 8 seconds, or until it is clicked, and each +hint line above stays on its own line. Other messages go after 1.8 seconds, as before. + +The commands that run git run away from the thread that draws the window, so the +window does not freeze while it waits for a slow server. + ## Tests - Reading locations: `host:/path`, `user@host:/path`, `host:~/x`, `C:\x`, `C:/x`, a diff --git a/src/App.vue b/src/App.vue index 937eaca..a345287 100644 --- a/src/App.vue +++ b/src/App.vue @@ -70,7 +70,7 @@ watch( if (!settingsLoaded) return; clearTimeout(settingsTimer); settingsTimer = window.setTimeout(() => { - invoke("save_settings", { value: { ...settings } }).catch((e) => showToast(`could not save settings: ${e}`)); + invoke("save_settings", { value: { ...settings } }).catch((e) => showToast(`could not save settings: ${e}`, { error: true })); }, 300); }, { deep: true }, @@ -117,7 +117,7 @@ async function load() { if (data) await applyRepo(data); } catch (e) { // A repository already on screen stays, for example when a connection drops. - if (repo.value) showToast(`could not reload: ${e}`); + if (repo.value) showToast(`could not reload: ${e}`, { error: true }); else error.value = String(e); } finally { loading = false; @@ -131,7 +131,7 @@ async function openRepo(path: string) { await applyRepo(await invoke("open_repo", { path })); } catch (e) { // The repository that was open stays open. - showToast(`could not open ${path}: ${e}`); + showToast(`could not open ${path}: ${e}`, { error: true }); } } @@ -177,7 +177,7 @@ watch( invoke("save_filters", { repo: repoRoot, value: { hidden: filters.hidden, rules: filters.rules, hideTags: filters.hideTags, collapseAll: collapseAll.value }, - }).catch((e) => showToast(`could not save filters: ${e}`)); + }).catch((e) => showToast(`could not save filters: ${e}`, { error: true })); }, 300); }, { deep: true }, @@ -366,10 +366,17 @@ async function copyText(text: string) { // ---- actions ------------------------------------------------------------- let toastTimer: number | undefined; -function showToast(text: string) { +/** Shows a short message. An error message stays longer, 8 seconds, so there is time to + * read it; a click on any message closes it. */ +function showToast(text: string, { error = false } = {}) { toast.value = text; clearTimeout(toastTimer); - toastTimer = window.setTimeout(() => (toast.value = null), 1800); + toastTimer = window.setTimeout(() => (toast.value = null), error ? 8000 : 1800); +} + +function closeToast() { + clearTimeout(toastTimer); + toast.value = null; } async function copyHash() { @@ -653,7 +660,7 @@ onUnmounted(() => { -
{{ toast }}
+
{{ toast }}
@@ -760,6 +767,11 @@ onUnmounted(() => { padding: 6px 14px; border-radius: 8px; box-shadow: 0 6px 24px #0008; + /* Keeps each line of a message on its own line, such as the hint under an ssh error. */ + white-space: pre-line; + max-width: min(640px, calc(100vw - 32px)); + overflow-wrap: anywhere; + cursor: pointer; } .fade-enter-active, .fade-leave-active { transition: opacity 0.2s; } .fade-enter-from, .fade-leave-to { opacity: 0; } diff --git a/src/components/Welcome.vue b/src/components/Welcome.vue index fe4d461..4c25c9e 100644 --- a/src/components/Welcome.vue +++ b/src/components/Welcome.vue @@ -56,7 +56,10 @@ const name = (p: string) => p.split(/[\\/]/).filter(Boolean).pop() ?? p; /> -

You can also start gites with a path: gites ~/code/my-project

+

+ You can also start gites with a path: gites ~/code/my-project, + or with a repository on another machine: gites myserver:/srv/app +