Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ What it looks like — the phone shows the exact same UI as your computer, live:
| 🧘 Session persistence | Enter the PIN once and you're set for a long time (login is tied to the computer's dsh web process: as long as it stays up, the phone won't ask again; **after a dsh web restart/update, enter it once more**) |
| ⚡ Real-time sync | Streaming output passes through WebSocket untouched — what the computer renders, the phone renders live; fully interactive both ways; built-in WS heartbeat keep-alive (defeats silent NAT/battery link drops with auto-reconnect) |
| 📱 Mobile-adaptive layout | Narrow screens get a drawer layout automatically (ported from dsh-web-mobile, MIT): sidebar drawer, full-width conversation, safe-area insets, touch optimizations |
| 🧭 Optional right sidebar | Shows the native right-sidebar entry on mobile; disable it for a compact phone header or keep it available alongside the terminal dock on an unfolded display |
| 📁 File browser | The mobile "Files" entries need a host-side explorer panel (a dsh-web-ui component); on stock DSH without it the entries are auto-hidden instead of doing nothing |
| 🗜️ Transfer compression | Large JSON responses are gzip/brotli'd on the fly (17MB session history → ~1MB; brotli quality 6: fast and bandwidth-friendly) — faster loads, less mobile data |
| 🔁 Tunnel auto-restore | After a DSH restart the previously-running public tunnel comes back automatically |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看
| 🧘 会话保持 | 手机输一次密码后**长期免输**(登录状态绑定电脑上的 dsh web 进程:只要它不重启,手机不用再输;**dsh web 重启/更新后需重新输入一次**) |
| ⚡ 实时同步 | 流式输出走 WebSocket 全透传——**电脑上在输出,手机上同步在滚**,可双向操作;内置心跳保活(防路由器 NAT/省电机制静默断链,断线自动重连) |
| 📱 移动端适配 | 窄屏自动变抽屉布局(移植 dsh-web-mobile,MIT):侧栏抽屉、会话全宽、状态栏安全区、触控优化 |
| 🧭 可选右边栏 | 手机端显示原生右边栏入口;普通手机可在设置中关闭以保持紧凑,折叠屏展开后可更方便地同时使用终端底栏和右边栏 |
| 📁 文件浏览 | 移动端「文件浏览」入口需要宿主提供 explorer 面板(dsh-web-ui 组件);官方 DSH 未内置时入口自动隐藏,不会出现"点了没反应" |
| 🗜️ 传输压缩 | 大 JSON 响应自动 gzip/brotli(长会话 17MB → ~1MB,brotli 质量 6:快且省流量),手机加载更快、更省流量 |
| 🔁 隧道自动恢复 | DSH 重启后自动重新拉起之前开着的公网隧道,无需手动重开 |
Expand Down
3 changes: 3 additions & 0 deletions client/api.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// dsh-pocket 设置页签 RPC 契约(client 与 host 共享)
export const POCKET_RPC_CHANNEL = '/dsh-pocket';
export const MOBILE_RIGHTBAR_ATTRIBUTE = 'data-dsh-pocket-mobile-rightbar';
export const MOBILE_RIGHTBAR_EVENT = 'dsh-pocket:mobile-rightbar';

