pi-ssh-remote 0.1.6 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -1
- package/LICENSE +0 -0
- package/README.md +4 -4
- package/README.zh-CN.md +5 -5
- package/index.ts +120 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,21 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Rel
|
|
|
18
18
|
|
|
19
19
|
- None.
|
|
20
20
|
|
|
21
|
+
## [0.1.7] - 2026-08-05
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Added non-secret, session-scoped SSH workspace state for endpoint, remote directory, tool-routing mode, and port forwards.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- `/new` now inherits the active SSH workspace, forks and clones retain their source workspace, and `/resume` restores the selected historical session's recorded workspace.
|
|
30
|
+
- Session changes close the previous transport cleanly and reconnect the target workspace without storing passwords or other credentials in session files.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- None.
|
|
35
|
+
|
|
21
36
|
## [0.1.6] - 2026-08-05
|
|
22
37
|
|
|
23
38
|
### Added
|
|
@@ -130,7 +145,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Rel
|
|
|
130
145
|
|
|
131
146
|
- None.
|
|
132
147
|
|
|
133
|
-
[Unreleased]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.
|
|
148
|
+
[Unreleased]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.7...HEAD
|
|
149
|
+
[0.1.7]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.6...v0.1.7
|
|
134
150
|
[0.1.6]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.5...v0.1.6
|
|
135
151
|
[0.1.5]: https://github.com/petrichor20211/pi-ssh-remote/compare/v0.1.4...v0.1.5
|
|
136
152
|
[0.1.4]: https://github.com/petrichor20211/pi-ssh-remote/releases/tag/v0.1.4
|
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Each `user@host:port` endpoint remembers its remote working directory, note, ser
|
|
|
35
35
|
|
|
36
36
|
### Resilient and bounded by default
|
|
37
37
|
|
|
38
|
-
Dropped connections are automatically re-established during the active session. Remote commands have a 30-second default timeout. Remote text reads fetch focused ranges instead of downloading complete files, command output is streamed through bounded buffers, and a 32 KB per-turn budget prevents parallel tools from flooding model context. Complete oversized command output is preserved in a permission-restricted temporary file.
|
|
38
|
+
Dropped connections are automatically re-established during the active session. SSH workspace state is also session-aware: `/new` inherits the current workspace, forks and clones retain their source workspace, and `/resume` restores the selected session's recorded endpoint, remote directory, routing mode, and forwards. Remote commands have a 30-second default timeout. Remote text reads fetch focused ranges instead of downloading complete files, command output is streamed through bounded buffers, and a 32 KB per-turn budget prevents parallel tools from flooding model context. Complete oversized command output is preserved in a permission-restricted temporary file.
|
|
39
39
|
|
|
40
40
|
### Hybrid local/remote workflows
|
|
41
41
|
|
|
@@ -195,7 +195,7 @@ Endpoint configuration is stored locally in:
|
|
|
195
195
|
~/.pi/agent/ssh-remote-config.json
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
-
Saved values include endpoints, active endpoint, notes, server-specific memories, remote working directories, forwards, preview settings, and model-output budgets. Server memory is user-configured trusted context and is inserted into each model request only while that endpoint is the active remote workspace; it is not read from the remote server. Passwords are **never written to this file**: SSH agent authentication is preferred, and prompted passwords remain only in process memory.
|
|
198
|
+
Saved global values include endpoints, active endpoint, notes, server-specific memories, remote working directories, forwards, preview settings, and model-output budgets. Each Pi session also stores non-secret SSH workspace metadata so `/resume` can restore the server associated with that session. Server memory is user-configured trusted context and is inserted into each model request only while that endpoint is the active remote workspace; it is not read from the remote server. Passwords are **never written to this file**: SSH agent authentication is preferred, and prompted passwords remain only in process memory.
|
|
199
199
|
|
|
200
200
|
New or changed host keys require interactive confirmation and are stored separately from OpenSSH. By default, remote text reads return at most 400 lines or 16 KB, remote command results return the last 200 lines or 8 KB, and all remote tools in one agent turn share a 32 KB output budget. Text reads support `offset`/`limit` continuation without downloading the complete remote file. Oversized command output is streamed to a permission-restricted temporary local file rather than accumulated in memory. Configured limits may be raised only to the extension's hard safety ceilings. Preview-line settings affect only the collapsed UI and never increase model output.
|
|
201
201
|
|
|
@@ -205,13 +205,13 @@ The extension currently supports direct SSH commands with `-p` and `-l`. It does
|
|
|
205
205
|
|
|
206
206
|
## Releases
|
|
207
207
|
|
|
208
|
-
Latest release: [v0.1.
|
|
208
|
+
Latest release: [v0.1.7](https://github.com/petrichor20211/pi-ssh-remote/releases/tag/v0.1.7)
|
|
209
209
|
|
|
210
210
|
| Version | Date | Highlights |
|
|
211
211
|
|---|---|---|
|
|
212
|
+
| [0.1.7](CHANGELOG.md#017---2026-08-05) | 2026-08-05 | Session-aware SSH workspace restoration for new, forked, and resumed sessions |
|
|
212
213
|
| [0.1.6](CHANGELOG.md#016---2026-08-05) | 2026-08-05 | Bounded remote reads, streamed command output, and per-turn output budgets |
|
|
213
214
|
| [0.1.5](CHANGELOG.md#015---2026-08-04) | 2026-08-04 | Endpoint-specific server memory and automatic context injection |
|
|
214
|
-
| [0.1.4](CHANGELOG.md#014---2026-07-31) | 2026-07-31 | Endpoint notes and enforced remote command timeouts |
|
|
215
215
|
|
|
216
216
|
See [CHANGELOG.md](CHANGELOG.md) for the complete release history, including additions, behavior changes, and bug fixes.
|
|
217
217
|
|
package/README.zh-CN.md
CHANGED
|
@@ -58,7 +58,7 @@ SSH remote H100 训练机 (root@gpu-box.example.com:2202):/srv/project
|
|
|
58
58
|
|
|
59
59
|
### 断线后可以自动恢复
|
|
60
60
|
|
|
61
|
-
当前会话中如果 SSH 连接意外断开,插件会尝试自动重连,不需要 Agent 从头建立工作环境。
|
|
61
|
+
当前会话中如果 SSH 连接意外断开,插件会尝试自动重连,不需要 Agent 从头建立工作环境。SSH 工作区状态也会跟随 Pi session:`/new` 继承当前工作区,`/fork` 和 `/clone` 保留来源工作区,`/resume` 恢复目标历史 session 记录的服务器、远程目录、路由模式和端口转发。
|
|
62
62
|
|
|
63
63
|
### 对 Agent 上下文更友好
|
|
64
64
|
|
|
@@ -249,7 +249,7 @@ Pi 会通过插件提供的 `ssh_remote_control` 工具完成这些操作。
|
|
|
249
249
|
~/.pi/agent/ssh-remote-config.json
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
全局配置包括:
|
|
253
253
|
|
|
254
254
|
- 已保存的服务器;
|
|
255
255
|
- 当前选中的服务器;
|
|
@@ -260,7 +260,7 @@ Pi 会通过插件提供的 `ssh_remote_control` 工具完成这些操作。
|
|
|
260
260
|
- 命令预览设置;
|
|
261
261
|
- 模型输出预算。
|
|
262
262
|
|
|
263
|
-
|
|
263
|
+
此外,每个 Pi session 都会记录不含凭据的 SSH 工作区元数据,用于在 `/resume` 时恢复该历史 session 对应的服务器环境。服务器记忆是由用户在本地配置的可信上下文,不会从远程服务器自动读取;仅当对应 endpoint 作为远端工作区启用时才会加入每次模型请求。
|
|
264
264
|
|
|
265
265
|
密码不会写入配置文件。插件会优先使用 SSH agent;如果需要手动输入密码,密码只会缓存在当前 Pi 进程的内存中。
|
|
266
266
|
|
|
@@ -283,13 +283,13 @@ Pi 会通过插件提供的 `ssh_remote_control` 工具完成这些操作。
|
|
|
283
283
|
|
|
284
284
|
## 版本发布
|
|
285
285
|
|
|
286
|
-
最新版本:[v0.1.
|
|
286
|
+
最新版本:[v0.1.7](https://github.com/petrichor20211/pi-ssh-remote/releases/tag/v0.1.7)
|
|
287
287
|
|
|
288
288
|
| 版本 | 日期 | 主要内容 |
|
|
289
289
|
|---|---|---|
|
|
290
|
+
| [0.1.7](CHANGELOG.md#017---2026-08-05) | 2026-08-05 | 新建、分叉和恢复 session 时自动恢复对应 SSH 工作区 |
|
|
290
291
|
| [0.1.6](CHANGELOG.md#016---2026-08-05) | 2026-08-05 | 有界远端读取、流式命令输出与每轮输出预算 |
|
|
291
292
|
| [0.1.5](CHANGELOG.md#015---2026-08-04) | 2026-08-04 | endpoint 级服务器记忆与自动上下文注入 |
|
|
292
|
-
| [0.1.4](CHANGELOG.md#014---2026-07-31) | 2026-07-31 | endpoint 备注与远程命令强制超时 |
|
|
293
293
|
|
|
294
294
|
完整的新增内容、行为变更和 Bug 修复记录请查看 [CHANGELOG.md](CHANGELOG.md)。
|
|
295
295
|
|
package/index.ts
CHANGED
|
@@ -48,7 +48,7 @@ interface RemoteState extends ParsedSsh {
|
|
|
48
48
|
|
|
49
49
|
interface CredentialCache {
|
|
50
50
|
passwords: Map<string, string>;
|
|
51
|
-
resume?: { command: string; cwd: string };
|
|
51
|
+
resume?: { command: string; cwd: string; routeRemoteTools: boolean; forwards?: string[] };
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
interface RemoteEndpointConfig {
|
|
@@ -80,6 +80,15 @@ interface ForwardSpec {
|
|
|
80
80
|
remotePort: number;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
interface SessionRemoteState {
|
|
84
|
+
version: 1;
|
|
85
|
+
connected: boolean;
|
|
86
|
+
command?: string;
|
|
87
|
+
cwd?: string;
|
|
88
|
+
routeRemoteTools?: boolean;
|
|
89
|
+
forwards?: string[];
|
|
90
|
+
}
|
|
91
|
+
|
|
83
92
|
const AGENT_DIR = join(process.env.HOME || ".", CONFIG_DIR_NAME, "agent");
|
|
84
93
|
const KNOWN_HOSTS_FILE = join(AGENT_DIR, "ssh-remote-known-hosts.json");
|
|
85
94
|
const REMOTE_CONFIG_FILE = join(AGENT_DIR, "ssh-remote-config.json");
|
|
@@ -95,6 +104,7 @@ const MIN_MODEL_OUTPUT_BYTES = 1024;
|
|
|
95
104
|
const OUTPUT_FOOTER_RESERVE_BYTES = 512;
|
|
96
105
|
const DEFAULT_REMOTE_TIMEOUT_SECONDS = 30;
|
|
97
106
|
const MAX_REMOTE_TIMEOUT_SECONDS = 2_147_483_647 / 1000;
|
|
107
|
+
const SESSION_STATE_ENTRY_TYPE = "pi-ssh-remote-state";
|
|
98
108
|
const CACHE_KEY = "__piHpcCredentialCacheV1";
|
|
99
109
|
const cacheHost = globalThis as typeof globalThis & { [CACHE_KEY]?: CredentialCache };
|
|
100
110
|
const credentialCache = cacheHost[CACHE_KEY] ??= { passwords: new Map<string, string>() };
|
|
@@ -659,6 +669,9 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
659
669
|
let currentCtx: any;
|
|
660
670
|
let reconnectPromise: Promise<RemoteState> | null = null;
|
|
661
671
|
const forwardServers = new Map<number, Server>();
|
|
672
|
+
const forwardSpecs = new Map<number, ForwardSpec>();
|
|
673
|
+
let sessionReady = false;
|
|
674
|
+
let restoringSessionState = false;
|
|
662
675
|
let lastConnectionError: string | undefined;
|
|
663
676
|
let lastCommand = credentialCache.resume?.command ?? activeSshCommand() ?? "";
|
|
664
677
|
let turnOutputBytes = 0;
|
|
@@ -684,6 +697,42 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
684
697
|
return posix.join(remote.cwd, normalized);
|
|
685
698
|
};
|
|
686
699
|
|
|
700
|
+
const serializeForward = (spec: ForwardSpec): string => `${spec.localPort}:${spec.remoteHost}:${spec.remotePort}`;
|
|
701
|
+
|
|
702
|
+
const currentSessionRemoteState = (): SessionRemoteState => remote ? {
|
|
703
|
+
version: 1,
|
|
704
|
+
connected: true,
|
|
705
|
+
command: remote.command,
|
|
706
|
+
cwd: remote.cwd,
|
|
707
|
+
routeRemoteTools,
|
|
708
|
+
forwards: [...forwardSpecs.values()].map(serializeForward),
|
|
709
|
+
} : { version: 1, connected: false };
|
|
710
|
+
|
|
711
|
+
const persistSessionRemoteState = (): void => {
|
|
712
|
+
if (!sessionReady || restoringSessionState) return;
|
|
713
|
+
pi.appendEntry(SESSION_STATE_ENTRY_TYPE, currentSessionRemoteState());
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
const loadSessionRemoteState = (ctx: any): SessionRemoteState | undefined => {
|
|
717
|
+
const branch = ctx.sessionManager.getBranch();
|
|
718
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
719
|
+
const entry = branch[index] as any;
|
|
720
|
+
if (entry.type !== "custom" || entry.customType !== SESSION_STATE_ENTRY_TYPE) continue;
|
|
721
|
+
const data = entry.data as Partial<SessionRemoteState> | undefined;
|
|
722
|
+
if (!data || data.version !== 1 || typeof data.connected !== "boolean") return undefined;
|
|
723
|
+
if (data.connected && (typeof data.command !== "string" || typeof data.cwd !== "string")) return undefined;
|
|
724
|
+
return {
|
|
725
|
+
version: 1,
|
|
726
|
+
connected: data.connected,
|
|
727
|
+
...(data.command ? { command: data.command } : {}),
|
|
728
|
+
...(data.cwd ? { cwd: data.cwd } : {}),
|
|
729
|
+
...(typeof data.routeRemoteTools === "boolean" ? { routeRemoteTools: data.routeRemoteTools } : {}),
|
|
730
|
+
...(Array.isArray(data.forwards) ? { forwards: data.forwards.filter((value): value is string => typeof value === "string") } : {}),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
return undefined;
|
|
734
|
+
};
|
|
735
|
+
|
|
687
736
|
const limitRemoteToolResult = (result: any, kind: "read" | "exec", startLine = 1, requestedMaxLines?: number) => {
|
|
688
737
|
const limits = configuredOutputLimits();
|
|
689
738
|
const configuredMaxBytes = kind === "read" ? limits.readMaxBytes : limits.execMaxBytes;
|
|
@@ -772,6 +821,7 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
772
821
|
|
|
773
822
|
async function reconnectRemote(): Promise<RemoteState> {
|
|
774
823
|
if (reconnectPromise) return reconnectPromise;
|
|
824
|
+
const resumeRouting = remote ? routeRemoteTools : (credentialCache.resume?.routeRemoteTools ?? true);
|
|
775
825
|
const source = remote ?? (credentialCache.resume ? { ...parseSshCommand(credentialCache.resume.command), cwd: credentialCache.resume.cwd } : null);
|
|
776
826
|
if (!source) throw new Error("No SSH remote connection is available to reconnect");
|
|
777
827
|
const parsed = parseSshCommand(source.command);
|
|
@@ -780,7 +830,8 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
780
830
|
const oldClient = remote?.client;
|
|
781
831
|
const next = await establish(parsed, password, source.cwd);
|
|
782
832
|
remote = next;
|
|
783
|
-
|
|
833
|
+
routeRemoteTools = resumeRouting;
|
|
834
|
+
credentialCache.resume = { command: parsed.command, cwd: next.cwd, routeRemoteTools, forwards: credentialCache.resume?.forwards };
|
|
784
835
|
oldClient?.end();
|
|
785
836
|
if (currentCtx) {
|
|
786
837
|
status(currentCtx);
|
|
@@ -810,9 +861,10 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
810
861
|
`cd -- ${quote(remote!.cwd)} && ${targetCommand} && pwd -P`,
|
|
811
862
|
))).toString().trim();
|
|
812
863
|
remote.cwd = resolved;
|
|
813
|
-
credentialCache.resume = { command: remote.command, cwd: resolved };
|
|
864
|
+
credentialCache.resume = { command: remote.command, cwd: resolved, routeRemoteTools, forwards: [...forwardSpecs.values()].map(serializeForward) };
|
|
814
865
|
saveEndpointConfig(remote.command, { remoteCwd: resolved }, true);
|
|
815
866
|
status(ctx);
|
|
867
|
+
persistSessionRemoteState();
|
|
816
868
|
return resolved;
|
|
817
869
|
};
|
|
818
870
|
|
|
@@ -874,11 +926,12 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
874
926
|
routeRemoteTools = true;
|
|
875
927
|
previous?.end();
|
|
876
928
|
if (password) setCachedPassword(parsed, password);
|
|
877
|
-
credentialCache.resume = { command, cwd: next.cwd };
|
|
929
|
+
credentialCache.resume = { command, cwd: next.cwd, routeRemoteTools, forwards: [] };
|
|
878
930
|
lastCommand = command;
|
|
879
931
|
lastConnectionError = undefined;
|
|
880
932
|
saveEndpointConfig(command, { remoteCwd: next.cwd }, true);
|
|
881
933
|
status(ctx);
|
|
934
|
+
persistSessionRemoteState();
|
|
882
935
|
ctx.ui.notify(`SSH remote connected: ${endpointDisplayLabel(next)}:${next.cwd}`, "info");
|
|
883
936
|
return next;
|
|
884
937
|
} catch (error) {
|
|
@@ -922,14 +975,32 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
922
975
|
});
|
|
923
976
|
});
|
|
924
977
|
forwardServers.set(spec.localPort, server);
|
|
978
|
+
forwardSpecs.set(spec.localPort, spec);
|
|
925
979
|
};
|
|
926
980
|
|
|
927
981
|
const stopForwards = async (): Promise<void> => {
|
|
928
982
|
const servers = [...forwardServers.values()];
|
|
929
983
|
forwardServers.clear();
|
|
984
|
+
forwardSpecs.clear();
|
|
930
985
|
await Promise.all(servers.map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
|
|
931
986
|
};
|
|
932
987
|
|
|
988
|
+
const restoreSessionRemoteState = async (saved: SessionRemoteState, ctx: any): Promise<void> => {
|
|
989
|
+
if (!saved.connected) {
|
|
990
|
+
credentialCache.resume = undefined;
|
|
991
|
+
status(ctx);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
const command = saved.command!;
|
|
995
|
+
const next = await connectInteractive(command, ctx, saved.cwd);
|
|
996
|
+
if (!next) throw new Error(lastConnectionError || "SSH remote session restore was cancelled or failed");
|
|
997
|
+
routeRemoteTools = saved.routeRemoteTools ?? true;
|
|
998
|
+
const specs = (saved.forwards ?? []).map(parseForwardSpec);
|
|
999
|
+
for (const spec of specs) await startForward(spec);
|
|
1000
|
+
credentialCache.resume = { command, cwd: next.cwd, routeRemoteTools, forwards: [...forwardSpecs.values()].map(serializeForward) };
|
|
1001
|
+
status(ctx);
|
|
1002
|
+
};
|
|
1003
|
+
|
|
933
1004
|
const disconnect = (ctx: any, forgetPassword = false) => {
|
|
934
1005
|
const previous = remote;
|
|
935
1006
|
remote = null;
|
|
@@ -948,6 +1019,7 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
948
1019
|
}
|
|
949
1020
|
previous?.client.end();
|
|
950
1021
|
status(ctx);
|
|
1022
|
+
persistSessionRemoteState();
|
|
951
1023
|
ctx.ui.notify(forgetPassword ? "SSH remote disconnected and cached password cleared" : "SSH remote mode disabled (password remains cached in memory only)", "info");
|
|
952
1024
|
};
|
|
953
1025
|
|
|
@@ -1104,6 +1176,7 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1104
1176
|
}
|
|
1105
1177
|
if (params.action === "unforward") {
|
|
1106
1178
|
await stopForwards();
|
|
1179
|
+
persistSessionRemoteState();
|
|
1107
1180
|
return { content: [{ type: "text", text: "Closed all extension-managed SSH port forwards." }], details: { forwardedPorts: [] } };
|
|
1108
1181
|
}
|
|
1109
1182
|
if (params.action === "forward") {
|
|
@@ -1113,7 +1186,9 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1113
1186
|
const specs = values.map(parseForwardSpec);
|
|
1114
1187
|
for (const spec of specs) await startForward(spec);
|
|
1115
1188
|
routeRemoteTools = false;
|
|
1189
|
+
credentialCache.resume = { command: state.command, cwd: state.cwd, routeRemoteTools, forwards: [...forwardSpecs.values()].map(serializeForward) };
|
|
1116
1190
|
if (currentCtx) status(currentCtx);
|
|
1191
|
+
persistSessionRemoteState();
|
|
1117
1192
|
const ports = specs.map((spec) => spec.localPort);
|
|
1118
1193
|
return { content: [{ type: "text", text: `Forwarded local ports: ${ports.join(", ")}; tools remain local.` }], details: { toolRouting: "local", forwardedPorts: ports } };
|
|
1119
1194
|
}
|
|
@@ -1231,6 +1306,7 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1231
1306
|
previous.client.end();
|
|
1232
1307
|
await stopForwards();
|
|
1233
1308
|
status(ctx);
|
|
1309
|
+
persistSessionRemoteState();
|
|
1234
1310
|
}
|
|
1235
1311
|
saveRemoteConfig({ ...config, activeEndpoint: key });
|
|
1236
1312
|
lastCommand = command;
|
|
@@ -1309,13 +1385,16 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1309
1385
|
const specs = values.map(parseForwardSpec);
|
|
1310
1386
|
for (const spec of specs) await startForward(spec);
|
|
1311
1387
|
routeRemoteTools = false;
|
|
1388
|
+
credentialCache.resume = { command: state.command, cwd: state.cwd, routeRemoteTools, forwards: [...forwardSpecs.values()].map(serializeForward) };
|
|
1312
1389
|
status(ctx);
|
|
1390
|
+
persistSessionRemoteState();
|
|
1313
1391
|
ctx.ui.notify(`SSH remote port forwarding started; tools remain local in ${localCwd}: ${specs.map((spec) => `127.0.0.1:${spec.localPort}`).join(", ")}`, "info");
|
|
1314
1392
|
} catch (error) { ctx.ui.notify(`SSH remote port forwarding failed: ${(error as Error).message}`, "error"); }
|
|
1315
1393
|
return;
|
|
1316
1394
|
}
|
|
1317
1395
|
if (action === "unforward") {
|
|
1318
1396
|
await stopForwards();
|
|
1397
|
+
persistSessionRemoteState();
|
|
1319
1398
|
ctx.ui.notify("Closed all extension-managed SSH remote port forwards", "info");
|
|
1320
1399
|
return;
|
|
1321
1400
|
}
|
|
@@ -1378,19 +1457,50 @@ export default function sshRemoteExtension(pi: ExtensionAPI) {
|
|
|
1378
1457
|
pi.on("turn_start", () => { turnOutputBytes = 0; });
|
|
1379
1458
|
pi.on("session_start", async (event, ctx) => {
|
|
1380
1459
|
currentCtx = ctx;
|
|
1460
|
+
sessionReady = true;
|
|
1381
1461
|
status(ctx);
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1462
|
+
const saved = loadSessionRemoteState(ctx);
|
|
1463
|
+
const canInheritCurrent = event.reason === "reload" || event.reason === "new" || event.reason === "fork";
|
|
1464
|
+
const inherited = !saved && canInheritCurrent && credentialCache.resume ? credentialCache.resume : undefined;
|
|
1465
|
+
const target: SessionRemoteState | undefined = saved ?? (inherited ? {
|
|
1466
|
+
version: 1,
|
|
1467
|
+
connected: true,
|
|
1468
|
+
command: inherited.command,
|
|
1469
|
+
cwd: inherited.cwd,
|
|
1470
|
+
routeRemoteTools: inherited.routeRemoteTools,
|
|
1471
|
+
forwards: inherited.forwards ?? [],
|
|
1472
|
+
} : undefined);
|
|
1473
|
+
if (!target) {
|
|
1474
|
+
credentialCache.resume = undefined;
|
|
1475
|
+
return;
|
|
1385
1476
|
}
|
|
1477
|
+
restoringSessionState = true;
|
|
1478
|
+
try {
|
|
1479
|
+
await restoreSessionRemoteState(target, ctx);
|
|
1480
|
+
} catch (error) {
|
|
1481
|
+
ctx.ui.notify(`SSH remote session restore failed: ${(error as Error).message}`, "error");
|
|
1482
|
+
} finally {
|
|
1483
|
+
restoringSessionState = false;
|
|
1484
|
+
}
|
|
1485
|
+
if (inherited && remote) persistSessionRemoteState();
|
|
1386
1486
|
});
|
|
1387
|
-
pi.on("session_shutdown", (event) => {
|
|
1487
|
+
pi.on("session_shutdown", async (event) => {
|
|
1488
|
+
sessionReady = false;
|
|
1388
1489
|
const previous = remote;
|
|
1490
|
+
const preserveConnection = event.reason === "reload" || event.reason === "new" || event.reason === "fork";
|
|
1491
|
+
if (previous && preserveConnection) {
|
|
1492
|
+
credentialCache.resume = {
|
|
1493
|
+
command: previous.command,
|
|
1494
|
+
cwd: previous.cwd,
|
|
1495
|
+
routeRemoteTools,
|
|
1496
|
+
forwards: [...forwardSpecs.values()].map(serializeForward),
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1389
1499
|
remote = null;
|
|
1390
1500
|
routeRemoteTools = false;
|
|
1391
|
-
|
|
1501
|
+
await stopForwards();
|
|
1392
1502
|
previous?.client.end();
|
|
1393
|
-
if (
|
|
1503
|
+
if (!preserveConnection) credentialCache.resume = undefined;
|
|
1394
1504
|
});
|
|
1395
1505
|
pi.on("user_bash", async (event, ctx) => {
|
|
1396
1506
|
if (!remote || !routeRemoteTools) return undefined;
|