dsh-ssh-terminal 0.1.1 → 0.1.3

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 CHANGED
@@ -2,6 +2,33 @@
2
2
 
3
3
  All notable changes to this project are documented in this file.
4
4
 
5
+ ## [0.1.3] - 2026-09-08
6
+
7
+ ### Fixed
8
+
9
+ - **Desktop freeze (renderer spin)**: the log container used an inline
10
+ `ref={(el) => setLogEl(el)}` callback. React re-attaches an inline ref on
11
+ every commit, so the component re-rendered itself in a tight loop and
12
+ pinned the renderer main thread at 100%, freezing the whole DSH desktop
13
+ UI. The DOM node is now held in a stable `useRef`.
14
+ - **Unbounded history poisoning**: single command outputs were stored
15
+ without a cap (a real-world history file grew to 31 MB), and every
16
+ appended step re-serialized and rewrote the whole file synchronously.
17
+
18
+ ### Performance
19
+
20
+ - Per-event text is capped at 4000 chars; legacy oversized entries are
21
+ clipped at load. The interactive terminal stream is ring-trimmed to a
22
+ ~400 KB tail.
23
+ - History persistence is throttled to at most one write per second and
24
+ flushed when the plugin stops.
25
+ - `status` polling is rev-gated: when nothing changed the host answers a
26
+ tiny `unchanged` response instead of resending the transcript; the poll
27
+ interval is 1500 ms (was 600 ms) and the busy spinner is a CSS animation
28
+ instead of a 50 ms re-render loop.
29
+ - Per-connection step counts are cached per history revision instead of
30
+ scanning the whole history on every `list` call.
31
+
5
32
  ## [0.1.0] - 2026-08-29
6
33
 
7
34
  - Initial release.