export const POCKET_ENDPOINTS = Object.freeze({
status: 'pocket.status',
Expand All @@ -13,6 +15,7 @@ export const POCKET_ENDPOINTS = Object.freeze({
lanAuthSetEnabled: 'lanAuth.setEnabled',
lanSetOverride: 'lan.setOverride',
lanSetEnabled: 'lan.setEnabled',
mobileRightbarSetEnabled: 'mobile.rightbar.setEnabled',
pinSetCustom: 'pin.setCustom',
pocketReset: 'pocket.reset',
// 移动端「复制文件内容」(issue #17):手机经此 RPC 让主机读取文件正文,
Expand Down
89 changes: 72 additions & 17 deletions client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ var import_react2 = require("react");

// client/api.js
var POCKET_RPC_CHANNEL = "/dsh-pocket";
var MOBILE_RIGHTBAR_ATTRIBUTE = "data-dsh-pocket-mobile-rightbar";
var MOBILE_RIGHTBAR_EVENT = "dsh-pocket:mobile-rightbar";
var POCKET_ENDPOINTS = Object.freeze({
status: "pocket.status",
tunnelStart: "tunnel.start",
Expand All @@ -53,6 +55,7 @@ var POCKET_ENDPOINTS = Object.freeze({
lanAuthSetEnabled: "lanAuth.setEnabled",
lanSetOverride: "lan.setOverride",
lanSetEnabled: "lan.setEnabled",
mobileRightbarSetEnabled: "mobile.rightbar.setEnabled",
pinSetCustom: "pin.setCustom",
pocketReset: "pocket.reset",
// 移动端「复制文件内容」(issue #17):手机经此 RPC 让主机读取文件正文,
Expand Down Expand Up @@ -962,12 +965,8 @@ var MOBILE_CSS = `
}

/* --- Session header on mobile ---
Layout goal: [toggle] [session title] [mode badge] in a row, with the
Session log capsule removed from the header (relocated to the drawer
footer). Stable structural hooks only:
[data-phase] header the session header element
header > :first-child titleRow (titleCluster + utilities)
header > :first-child > :last-child headerUtilities (Session log seat) */
Layout goal: [toggle] [session title] [mode badge] in a row. The optional
rightbar entry uses the shell's stable header-corner hook. */
[data-phase] header {
padding: 8px 12px 0 !important;
}
Expand Down Expand Up @@ -1004,11 +1003,14 @@ var MOBILE_CSS = `
top: 12px !important;
z-index: 2 !important;
}
/* Session log download: gone from the header row on mobile (the utilities
seat holds only the session-log-export capsule). */
[data-phase] header > :first-child > :last-child {
/* The native rightbar entry is visible by default. Users who prefer the
compact header can turn it off in Pocket settings. */
body[data-dsh-pocket-mobile-rightbar="off"] [data-conversation-header-corner] {
display: none !important;
}
body:not([data-dsh-pocket-mobile-rightbar="off"]) [data-mobile-nav="files"] {
right: 44px !important;
}

/* --- Settings dialog on mobile ---
Desktop: 800px two-column flex (188px nav + content). Mobile: a
Expand Down Expand Up @@ -1615,6 +1617,30 @@ function mobileApply(ctx) {
} };
}
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-mobile-nav: dictionaries");
ctx.effect(() => {
let active = true;
const applyEnabled = (enabled) => {
document.body?.setAttribute(MOBILE_RIGHTBAR_ATTRIBUTE, enabled ? "on" : "off");
};
const onChange = (event) => {
applyEnabled(event.detail?.enabled === true);
};
const load = async () => {
try {
const result = await ctx.connection.rpc.call(POCKET_RPC_CHANNEL, POCKET_ENDPOINTS.status, {});
if (active) applyEnabled(result?.ok === true ? result.value?.mobileRightbarEnabled !== false : true);
} catch {
if (active) applyEnabled(true);
}
};
window.addEventListener(MOBILE_RIGHTBAR_EVENT, onChange);
void load();
return () => {
active = false;
window.removeEventListener(MOBILE_RIGHTBAR_EVENT, onChange);
document.body?.removeAttribute(MOBILE_RIGHTBAR_ATTRIBUTE);
};
}, "dsh-mobile-nav: optional right sidebar");
ctx.effect(() => {
const tag = document.createElement("style");
tag.dataset.plugin = "@dsh-external/dsh-mobile-nav";
Expand Down Expand Up @@ -1799,8 +1825,8 @@ function mobileApply(ctx) {
ctx.effect(() => {
if (!narrow.matches) return () => {
};
const PHRASES = ["加载提供方目录失败", "Settings are unavailable in this browser"];
const NOTICE = "手机上不支持模型设置,请去电脑端修改设置";
const PHRASES = ["\u52A0\u8F7D\u63D0\u4F9B\u65B9\u76EE\u5F55\u5931\u8D25", "Settings are unavailable in this browser"];
const NOTICE = "\u624B\u673A\u4E0A\u4E0D\u652F\u6301\u6A21\u578B\u8BBE\u7F6E\uFF0C\u8BF7\u53BB\u7535\u8111\u7AEF\u4FEE\u6539\u8BBE\u7F6E";
const findDeepest = (el) => {
let deepest = el;
for (const child of el.querySelectorAll("*")) {
Expand All @@ -1821,9 +1847,7 @@ function mobileApply(ctx) {
const observer = new MutationObserver(patch);
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
patch();
return () => {
observer.disconnect();
};
return () => observer.disconnect();
}, "dsh-mobile-nav: replace model-settings load error with mobile hint");
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
name: "conversation.session.header.actions",
Expand Down Expand Up @@ -1891,7 +1915,7 @@ var zh2 = {
"resetGo": "\u6062\u590D",
"resetIntro": "\u8BBE\u7F6E\u641E\u51FA\u95EE\u9898\u65F6\u7684\u4E34\u65F6\u515C\u5E95\uFF1A\u6E05\u7A7A\u672C\u673A\u914D\u7F6E\u5E76\u91CD\u8BBE\u968F\u673A\u5BC6\u7801\uFF08DSH \u7684\u4F1A\u8BDD\u3001\u6A21\u578B\u3001\u63D2\u4EF6\u914D\u7F6E\u4E0D\u53D7\u5F71\u54CD\uFF09",
"resetTitle": "\u26A0\uFE0F \u786E\u8BA4\u6062\u590D\u51FA\u5382\u8BBE\u7F6E\uFF1F",
"resetBody": "\u5C06\u6E05\u7A7A\u5E76\u6062\u590D\u9ED8\u8BA4\uFF1A\n\u2460 \u5F00\u5173\uFF1A\u5C40\u57DF\u7F51\u8BBF\u95EE=\u5F00\u3001\u8BBF\u95EE\u5BC6\u7801=\u5F00\u3001\u5C40\u57DF\u7F51\u5730\u5740=\u81EA\u52A8\n\u2461 \u516C\u7F51\uFF1A\u6A21\u5F0F\u56DE\u5230\u968F\u673A\u57DF\u540D\uFF0C\u6E05\u7A7A Tunnel Token \u4E0E\u56FA\u5B9A\u57DF\u540D\uFF0C\u5E76\u5173\u95ED\u6B63\u5728\u8FD0\u884C\u7684\u516C\u7F51\n\u2462 \u5BC6\u7801\uFF1A\u516C\u7F51\u548C\u5C40\u57DF\u7F51\u90FD\u6362\u6210\u65B0\u7684\u968F\u673A 8 \u4F4D\u5BC6\u7801\uFF08\u65E7\u5BC6\u7801\u7ACB\u5373\u4F5C\u5E9F\uFF0C\u624B\u673A\u9700\u91CD\u65B0\u8F93\u5165\uFF09\n\nDSH \u81EA\u8EAB\u7684\u4F1A\u8BDD\u3001\u6A21\u578B\u3001\u63D2\u4EF6\u914D\u7F6E\u4E0D\u53D7\u5F71\u54CD\uFF1B\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002",
"resetBody": "\u5C06\u6E05\u7A7A\u5E76\u6062\u590D\u9ED8\u8BA4\uFF1A\n\u2460 \u5F00\u5173\uFF1A\u5C40\u57DF\u7F51\u8BBF\u95EE=\u5F00\u3001\u8BBF\u95EE\u5BC6\u7801=\u5F00\u3001\u624B\u673A\u7AEF\u53F3\u8FB9\u680F=\u5F00\u3001\u5C40\u57DF\u7F51\u5730\u5740=\u81EA\u52A8\n\u2461 \u516C\u7F51\uFF1A\u6A21\u5F0F\u56DE\u5230\u968F\u673A\u57DF\u540D\uFF0C\u6E05\u7A7A Tunnel Token \u4E0E\u56FA\u5B9A\u57DF\u540D\uFF0C\u5E76\u5173\u95ED\u6B63\u5728\u8FD0\u884C\u7684\u516C\u7F51\n\u2462 \u5BC6\u7801\uFF1A\u516C\u7F51\u548C\u5C40\u57DF\u7F51\u90FD\u6362\u6210\u65B0\u7684\u968F\u673A 8 \u4F4D\u5BC6\u7801\uFF08\u65E7\u5BC6\u7801\u7ACB\u5373\u4F5C\u5E9F\uFF0C\u624B\u673A\u9700\u91CD\u65B0\u8F93\u5165\uFF09\n\nDSH \u81EA\u8EAB\u7684\u4F1A\u8BDD\u3001\u6A21\u578B\u3001\u63D2\u4EF6\u914D\u7F6E\u4E0D\u53D7\u5F71\u54CD\uFF1B\u6B64\u64CD\u4F5C\u4E0D\u53EF\u64A4\u9500\u3002",
"resetConfirm": "\u786E\u8BA4\u6062\u590D",
"resetDone": "\u2705 \u5DF2\u6062\u590D\u51FA\u5382\u8BBE\u7F6E\uFF1A\u8BBE\u7F6E\u5DF2\u6E05\u7A7A\uFF0C\u5BC6\u7801\u5DF2\u6362\u65B0\uFF08\u624B\u673A\u9700\u91CD\u65B0\u8F93\u5165\uFF09",
"resetFailed": "\u274C \u6062\u590D\u5931\u8D25\uFF0C\u8BF7\u91CD\u8BD5",
Expand Down Expand Up @@ -1920,6 +1944,8 @@ var zh2 = {
"pinCustomHint": "\u81EA\u5B9A\u4E49\u540E\u5F00\u542F\u516C\u7F51\u4E0D\u518D\u81EA\u52A8\u6362\u65B0",
"lanPinOff": "\u{1F513} \u5BC6\u7801\u5DF2\u5173\u95ED\uFF1A\u626B\u7801\u76F4\u8FDE\uFF0C\u65E0\u9700\u5BC6\u7801\uFF08\u4EC5\u540C\u4E00\u5C40\u57DF\u7F51\u8BBE\u5907\u53EF\u8BBF\u95EE\uFF1B\u516C\u7F51\u4ECD\u8981\u5BC6\u7801\uFF09",
"lanStarting": "\u4EE3\u7406\u672A\u5C31\u7EEA\u2026",
"mobileRightbar": "\u624B\u673A\u7AEF\u53F3\u8FB9\u680F",
"mobileRightbarHint": "\u663E\u793A\u539F\u751F\u53F3\u8FB9\u680F\u5165\u53E3\uFF1B\u666E\u901A\u624B\u673A\u53EF\u6309\u9700\u5173\u95ED\uFF0C\u6298\u53E0\u5C4F\u5C55\u5F00\u540E\u4F7F\u7528\u66F4\u65B9\u4FBF",
"wanTitle": "\u{1F310} \u516C\u7F51\uFF08\u4EBA\u5728\u5916\u9762\uFF09",
"wanHint": "\u4EFB\u4F55\u7F51\u7EDC\u626B\u7801\u5373\u7528\uFF08URL \u6BCF\u6B21\u91CD\u542F\u81EA\u52A8\u6362\u65B0\uFF09",
"wanPin": "\u{1F510} \u8BBF\u95EE\u5BC6\u7801\uFF1A{pin}\uFF08\u6BCF\u6B21\u5F00\u542F\u516C\u7F51\u53D8\u65B0\uFF1B\u624B\u673A\u6253\u5F00\u94FE\u63A5\u9700\u8F93\u5165\u6B64\u5BC6\u7801\uFF09",
Expand Down Expand Up @@ -1987,7 +2013,7 @@ var en2 = {
"resetGo": "Reset",
"resetIntro": "Temporary fallback when settings break: clear local config and re-roll random PINs (DSH sessions, models and plugin config are untouched)",
"resetTitle": "\u26A0\uFE0F Confirm factory reset?",
"resetBody": "This clears and restores defaults:\n\u2460 Switches: LAN access on, access PIN on, LAN address auto\n\u2461 Public: mode back to random URL, Tunnel Token and fixed domain cleared, and any running tunnel is stopped\n\u2462 PINs: both public and LAN become new random 8-character PINs (old ones stop working; the phone must re-enter)\n\nYour DSH sessions, models and plugin config are untouched. This cannot be undone.",
"resetBody": "This clears and restores defaults:\n\u2460 Switches: LAN access on, access PIN on, mobile right sidebar on, LAN address auto\n\u2461 Public: mode back to random URL, Tunnel Token and fixed domain cleared, and any running tunnel is stopped\n\u2462 PINs: both public and LAN become new random 8-character PINs (old ones stop working; the phone must re-enter)\n\nYour DSH sessions, models and plugin config are untouched. This cannot be undone.",
"resetConfirm": "Reset",
"resetDone": "\u2705 Factory reset done: settings cleared and PINs re-rolled (re-enter the PIN on your phone)",
"resetFailed": "\u274C Reset failed \u2014 please retry",
Expand Down Expand Up @@ -2016,6 +2042,8 @@ var en2 = {
"pinCustomHint": "custom PINs are not rotated on tunnel start",
"lanPinOff": "\u{1F513} PIN off \u2014 scan & go, no PIN (LAN devices only; public still requires PIN)",
"lanStarting": "Proxy starting\u2026",
"mobileRightbar": "Mobile right sidebar",
"mobileRightbarHint": "Show the native right-sidebar entry; disable it for a compact phone header or keep it on for an unfolded display",
"wanTitle": "\u{1F310} Anywhere (public)",
"wanHint": "Scan from any network (the URL changes on every restart)",
"wanPin": "\u{1F510} PIN: {pin} (changes each time the tunnel is enabled; required on the phone)",
Expand Down Expand Up @@ -2074,6 +2102,11 @@ var styles = {
qr: { width: 220, height: 220, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2,#e5e7eb)", margin: "8px 0" },
warn: { color: "var(--dsw-alias-state-warn-primary,#b45309)", fontSize: 12, lineHeight: 1.5 }
};
function applyMobileRightbarSetting(enabled) {
const on = enabled !== false;
document.body?.setAttribute(MOBILE_RIGHTBAR_ATTRIBUTE, on ? "on" : "off");
window.dispatchEvent(new CustomEvent(MOBILE_RIGHTBAR_EVENT, { detail: { enabled: on } }));
}
function PocketSettingsTab({ rpcCall, t }) {
const [status, setStatus] = (0, import_react2.useState)(null);
const [busy, setBusy] = (0, import_react2.useState)(false);
Expand All @@ -2097,6 +2130,7 @@ function PocketSettingsTab({ rpcCall, t }) {
try {
const s = await call(POCKET_ENDPOINTS.status, {});
setStatus(s);
applyMobileRightbarSetting(s.mobileRightbarEnabled);
setTunnelState(s.tunnelState ?? null);
if (s.desktop) setIsDesktop(true);
if (s.restartNotice) {
Expand Down Expand Up @@ -2243,7 +2277,9 @@ function PocketSettingsTab({ rpcCall, t }) {
setBusy(true);
setError(null);
try {
setStatus(await call(POCKET_ENDPOINTS.pocketReset, { confirm: true }));
const next = await call(POCKET_ENDPOINTS.pocketReset, { confirm: true });
setStatus(next);
applyMobileRightbarSetting(next.mobileRightbarEnabled);
setTunnelCfg(null);
setCustomPin(null);
setAdvOpen(false);
Expand All @@ -2269,6 +2305,16 @@ function PocketSettingsTab({ rpcCall, t }) {
} catch {
}
};
const setMobileRightbar = async (on) => {
try {
const r = await call(POCKET_ENDPOINTS.mobileRightbarSetEnabled, { on });
const enabled = r.mobileRightbarEnabled === true;
setStatus((s) => ({ ...s, mobileRightbarEnabled: enabled }));
applyMobileRightbarSetting(enabled);
} catch (err) {
setError(err.message);
}
};
const [lanToggleOpen, setLanToggleOpen] = (0, import_react2.useState)(null);
const requestLanToggle = (on) => setLanToggleOpen(on);
const confirmLanToggle = async () => {
Expand Down Expand Up @@ -2612,6 +2658,15 @@ function PocketSettingsTab({ rpcCall, t }) {
) : null
) : null
),
(0, import_react2.createElement)(
"div",
{ style: styles.block },
row(
t("mobileRightbar"),
Switch(status?.mobileRightbarEnabled !== false, () => setMobileRightbar(status?.mobileRightbarEnabled === false)),
(0, import_react2.createElement)("div", { style: { ...styles.muted, marginTop: 6 } }, t("mobileRightbarHint"))
)
),
error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${errText(error)}`) : null,
// 恢复出厂设置:设置出问题时的临时兜底(最底部,避免误触)
(0, import_react2.createElement)(
Expand Down
Loading