package/lib/client.js CHANGED
@@ -13,7 +13,7 @@ window.__ModuleLoader__.load({
13
13
  return p(d.getHours()) + ":" + p(d.getMinutes()) + ":" + p(d.getSeconds());
14
14
  }
15
15
  const SSH_CSS = `
16
- .dshssh-body{display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}
16
+ .dshssh-body{position:relative;display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}
17
17
  .dshssh-head{display:flex;align-items:center;gap:8px;}
18
18
  .dshssh-title{font-weight:700;flex:1;}
19
19
  .dshssh-status{font-weight:600;}
@@ -34,7 +34,7 @@ window.__ModuleLoader__.load({
34
34
  .dshssh-pin{color:var(--dsw-alias-accent,#facc15);margin-right:3px;}
35
35
  .dshssh-session-meta{color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:10px;}
36
36
  .dshssh-empty{padding:6px;color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:11px;}
37
- .dshssh-context-menu{position:fixed;z-index:1000;display:flex;flex-direction:column;min-width:140px;padding:4px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));border-radius:7px;background:var(--dsw-alias-bg-layer-3,#252529);box-shadow:0 8px 24px rgba(0,0,0,.35);}
37
+ .dshssh-context-menu{position:absolute;z-index:1000;display:flex;flex-direction:column;min-width:140px;padding:4px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));border-radius:7px;background:var(--dsw-alias-bg-layer-3,#252529);box-shadow:0 8px 24px rgba(0,0,0,.35);}
38
38
  .dshssh-context-menu button{padding:6px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,inherit);text-align:left;cursor:pointer;font-size:12px;}
39
39
  .dshssh-context-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}
40
40
  .dshssh-context-menu button.danger{color:var(--dsw-alias-state-error-primary,#fca5a5);}
@@ -89,9 +89,10 @@ window.__ModuleLoader__.load({
89
89
  const [connected, setConnected] = (0, react.useState)(false);
90
90
  const [target, setTarget] = (0, react.useState)("");
91
91
  const [steps, setSteps] = (0, react.useState)([]);
92
- const [logEl, setLogEl] = (0, react.useState)(null);
92
+ const logRef = (0, react.useRef)(null);
93
93
  const [contextMenu, setContextMenu] = (0, react.useState)(null);
94
94
  const refreshSeq = (0, react.useRef)(0);
95
+ const statusRevRef = (0, react.useRef)({});
95
96
  const selectedSession = sessions.find((s) => s.connectionId === connectionId);
96
97
  const contextSession = contextMenu && sessions.find((s) => s.connectionId === contextMenu.connectionId);
97
98
  const canRun = connected && !!selectedSession && !!selectedSession.connected;
@@ -112,8 +113,13 @@ window.__ModuleLoader__.load({
112
113
  setSteps([]);
113
114
  return;
114
115
  }
115
- const t = await callApi("status", { connectionId: selected });
116
+ const t = await callApi("status", {
117
+ connectionId: selected,
118
+ rev: statusRevRef.current[selected] || 0
119
+ });
116
120
  if (seq !== refreshSeq.current) return;
121
+ if (t && t.unchanged) return;
122
+ if (t && typeof t.rev === "number") statusRevRef.current[selected] = t.rev;
117
123
  setConnected(!!(t && t.connected));
118
124
  setTarget(t && t.target || "");
119
125
  setSteps(t && t.steps || []);
@@ -126,7 +132,7 @@ window.__ModuleLoader__.load({
126
132
  return;
127
133
  }
128
134
  refresh(connectionId);
129
- const stop = ctx.interval(() => refresh(connectionId), 600);
135
+ const stop = ctx.interval(() => refresh(connectionId), 1500);
130
136
  return () => {
131
137
  refreshSeq.current += 1;
132
138
  try {
@@ -135,10 +141,10 @@ window.__ModuleLoader__.load({
135
141
  };
136
142
  }, [visible, connectionId]);
137
143
  (0, react.useEffect)(() => {
138
- const el = logEl;
144
+ const el = logRef.current;
139
145
  if (!el) return;
140
146
  if (el.scrollTop + el.clientHeight >= el.scrollHeight - 40) el.scrollTop = el.scrollHeight;
141
- }, [steps, logEl]);
147
+ }, [steps]);
142
148
  async function connect() {
143
149
  setBusy(true);
144
150
  setErr("");
@@ -238,8 +244,15 @@ window.__ModuleLoader__.load({
238
244
  onContextMenu: (e) => {
239
245
  e.preventDefault();
240
246
  e.stopPropagation();
241
- const x = Math.min(e.clientX, Math.max(4, window.innerWidth - 160));
242
- const y = Math.min(e.clientY, Math.max(4, window.innerHeight - 80));
247
+ const body = e.currentTarget.closest(".dshssh-body");
248
+ const rect = body ? body.getBoundingClientRect() : {
249
+ left: 0,
250
+ top: 0,
251
+ width: window.innerWidth,
252
+ height: window.innerHeight
253
+ };
254
+ const x = Math.max(4, Math.min(e.clientX - rect.left, rect.width - 160));
255
+ const y = Math.max(4, Math.min(e.clientY - rect.top, rect.height - 80));
243
256
  setContextMenu({
244
257
  connectionId: s.connectionId,
245
258
  x,
@@ -293,7 +306,7 @@ window.__ModuleLoader__.load({
293
306
  onClick: disconnect
294
307
  }, "断开")), (0, react.createElement)("div", {
295
308
  className: "dshssh-log",
296
- ref: (el) => setLogEl(el)
309
+ ref: logRef
297
310
  }, steps.length ? steps.map((s, i) => (0, react.createElement)("div", {
298
311
  key: i,
299
312
  className: "dshssh-step dshssh-" + s.kind
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState","useRef","createElement"],"sources":["../src/client/index.tsx"],"sourcesContent":["import { createElement, useState, useEffect, useRef } from 'react'\n\nconst API = '/ssh-terminal/api'\n\nfunction fmt(ts) {\n const d = new Date(ts)\n const p = (n) => String(n).padStart(2, '0')\n return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())\n}\n\nconst SSH_CSS = `\n.dshssh-body{display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}\n.dshssh-head{display:flex;align-items:center;gap:8px;}\n.dshssh-title{font-weight:700;flex:1;}\n.dshssh-status{font-weight:600;}\n.dshssh-status.ok{color:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-status.off{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-layout{display:flex;gap:8px;flex:1;min-height:0;}\n.dshssh-sessions{width:150px;min-width:125px;display:flex;flex-direction:column;gap:6px;min-height:0;}\n.dshssh-session-title{font-size:11px;font-weight:700;color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-session-list{display:flex;flex-direction:column;gap:5px;overflow:auto;min-height:0;}\n.dshssh-session{display:flex;align-items:center;gap:6px;width:100%;padding:6px 7px;text-align:left;border-radius:7px;border:1px solid transparent;background:var(--dsw-alias-bg-layer-2,rgba(148,163,184,.08));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:11px;}\n.dshssh-session:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-session.selected{border-color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-session-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-session-dot.ok{background:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-session-copy{min-width:0;display:flex;flex-direction:column;gap:2px;}\n.dshssh-session-name,.dshssh-session-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.dshssh-session-name{font-weight:600;}\n.dshssh-pin{color:var(--dsw-alias-accent,#facc15);margin-right:3px;}\n.dshssh-session-meta{color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:10px;}\n.dshssh-empty{padding:6px;color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:11px;}\n.dshssh-context-menu{position:fixed;z-index:1000;display:flex;flex-direction:column;min-width:140px;padding:4px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));border-radius:7px;background:var(--dsw-alias-bg-layer-3,#252529);box-shadow:0 8px 24px rgba(0,0,0,.35);}\n.dshssh-context-menu button{padding:6px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,inherit);text-align:left;cursor:pointer;font-size:12px;}\n.dshssh-context-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-context-menu button.danger{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-main{display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;min-height:0;}\n.dshssh-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;}\n.dshssh-input{padding:4px 7px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-bg-layer-3,rgba(2,6,23,.5));color:var(--dsw-alias-label-primary,inherit);font-size:12px;min-width:70px;}\n.dshssh-input::placeholder{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-btn{padding:4px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:12px;}\n.dshssh-btn:hover{background:var(--dsw-alias-interactive-bg-active,rgba(148,163,184,.28));}\n.dshssh-btn:disabled{opacity:.5;cursor:default;}\n.dshssh-log{flex:1;min-height:120px;overflow:auto;background:var(--dsw-alias-bg-layer-1,rgba(2,6,23,.55));border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.25));border-radius:8px;padding:8px;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word;}\n.dshssh-step{margin:0 0 6px;}\n.dshssh-cmd{color:var(--dsw-alias-accent,#93c5fd);}\n.dshssh-cmd::before{content:\"❯ \";color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-out{color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-connect{color:var(--dsw-alias-state-success-primary,#86efac);}\n.dshssh-disconnect{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-system{color:var(--dsw-alias-label-tertiary,#94a3b8);font-style:italic;}\n.dshssh-err{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n`\n\nasync function callApi(action, extra = undefined) {\n const res = await fetch(API, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action, ...(extra || {}) }),\n })\n return await res.json()\n}\n\nfunction injectCss() {\n if (typeof document === 'undefined') return\n if (document.querySelector('style[data-plugin-css=\"dsh-ssh-terminal\"]')) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-ssh-terminal'\n tag.dataset.pluginCss = 'dsh-ssh-terminal'\n tag.textContent = SSH_CSS\n document.head.appendChild(tag)\n}\n\nfunction SshBody(props) {\n const ctx = props.ctx\n const visible = props.visible === undefined ? true : props.visible\n const [busy, setBusy] = useState(false)\n const [err, setErr] = useState('')\n const [hostV, setHostV] = useState('')\n const [userV, setUserV] = useState('')\n const [portV, setPortV] = useState('22')\n const [passV, setPassV] = useState('')\n const [cmdV, setCmdV] = useState('')\n const [sessions, setSessions] = useState([])\n const [connectionId, setConnectionId] = useState('')\n const [connected, setConnected] = useState(false)\n const [target, setTarget] = useState('')\n const [steps, setSteps] = useState([])\n const [logEl, setLogEl] = useState(null)\n const [contextMenu, setContextMenu] = useState(null)\n const refreshSeq = useRef(0)\n\n const selectedSession = sessions.find((s) => s.connectionId === connectionId)\n const contextSession = contextMenu && sessions.find((s) => s.connectionId === contextMenu.connectionId)\n const canRun = connected && !!selectedSession && !!selectedSession.connected\n\n async function refresh(preferredId) {\n const seq = ++refreshSeq.current\n try {\n const list = await callApi('list')\n if (seq !== refreshSeq.current) return\n const nextSessions = Array.isArray(list && list.sessions) ? list.sessions : []\n setSessions(nextSessions)\n const preferred = preferredId || connectionId\n const selected = nextSessions.some((s) => s.connectionId === preferred)\n ? preferred\n : (list && list.activeConnectionId && nextSessions.some((s) => s.connectionId === list.activeConnectionId)\n ? list.activeConnectionId\n : ((nextSessions.find((s) => s.connected) || nextSessions[0] || {}).connectionId || ''))\n if (selected !== connectionId) setConnectionId(selected)\n if (!selected) {\n if (seq !== refreshSeq.current) return\n setConnected(false)\n setTarget('')\n setSteps([])\n return\n }\n const t = await callApi('status', { connectionId: selected })\n if (seq !== refreshSeq.current) return\n setConnected(!!(t && t.connected))\n setTarget((t && t.target) || '')\n setSteps((t && t.steps) || [])\n } catch (e) {}\n }\n\n useEffect(() => {\n injectCss()\n if (visible === false) { refreshSeq.current += 1; return }\n refresh(connectionId)\n const stop = ctx.interval(() => refresh(connectionId), 600)\n return () => {\n refreshSeq.current += 1\n try { stop() } catch (e) {}\n }\n }, [visible, connectionId])\n\n useEffect(() => {\n const el = logEl\n if (!el) return\n const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 40\n if (nearBottom) el.scrollTop = el.scrollHeight\n }, [steps, logEl])\n\n async function connect() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('connect', { host: hostV, user: userV, port: Number(portV) || 22, password: passV || undefined })\n if (r && r.error) setErr(String(r.error))\n await refresh(r && r.connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function run() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('exec', { connectionId, command: cmdV, waitMs: 15000 })\n if (r && r.error) setErr(String(r.error))\n await refresh(connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function disconnect() {\n setBusy(true); setErr('')\n try { await callApi('disconnect', { connectionId }); await refresh(connectionId) }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function clearHistory() {\n try {\n if (window.confirm('确定清空当前连接的 SSH 操作记录吗?')) {\n await callApi('clear', connectionId ? { connectionId } : undefined)\n await refresh(connectionId)\n }\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function togglePinned(id) {\n setContextMenu(null)\n try {\n const r = await callApi('pin', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function deleteRecord(id) {\n const session = sessions.find((s) => s.connectionId === id)\n setContextMenu(null)\n if (!session || !window.confirm('删除该连接记录吗?在线连接不会断开。')) return\n try {\n const r = await callApi('delete', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n const onKey = (e) => { if (e.key === 'Enter' && canRun && !busy) run() }\n const status = connected ? ('● ' + target) : (selectedSession ? (selectedSession.exited ? '○ 已退出' : '○ 未连接') : '○ 未选择连接')\n\n return createElement('div', { className: 'dshssh-body', onClick: () => setContextMenu(null) },\n createElement('div', { className: 'dshssh-head' },\n createElement('span', { className: 'dshssh-title' }, 'SSH 远程终端'),\n createElement('span', { className: 'dshssh-status ' + (connected ? 'ok' : 'off') }, status),\n createElement('button', { className: 'dshssh-btn dshssh-clear', onClick: clearHistory }, '清空当前'),\n ),\n createElement('div', { className: 'dshssh-layout' },\n createElement('div', { className: 'dshssh-sessions' },\n createElement('div', { className: 'dshssh-session-title' }, '连接列表 (' + sessions.length + ')'),\n createElement('div', { className: 'dshssh-session-list' },\n sessions.length\n ? sessions.map((s) => createElement('button', {\n key: s.connectionId,\n className: 'dshssh-session' + (s.connectionId === connectionId ? ' selected' : ''),\n onClick: () => { setConnectionId(s.connectionId); setErr('') },\n onContextMenu: (e) => {\n e.preventDefault()\n e.stopPropagation()\n const x = Math.min(e.clientX, Math.max(4, window.innerWidth - 160))\n const y = Math.min(e.clientY, Math.max(4, window.innerHeight - 80))\n setContextMenu({ connectionId: s.connectionId, x, y })\n },\n },\n createElement('span', { className: 'dshssh-session-dot' + (s.connected ? ' ok' : '') }),\n createElement('span', { className: 'dshssh-session-copy' },\n createElement('span', { className: 'dshssh-session-name' },\n s.pinned ? createElement('span', { className: 'dshssh-pin', title: '已置顶' }, '★') : null,\n s.label || s.target || 'SSH 连接',\n ),\n createElement('span', { className: 'dshssh-session-meta' }, s.connected ? (s.busy ? '执行中' : '已连接') : (s.historical ? '历史记录' : '已断开')),\n )))\n : createElement('div', { className: 'dshssh-empty' }, '暂无连接'),\n ),\n ),\n createElement('div', { className: 'dshssh-main' },\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input', placeholder: '主机', value: hostV, onChange: (e) => setHostV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', placeholder: '用户', value: userV, onChange: (e) => setUserV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 54 }, placeholder: '端口', value: portV, onChange: (e) => setPortV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 96 }, type: 'password', placeholder: '密码(可选)', value: passV, onChange: (e) => setPassV(e.target.value) }),\n createElement('button', { className: 'dshssh-btn', disabled: busy, onClick: connect }, '新建连接'),\n ),\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input dshssh-cmdline', style: { flex: 1 }, placeholder: '输入命令后回车或点执行', value: cmdV, onChange: (e) => setCmdV(e.target.value), onKeyDown: onKey }),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: run }, '执行'),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: disconnect }, '断开'),\n ),\n createElement('div', { className: 'dshssh-log', ref: (el) => setLogEl(el) },\n steps.length\n ? steps.map((s, i) => createElement('div', { key: i, className: 'dshssh-step dshssh-' + s.kind }, (fmt(s.ts) + ' ' + s.text)))\n : createElement('div', { className: 'dshssh-step dshssh-system' }, '还没有操作。'),\n ),\n err ? createElement('div', { className: 'dshssh-err' }, String(err)) : null,\n ),\n contextMenu && contextSession\n ? createElement('div', {\n className: 'dshssh-context-menu',\n style: { left: contextMenu.x, top: contextMenu.y },\n onClick: (e) => e.stopPropagation(),\n },\n createElement('button', { type: 'button', onClick: () => togglePinned(contextSession.connectionId) }, contextSession.pinned ? '取消置顶' : '置顶'),\n createElement('button', { type: 'button', className: 'danger', onClick: () => deleteRecord(contextSession.connectionId) }, '删除该连接记录'),\n )\n : null,\n ),\n )\n}\n\nexport const inject = ['timer', 'betterSidebar']\n\nexport function apply(ctx) {\n const bs = ctx.get('betterSidebar')\n if (!bs) return\n ctx.effect(() => bs.registerTab({\n id: 'ssh-remote:terminal',\n title: 'SSH 终端',\n order: 60,\n single: true,\n component: (p) => createElement(SshBody, { ctx, visible: p.visible }),\n }))\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState","useRef","createElement"],"sources":["../src/client/index.tsx"],"sourcesContent":["import { createElement, useState, useEffect, useRef } from 'react'\n\nconst API = '/ssh-terminal/api'\n\nfunction fmt(ts) {\n const d = new Date(ts)\n const p = (n) => String(n).padStart(2, '0')\n return p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())\n}\n\nconst SSH_CSS = `\n.dshssh-body{position:relative;display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;padding:10px;color:var(--dsw-alias-label-primary,inherit);}\n.dshssh-head{display:flex;align-items:center;gap:8px;}\n.dshssh-title{font-weight:700;flex:1;}\n.dshssh-status{font-weight:600;}\n.dshssh-status.ok{color:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-status.off{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-layout{display:flex;gap:8px;flex:1;min-height:0;}\n.dshssh-sessions{width:150px;min-width:125px;display:flex;flex-direction:column;gap:6px;min-height:0;}\n.dshssh-session-title{font-size:11px;font-weight:700;color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-session-list{display:flex;flex-direction:column;gap:5px;overflow:auto;min-height:0;}\n.dshssh-session{display:flex;align-items:center;gap:6px;width:100%;padding:6px 7px;text-align:left;border-radius:7px;border:1px solid transparent;background:var(--dsw-alias-bg-layer-2,rgba(148,163,184,.08));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:11px;}\n.dshssh-session:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-session.selected{border-color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-session-dot{width:7px;height:7px;flex:0 0 7px;border-radius:50%;background:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-session-dot.ok{background:var(--dsw-alias-state-success-primary,#22c55e);}\n.dshssh-session-copy{min-width:0;display:flex;flex-direction:column;gap:2px;}\n.dshssh-session-name,.dshssh-session-meta{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.dshssh-session-name{font-weight:600;}\n.dshssh-pin{color:var(--dsw-alias-accent,#facc15);margin-right:3px;}\n.dshssh-session-meta{color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:10px;}\n.dshssh-empty{padding:6px;color:var(--dsw-alias-label-tertiary,#94a3b8);font-size:11px;}\n.dshssh-context-menu{position:absolute;z-index:1000;display:flex;flex-direction:column;min-width:140px;padding:4px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));border-radius:7px;background:var(--dsw-alias-bg-layer-3,#252529);box-shadow:0 8px 24px rgba(0,0,0,.35);}\n.dshssh-context-menu button{padding:6px 8px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary,inherit);text-align:left;cursor:pointer;font-size:12px;}\n.dshssh-context-menu button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));}\n.dshssh-context-menu button.danger{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-main{display:flex;flex-direction:column;gap:8px;flex:1;min-width:0;min-height:0;}\n.dshssh-form{display:flex;flex-wrap:wrap;gap:6px;align-items:center;}\n.dshssh-input{padding:4px 7px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-bg-layer-3,rgba(2,6,23,.5));color:var(--dsw-alias-label-primary,inherit);font-size:12px;min-width:70px;}\n.dshssh-input::placeholder{color:var(--dsw-alias-label-tertiary,#94a3b8);}\n.dshssh-btn{padding:4px 10px;border-radius:6px;border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.4));background:var(--dsw-alias-interactive-bg-hover,rgba(148,163,184,.15));color:var(--dsw-alias-label-primary,inherit);cursor:pointer;font-size:12px;}\n.dshssh-btn:hover{background:var(--dsw-alias-interactive-bg-active,rgba(148,163,184,.28));}\n.dshssh-btn:disabled{opacity:.5;cursor:default;}\n.dshssh-log{flex:1;min-height:120px;overflow:auto;background:var(--dsw-alias-bg-layer-1,rgba(2,6,23,.55));border:1px solid var(--dsw-alias-border-l2,rgba(148,163,184,.25));border-radius:8px;padding:8px;font-family:var(--dsw-font-mono,ui-monospace,SFMono-Regular,Menlo,Consolas,monospace);font-size:12px;line-height:1.45;white-space:pre-wrap;word-break:break-word;}\n.dshssh-step{margin:0 0 6px;}\n.dshssh-cmd{color:var(--dsw-alias-accent,#93c5fd);}\n.dshssh-cmd::before{content:\"❯ \";color:var(--dsw-alias-accent,#60a5fa);}\n.dshssh-out{color:var(--dsw-alias-label-secondary,#d1d5db);}\n.dshssh-connect{color:var(--dsw-alias-state-success-primary,#86efac);}\n.dshssh-disconnect{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n.dshssh-system{color:var(--dsw-alias-label-tertiary,#94a3b8);font-style:italic;}\n.dshssh-err{color:var(--dsw-alias-state-error-primary,#fca5a5);}\n`\n\nasync function callApi(action, extra = undefined) {\n const res = await fetch(API, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ action, ...(extra || {}) }),\n })\n return await res.json()\n}\n\nfunction injectCss() {\n if (typeof document === 'undefined') return\n if (document.querySelector('style[data-plugin-css=\"dsh-ssh-terminal\"]')) return\n const tag = document.createElement('style')\n tag.dataset.plugin = 'dsh-ssh-terminal'\n tag.dataset.pluginCss = 'dsh-ssh-terminal'\n tag.textContent = SSH_CSS\n document.head.appendChild(tag)\n}\n\nfunction SshBody(props) {\n const ctx = props.ctx\n const visible = props.visible === undefined ? true : props.visible\n const [busy, setBusy] = useState(false)\n const [err, setErr] = useState('')\n const [hostV, setHostV] = useState('')\n const [userV, setUserV] = useState('')\n const [portV, setPortV] = useState('22')\n const [passV, setPassV] = useState('')\n const [cmdV, setCmdV] = useState('')\n const [sessions, setSessions] = useState([])\n const [connectionId, setConnectionId] = useState('')\n const [connected, setConnected] = useState(false)\n const [target, setTarget] = useState('')\n const [steps, setSteps] = useState([])\n const logRef = useRef(null)\n const [contextMenu, setContextMenu] = useState(null)\n const refreshSeq = useRef(0)\n const statusRevRef = useRef({})\n\n const selectedSession = sessions.find((s) => s.connectionId === connectionId)\n const contextSession = contextMenu && sessions.find((s) => s.connectionId === contextMenu.connectionId)\n const canRun = connected && !!selectedSession && !!selectedSession.connected\n\n async function refresh(preferredId) {\n const seq = ++refreshSeq.current\n try {\n const list = await callApi('list')\n if (seq !== refreshSeq.current) return\n const nextSessions = Array.isArray(list && list.sessions) ? list.sessions : []\n setSessions(nextSessions)\n const preferred = preferredId || connectionId\n const selected = nextSessions.some((s) => s.connectionId === preferred)\n ? preferred\n : (list && list.activeConnectionId && nextSessions.some((s) => s.connectionId === list.activeConnectionId)\n ? list.activeConnectionId\n : ((nextSessions.find((s) => s.connected) || nextSessions[0] || {}).connectionId || ''))\n if (selected !== connectionId) setConnectionId(selected)\n if (!selected) {\n if (seq !== refreshSeq.current) return\n setConnected(false)\n setTarget('')\n setSteps([])\n return\n }\n // Rev-gated status: the host answers with a tiny unchanged marker\n // unless new steps appeared, so idle polling stays cheap.\n const echoed = statusRevRef.current[selected] || 0\n const t = await callApi('status', { connectionId: selected, rev: echoed })\n if (seq !== refreshSeq.current) return\n if (t && t.unchanged) return\n if (t && typeof t.rev === 'number') statusRevRef.current[selected] = t.rev\n setConnected(!!(t && t.connected))\n setTarget((t && t.target) || '')\n setSteps((t && t.steps) || [])\n } catch (e) {}\n }\n\n useEffect(() => {\n injectCss()\n if (visible === false) { refreshSeq.current += 1; return }\n refresh(connectionId)\n const stop = ctx.interval(() => refresh(connectionId), 1500)\n return () => {\n refreshSeq.current += 1\n try { stop() } catch (e) {}\n }\n }, [visible, connectionId])\n\n useEffect(() => {\n const el = logRef.current\n if (!el) return\n const nearBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 40\n if (nearBottom) el.scrollTop = el.scrollHeight\n }, [steps])\n\n async function connect() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('connect', { host: hostV, user: userV, port: Number(portV) || 22, password: passV || undefined })\n if (r && r.error) setErr(String(r.error))\n await refresh(r && r.connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function run() {\n setBusy(true); setErr('')\n try {\n const r = await callApi('exec', { connectionId, command: cmdV, waitMs: 15000 })\n if (r && r.error) setErr(String(r.error))\n await refresh(connectionId)\n }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function disconnect() {\n setBusy(true); setErr('')\n try { await callApi('disconnect', { connectionId }); await refresh(connectionId) }\n catch (e) { setErr(String((e && e.message) || e)) } finally { setBusy(false) }\n }\n async function clearHistory() {\n try {\n if (window.confirm('确定清空当前连接的 SSH 操作记录吗?')) {\n await callApi('clear', connectionId ? { connectionId } : undefined)\n await refresh(connectionId)\n }\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function togglePinned(id) {\n setContextMenu(null)\n try {\n const r = await callApi('pin', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n async function deleteRecord(id) {\n const session = sessions.find((s) => s.connectionId === id)\n setContextMenu(null)\n if (!session || !window.confirm('删除该连接记录吗?在线连接不会断开。')) return\n try {\n const r = await callApi('delete', { connectionId: id })\n if (r && r.error) setErr(String(r.error))\n await refresh(id)\n } catch (e) { setErr(String((e && e.message) || e)) }\n }\n\n const onKey = (e) => { if (e.key === 'Enter' && canRun && !busy) run() }\n const status = connected ? ('● ' + target) : (selectedSession ? (selectedSession.exited ? '○ 已退出' : '○ 未连接') : '○ 未选择连接')\n\n return createElement('div', { className: 'dshssh-body', onClick: () => setContextMenu(null) },\n createElement('div', { className: 'dshssh-head' },\n createElement('span', { className: 'dshssh-title' }, 'SSH 远程终端'),\n createElement('span', { className: 'dshssh-status ' + (connected ? 'ok' : 'off') }, status),\n createElement('button', { className: 'dshssh-btn dshssh-clear', onClick: clearHistory }, '清空当前'),\n ),\n createElement('div', { className: 'dshssh-layout' },\n createElement('div', { className: 'dshssh-sessions' },\n createElement('div', { className: 'dshssh-session-title' }, '连接列表 (' + sessions.length + ')'),\n createElement('div', { className: 'dshssh-session-list' },\n sessions.length\n ? sessions.map((s) => createElement('button', {\n key: s.connectionId,\n className: 'dshssh-session' + (s.connectionId === connectionId ? ' selected' : ''),\n onClick: () => { setConnectionId(s.connectionId); setErr('') },\n onContextMenu: (e) => {\n e.preventDefault()\n e.stopPropagation()\n const body = e.currentTarget.closest('.dshssh-body')\n const rect = body ? body.getBoundingClientRect() : { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }\n const x = Math.max(4, Math.min(e.clientX - rect.left, rect.width - 160))\n const y = Math.max(4, Math.min(e.clientY - rect.top, rect.height - 80))\n setContextMenu({ connectionId: s.connectionId, x, y })\n },\n },\n createElement('span', { className: 'dshssh-session-dot' + (s.connected ? ' ok' : '') }),\n createElement('span', { className: 'dshssh-session-copy' },\n createElement('span', { className: 'dshssh-session-name' },\n s.pinned ? createElement('span', { className: 'dshssh-pin', title: '已置顶' }, '★') : null,\n s.label || s.target || 'SSH 连接',\n ),\n createElement('span', { className: 'dshssh-session-meta' }, s.connected ? (s.busy ? '执行中' : '已连接') : (s.historical ? '历史记录' : '已断开')),\n )))\n : createElement('div', { className: 'dshssh-empty' }, '暂无连接'),\n ),\n ),\n createElement('div', { className: 'dshssh-main' },\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input', placeholder: '主机', value: hostV, onChange: (e) => setHostV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', placeholder: '用户', value: userV, onChange: (e) => setUserV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 54 }, placeholder: '端口', value: portV, onChange: (e) => setPortV(e.target.value) }),\n createElement('input', { className: 'dshssh-input', style: { width: 96 }, type: 'password', placeholder: '密码(可选)', value: passV, onChange: (e) => setPassV(e.target.value) }),\n createElement('button', { className: 'dshssh-btn', disabled: busy, onClick: connect }, '新建连接'),\n ),\n createElement('div', { className: 'dshssh-form' },\n createElement('input', { className: 'dshssh-input dshssh-cmdline', style: { flex: 1 }, placeholder: '输入命令后回车或点执行', value: cmdV, onChange: (e) => setCmdV(e.target.value), onKeyDown: onKey }),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: run }, '执行'),\n createElement('button', { className: 'dshssh-btn', disabled: busy || !canRun, onClick: disconnect }, '断开'),\n ),\n createElement('div', { className: 'dshssh-log', ref: logRef },\n steps.length\n ? steps.map((s, i) => createElement('div', { key: i, className: 'dshssh-step dshssh-' + s.kind }, (fmt(s.ts) + ' ' + s.text)))\n : createElement('div', { className: 'dshssh-step dshssh-system' }, '还没有操作。'),\n ),\n err ? createElement('div', { className: 'dshssh-err' }, String(err)) : null,\n ),\n contextMenu && contextSession\n ? createElement('div', {\n className: 'dshssh-context-menu',\n style: { left: contextMenu.x, top: contextMenu.y },\n onClick: (e) => e.stopPropagation(),\n },\n createElement('button', { type: 'button', onClick: () => togglePinned(contextSession.connectionId) }, contextSession.pinned ? '取消置顶' : '置顶'),\n createElement('button', { type: 'button', className: 'danger', onClick: () => deleteRecord(contextSession.connectionId) }, '删除该连接记录'),\n )\n : null,\n ),\n )\n}\n\nexport const inject = ['timer', 'betterSidebar']\n\nexport function apply(ctx) {\n const bs = ctx.get('betterSidebar')\n if (!bs) return\n ctx.effect(() => bs.registerTab({\n id: 'ssh-remote:terminal',\n title: 'SSH 终端',\n order: 60,\n single: true,\n component: (p) => createElement(SshBody, { ctx, visible: p.visible }),\n }))\n}\n"],"mappings":"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"}
package/lib/index.js CHANGED
@@ -16,8 +16,16 @@ export function apply(ctx) {
16
16
  const HISTORY_PATH = join(homedir(), '.dsh', 'ssh-terminal-history.json')
17
17
  const MAX_HISTORY = 1200
18
18
  const MAX_STEPS = 600
19
+ // One event's text cap: a single huge command output must not poison the
20
+ // history file, persist() and every status poll with megabytes of JSON.
21
+ const MAX_TEXT = 4000
22
+ // Interactive terminal log ring: trim s.log once it grows past the keep
23
+ // size plus a buffer, so prompt detection keeps working.
24
+ const LOG_KEEP = 400000
19
25
  const LEGACY_SESSION_ID = 'legacy'
26
+ const clipText = (t) => (t.length > MAX_TEXT ? t.slice(0, MAX_TEXT) + '\n…[truncated]' : t)
20
27
  let history = []
28
+ let loadedDirty = false
21
29
  const pinnedConnectionIds = new Set()
22
30
  try {
23
31
  const raw = readFileSync(HISTORY_PATH, 'utf8')
@@ -29,26 +37,46 @@ export function apply(ctx) {
29
37
  }
30
38
  }
31
39
  if (Array.isArray(entries)) {
32
- history = entries.map((entry) => ({
33
- connectionId: entry && entry.connectionId ? String(entry.connectionId) : LEGACY_SESSION_ID,
34
- kind: entry && entry.kind ? String(entry.kind) : 'system',
35
- text: entry && entry.text != null ? String(entry.text) : '',
36
- ts: entry && Number(entry.ts) ? Number(entry.ts) : Date.now(),
37
- ...(entry && entry.target ? { target: String(entry.target) } : {}),
38
- ...(entry && entry.label ? { label: String(entry.label) } : {}),
39
- }))
40
+ history = entries.map((entry) => {
41
+ const rawText = entry && entry.text != null ? String(entry.text) : ''
42
+ const text = clipText(rawText)
43
+ if (text.length !== rawText.length) loadedDirty = true
44
+ return {
45
+ connectionId: entry && entry.connectionId ? String(entry.connectionId) : LEGACY_SESSION_ID,
46
+ kind: entry && entry.kind ? String(entry.kind) : 'system',
47
+ text,
48
+ ts: entry && Number(entry.ts) ? Number(entry.ts) : Date.now(),
49
+ ...(entry && entry.target ? { target: String(entry.target) } : {}),
50
+ ...(entry && entry.label ? { label: String(entry.label) } : {}),
51
+ }
52
+ })
40
53
  }
41
54
  } catch (e) {}
42
55
  if (history.length > MAX_HISTORY) history = history.slice(-MAX_HISTORY)
43
56
 
44
57
  let activeSessionId = null
45
58
  let lastTarget = ''
59
+ let persistScheduled = false
60
+ let lastPersistAt = 0
46
61
  function persist() {
47
62
  try {
48
63
  mkdirSync(dirname(HISTORY_PATH), { recursive: true })
49
64
  writeFileSync(HISTORY_PATH, JSON.stringify({ version: 2, events: history, pinnedConnectionIds: Array.from(pinnedConnectionIds) }))
50
65
  } catch (e) {}
51
66
  }
67
+ // persist() stringifies the whole history; appendStep fires per output
68
+ // chunk, so coalesce writes to at most one per second and flush on stop.
69
+ function schedulePersist() {
70
+ if (persistScheduled) return
71
+ persistScheduled = true
72
+ void (async () => {
73
+ await ctx.timeout(Math.max(0, 1000 - (Date.now() - lastPersistAt)))
74
+ persistScheduled = false
75
+ lastPersistAt = Date.now()
76
+ persist()
77
+ })()
78
+ }
79
+ if (loadedDirty) schedulePersist()
52
80
 
53
81
  function ensureMeta(connectionId) {
54
82
  let meta = connectionMeta.get(connectionId)
@@ -80,7 +108,7 @@ export function apply(ctx) {
80
108
  const event = {
81
109
  connectionId: connectionId || LEGACY_SESSION_ID,
82
110
  kind,
83
- text: String(text),
111
+ text: clipText(String(text)),
84
112
  ts: Date.now(),
85
113
  ...(extra && extra.target ? { target: String(extra.target) } : {}),
86
114
  ...(extra && extra.label ? { label: String(extra.label) } : {}),
@@ -94,7 +122,8 @@ export function apply(ctx) {
94
122
  meta.lastActivityAt = event.ts
95
123
  if (kind === 'connect') meta.connected = true
96
124
  if (kind === 'disconnect') meta.connected = false
97
- persist()
125
+ bumpRev()
126
+ schedulePersist()
98
127
  }
99
128
 
100
129
  function startDrain(s) {
@@ -106,6 +135,9 @@ export function apply(ctx) {
106
135
  const text = cleanStream(decoder.decode(chunk, { stream: true }))
107
136
  if (!text) continue
108
137
  s.log += text
138
+ // Ring-trim the interactive log so a chatty remote cannot grow
139
+ // host memory without bound; keep the tail for prompt detection.
140
+ if (s.log.length > LOG_KEEP + 100000) s.log = s.log.slice(-LOG_KEEP)
109
141
  s.lastActivityAt = Date.now()
110
142
  tryPassword(s)
111
143
  }
@@ -329,6 +361,22 @@ export function apply(ctx) {
329
361
  return { ok: true, connectionId, disconnected: true, target: s.target }
330
362
  }
331
363
 
364
+ // Cheap change detection for status polling: every history or session
365
+ // state mutation bumps a global rev; pollers echo back the rev they last
366
+ // saw and get a tiny "same" response instead of the full transcript.
367
+ let historyRev = 1
368
+ function bumpRev() { historyRev++ }
369
+ let countsAtRev = 0
370
+ const stepCounts = new Map()
371
+ function stepCountOf(connectionId) {
372
+ if (countsAtRev !== historyRev) {
373
+ stepCounts.clear()
374
+ for (const event of history) stepCounts.set(event.connectionId, (stepCounts.get(event.connectionId) || 0) + 1)
375
+ countsAtRev = historyRev
376
+ }
377
+ return stepCounts.get(connectionId) || 0
378
+ }
379
+
332
380
  function sessionSummary(s) {
333
381
  const meta = ensureMeta(s.connectionId)
334
382
  return {
@@ -341,7 +389,7 @@ export function apply(ctx) {
341
389
  historical: false,
342
390
  pinned: !!meta.pinned,
343
391
  lastActivityAt: s.lastActivityAt,
344
- stepCount: history.reduce((count, event) => count + (event.connectionId === s.connectionId ? 1 : 0), 0),
392
+ stepCount: stepCountOf(s.connectionId),
345
393
  }
346
394
  }
347
395
 
@@ -360,31 +408,39 @@ export function apply(ctx) {
360
408
  historical: true,
361
409
  pinned: !!meta.pinned,
362
410
  lastActivityAt: meta.lastActivityAt,
363
- stepCount: history.reduce((count, event) => count + (event.connectionId === meta.connectionId ? 1 : 0), 0),
411
+ stepCount: stepCountOf(meta.connectionId),
364
412
  })
365
413
  }
366
414
  const sessionsList = Array.from(listed.values()).sort((a, b) => Number(b.pinned) - Number(a.pinned) || b.lastActivityAt - a.lastActivityAt)
367
415
  return { sessions: sessionsList, activeConnectionId: activeSessionId && sessions.has(activeSessionId) ? activeSessionId : null }
368
416
  }
369
417
 
370
- function transcriptFor(connectionId) {
418
+ function transcriptFor(connectionId, clientRev, sameOk) {
371
419
  const s = sessions.get(connectionId)
372
420
  const meta = connectionMeta.get(connectionId)
373
- return {
421
+ const base = {
374
422
  connectionId,
375
423
  label: s ? s.label : (meta && meta.label) || (connectionId === LEGACY_SESSION_ID ? '历史记录' : ''),
376
424
  connected: !!(s && s.connected && !s.exited),
377
425
  exited: !!(!s || s.exited),
378
426
  busy: !!(s && s.busy),
379
427
  target: s ? s.target : (meta && meta.target) || (connectionId === LEGACY_SESSION_ID ? '历史记录' : lastTarget),
380
- steps: history.filter((event) => event.connectionId === connectionId).slice(-MAX_STEPS),
428
+ rev: historyRev,
381
429
  }
430
+ // Echoed rev matches → steps unchanged: skip the history scan entirely.
431
+ if (sameOk && clientRev === historyRev) return { ...base, unchanged: true }
432
+ const steps = []
433
+ for (let i = history.length - 1; i >= 0 && steps.length < MAX_STEPS; i--) {
434
+ if (history[i].connectionId === connectionId) steps.push(history[i])
435
+ }
436
+ steps.reverse()
437
+ return { ...base, steps }
382
438
  }
383
439
 
384
- function transcript(requestedId) {
440
+ function transcript(requestedId, clientRev) {
385
441
  const connectionId = resolveConnectionId(requestedId, true)
386
- if (connectionId) return transcriptFor(connectionId)
387
- return { connectionId: null, connected: false, target: lastTarget, steps: history.slice(-MAX_STEPS) }
442
+ if (connectionId) return transcriptFor(connectionId, clientRev, true)
443
+ return { connectionId: null, connected: false, target: lastTarget, rev: historyRev, steps: history.slice(-MAX_STEPS) }
388
444
  }
389
445
 
390
446
  function clearHistory(requestedId) {
@@ -400,6 +456,7 @@ export function apply(ctx) {
400
456
  connectionMeta.clear()
401
457
  pinnedConnectionIds.clear()
402
458
  }
459
+ bumpRev()
403
460
  persist()
404
461
  return { ok: true, cleared: true, connectionId }
405
462
  }
@@ -424,6 +481,7 @@ export function apply(ctx) {
424
481
  pinnedConnectionIds.delete(connectionId)
425
482
  if (!sessions.has(connectionId)) connectionMeta.delete(connectionId)
426
483
  else ensureMeta(connectionId).pinned = false
484
+ bumpRev()
427
485
  persist()
428
486
  return { ok: true, deleted: true, connectionId }
429
487
  }
@@ -523,7 +581,7 @@ export function apply(ctx) {
523
581
  else if (action === 'connect') out = await doConnect(data)
524
582
  else if (action === 'exec') out = await doExec(data.command, data)
525
583
  else if (action === 'disconnect') out = await doDisconnect(data.connectionId)
526
- else out = transcript(data.connectionId)
584
+ else out = transcript(data.connectionId, data.rev)
527
585
  send(res, out)
528
586
  } catch (e) {
529
587
  send(res, { ok: false, error: ((e && e.message) || String(e)) })
@@ -533,5 +591,6 @@ export function apply(ctx) {
533
591
 
534
592
  ctx.effect(() => () => {
535
593
  for (const connectionId of Array.from(sessions.keys())) void doDisconnect(connectionId)
594
+ persist()
536
595
  })
537
596
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ssh-terminal",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "SSH 远程终端:在 dsh-better-sidebar 侧边栏里连接远程主机,逐步查看命令与输出。",
5
5
  "author": "sunzhyang1616-ui",
6
6
  "license": "MIT",